Excerpted with permission from The Book of VB .NET, by Matthew MacDonald, No Starch Press, Copyright 2002.
A traditional computer book usually discusses migration-thequestion of how to work with existing data files-early on. In manyprograms, including office productivity software such as MicrosoftWord, working with files from a previous version is just as easy as creatingnew documents. Unfortunately, Visual Basic .NET doesn’t work thisway. As you’ve discovered in the preceding thirteen chapters of The Book of VB .NET, Visual Basic .NET introduces an entirely new programming framework called .NET. To programwell in .NET, you have to surrender many time-honored habits and adopt a new,object-oriented style. When it comes to migration, the question is not how youcan import your existing applications, but whether you should at all.
This article surveys the major changes between Visual Basic 6 andVB .NET, and in doing so, provides a nice summary of the .NET philosophy.We’ll take a look at t he Upgrade Wizard, and see how a sample VB 6 projectweathers the transition to .NET. You’ll also learn about how to integrate legacycode, which is useful when migration is simply too painful. Specifically, we’llexamine how you can access COM components and ActiveX controls-the “oldworld” of programming-in a .NET project. These techniques allow you to makemaximum use of your existing code, while pursuing new development inVisual Basic .NET. Integration and interaction may not always be convenient,but it will be an essential ingredient of .NET programming for the next fewyears. It will also be the best strategy in many cases where migration is not possibleand re-coding is just too time-consuming.
Introducing .NET Migration
Visual Basic .NET compromises backward compatibility in a number of troublesomeways. Hopefully, the advances you’ve seen over the course of this book willmake these complications worthwhile. Once you’re in the .NET world, there’sreally no easy way back.
Visual Basic 6 in the .NET World
Visual Basic 6 is a mature, well-developed programming environment. There’sno immediate need to replace a VB 6 program (and there’s no shame in maintaininga program in Visual Basic 6). Think of Visual Basic 6 as a well-worn, austerelanguage at the end of its evolution. It still has a nice autumn glow to it, butit will gradually fade into disuse. On the other hand, Visual Basic .NET is ayoung, dynamic upstart at the beginning of its life cycle. As with any new program,changes will abound in VB .NET over the next few years. However, it’sVisual Basic .NET, not Visual Basic 6, that will power the next generation of VBapplications.
A key theme in this article is breaking down the barriers between VB 6and VB .NET. There may not be an easy migration solution for many of yourprojects. Instead, you may need to start over again in .NET, and make heavy useof the COM compatibility layer that is built into .NET, in order to use your existingVB components.
There is no file compatibility between Visual Basic .NET and earlier versions. InChapter 3 of The Book of VB .NET, we explored the new file formats used in VB .NET projects. Thesefiles have different extensions (.vb instead of .mod or .frm, and .vbproj insteadof .vbp). VB .NET files also use a block structure that allows modules, forms,and classes to be combined in a single file, while Visual Basic 6 used a specialsyntax for its form files (as explained in Chapter 4 of The Book of VB .NET).
The syntax of the language of itself has been updated, and the alterationsrange from minor cosmetic changes to entirely new concepts such as namespaces.All these technical details have a single result: There is no way to open aVisual Basic 6 project in VB .NET. Instead, you have to migrate the project.
The Upgrade Wizard
Migration is a special procedure carried out by Visual Studio .NET’s built- inUpgrade Wizard. Essentially, the Upgrade Wizard scans through every line ofevery file in your project. It examines the line, analyzes it for a variety of potentialproblems, and tries to assign an equivalent VB .NET statement. When dealingwith simple programs (for example, utilities that have only one window, orthat have the majority of their capabilities concentrated in a few core procedures),it does remarkably well. However, for complex programs that manage asophisticated user interface and a large amount of data, it works almost embarrassinglybadly. In these cases, migration usually isn’t a feasible option.
Migrating a Simple Project
Here’s a relatively simple VB 6 project using several concepts that are foreign toVB .NET. The full project is available with the samples for this article.
It starts with a simple startup procedure, contained in a module file:
' VB 6 code.Public Sub Main() frmSplash.Show vbModal frmMain.ShowEnd Sub
The first line launches a window modally-a splash screen with a companylogo. The window uses a timer that unloads itself automatically after a setamount of time:
' VB 6 code.Private Sub tmrClose_Timer() Unload MeEnd Sub
Then the code continues to the second line, launches the main program windownonmodally, and allows the Main subroutine to end. The main window consistsof a simple form with an MSFlexGrid control and a single button. When theuser clicks on the button, a short routine runs, retrieves information from a databasetable, and uses that information to fill the grid, as shown in Figure 14-1.
Figure 14-1: A VB 6 program
The code is quite straightforward. The Form_Load event handler configuresthe grid appropriately, and opens a database connection (using the form-levelvariable con):
' VB 6 code.Private con As ADODB.Connection
Private Sub Form_Load() grid.Cols = 2 grid.Rows = 0 grid.ColWidth(1) = 3000 grid.ColAlignment(0) = 1
Set con = New ADODB.Connection con.ConnectionString = "Provider=SQLOLEDB.1;Data Source=localhost;" & _ "Initial Catalog=Northwind;Integrated Security=SSPI" con.OpenEnd Sub
Of course, opening a database connection in the Form_Load event andclosing it in the Form_Unload event is an extremely bad design practice,because it ties up a limited database connection for an undetermined amount oftime. The individual using the computer could easily forget, leave the computerrunning, and go on holiday, tying up the connection indefinitely. However,there’s nothing invalid in this code (and probably nothing unusual either).
The button event handler contains the following code:
' VB 6 code.Private Sub cmdFill_Click()
Me.MousePointer = vbHourglass grid.Rows = 0
Dim rs As ADODB.Recordset Set rs = con.Execute("SELECT * From Customers")
Dim i As Integer Do While rs.EOF <> True grid.AddItem (rs("CustomerID") & vbTab & rs("ContactName")) rs.MoveNext Loop
Me.MousePointer = vbDefault
Don’t spend too much time analyzing this code; it uses the ADO library,which is the connection-based predecessor to ADO.NET. ADO works quite a bitdifferently, using a live connection and a MoveNext method to access all theinformation in a Recordset (instead of a Rows collection in a DataSet). This techniqueis similar to the way you use ADO.NET’s special DataReader object.
You might also notice that this code uses the MousePointer property tothoughtfully turn the user’s mouse pointer into an hourglass, indicating that adatabase operation is underway and that no other user action can be taken untilthe operation is finished.
One additional frill is the form’s automatic resizing code:
' VB 6 code.Private Sub Form_Resize() grid.Width = Me.Width - 350 grid.Height = Me.Height - 1200 cmdFill.Top = Me.Height - 1000 cmdFill.Left = (Me.Width - cmdFill.Width - 60) 2End Sub
While this code can’t stop the form from being made too small, like ourVB .NET code can, it still manages to ensure that the button and grid use theappropriate amount of space. The drawback is the introduction of hard-codedvalues, and generally ugly code, into the Form_Resize event handler. Interestingly,this code is the manual equivalent of two different VB .NET concepts wetake for granted. The grid’s size changes, but its position does not; this is anexample of manual docking. The command button’s position changes, but itssize is constant, which is an example of anchoring.
Clearly, this is an extremely simple program. However, it does have someaspects that can pose difficulty in the .NET world. They are:
- The use of ADO, which is a database technology built on COM.(Remember, COM isn’t native to .NET.)
- The use of the MSFlexGrid control, which is an ActiveX control. Like allActiveX controls, it’s also based on COM, and so there is no directequivalent in the .NET class library.
- The treatment of forms. In Visual Basic .NET, forms are classes, and youhave to create an instance of a form before using it. Clearly, the startuproutine in our example doesn’t follow these rules. If you’d known that thisprogram was destined for VB .NET migration, you could have programmedaccordingly by dynamically creating forms, even in your VB 6 code.However, if you haven’t specifically planned for this step, or if you aredealing with an older application, this technique probably hasn’t been used.
Design-wise, there are a couple of other potential problems in this example,such as the way the database connection is held open. However, you can create apoorly designed program in VB .NET with the same ease that you could inVisual Basic 6. This example of poor programming should not affect the migrationprocess.
Importing the Project
To import this project into Visual Basic .NET, all you need to do is open the.vbp file. The Upgrade Wizard will automatically appear, as seen in Figure 14- 2.
Figure 14-2: The Upgrade Wizard
At this point, it’s just a matter of clicking on Next several times, and theconversion will begin. Along the way, you will be prompted to choose a newdirectory where the .NET version of our project will be stored (see Figure 14- 3).
Figure 14-3: Creating the new .NET project
Remember, this a complex migration, not a simple File Open operation.The whole process is surprisingly slow, as you’ll notice with any real-life application.Even with this simple program, Visual Studio .NET may still take a coupleof minutes to complete the migration.
Once the process is completed, the first thing you should do is read themigration report that has been created for you. You can find this as an HTMLfile (_UpdateReport.htm) in the Solution Explorer. If you double- click on it,you’ll see an impressive file-by-file analysis of the project (Figure 14- 4). Eachsection lists migration problems and warnings, and can be expanded or collapsedindividually.
Figure 14-4: The upgrade report
In the case of the simple ADOTest project, no errors are reported, butthree warnings are flagged. You can read a full description of these issues byclicking the provided hyperlink, which takes you to a help topic. Usually, though,you’ll want to investigate the code yourself. The appropriate area will be markedwith a comment and another hyperlink to the help topic:
' UPGRADE_WARNING: Form event frmMain.Unload has a new behavior.' Click for more: ms-help://MS.MSDNVS/vbcon/html/vbup2065.htmPrivate Sub frmMain_Closed(ByVal eventSender As System.Object, _ ByVal eventArgs As System.EventArgs) Handles MyBase.Closed con.Close()End Sub
In this case, the Wizard has changed the event handler for the Unload eventto the corresponding Close event. (The Wizard has also taken care of additionaldetails, such as adding the Handles clause and changing the event handler signatureto the .NET standard.)
The second warning is a similar false alarm that alerts us that the Resizeevent may occur when the form is first initialized. The third warning in themigration report informs us, rather cryptically, that the application will endwhen the Main subroutine ends. This warning highlights another differencebetween VB .NET and VB 6: In Visual Basic 6, a program wouldn’t end untilevery window was closed. If you used a startup routine to begin your program,the startup routine could end and leave the other windows running to take careof the rest of the program. In VB .NET, applications work a little differently.
If you use a startup subroutine in Visual Basic .NET, the program will closeas soon as the subroutine ends, even if other windows are still open (somewhatlike using the End statement).
Module StartupModule Public Sub Main() frmSplash.DefInstance.ShowDialog()
' Program will end immediately after executing the next line. frmMain.DefInstance.Show() End SubEnd Module
This migration problem could have been avoided if the original programhad displayed both windows modally from the Main subroutine. In that case, thesubroutine would pause until the frmMain window had closed, rather than endingearly. To fix this minor problem, all you need to do is make this modification:
Module StartupModule Public Sub Main() frmSplash.DefInstance.ShowDialog() frmMain.DefInstance.ShowDialog() End SubEnd Module
Forms and the Default Instance
You may notice another unusual feature in this portion of the code: the referenceto DefInstance. A logical .NET startup routine would look more like this:
Public Sub Main() ' Create and show first window. Dim Splash As New frmSplash Splash.ShowDialog()
' Create and show second window. Dim Main As New frmMain Main.ShowDialog()End Sub
The .NET Upgrade Wizard doesn’t have enough intelligence to make thischange. In fact, the problem is potentially a lot more complicated. In traditionalVB code, the Wizard really has no way of knowing when you are referring to aform, and when you are trying to create it. In VB 6, a form is loaded automaticallythe first time it is referred to in code, even if it isn’t displayed. This systemallows the following kind of logic to work:
' This is VB6 code.frmMain.TextBox1.Text = "Hi" ' The form is created and loaded automatically.frmMain.Show ' Now the form is displayed.
To emulate this logic, the Upgrade Wizard adds a special block of code toevery form under the collapsed Upgrade Support region that works like this:
Private Shared m_vb6FormDefInstance As frmMainPrivate Shared m_InitializingDefInstance As Boolean
Public Shared Property DefInstance() As frmMain Get If m_vb6FormDefInstance Is Nothing _ OrElse m_vb6FormDefInstance.IsDisposed Then m_InitializingDefInstance = True m_vb6FormDefInstance = New frmMain() m_InitializingDefInstance = False End If DefInstance = m_vb6FormDefInstance End Get
Set m_vb6FormDefInstance = Value End SetEnd Property
The logic here is quite interesting. It works like this:
- Every form has a shared property called DefInstance. Because it is shared, itcan be accessed even without creating a form instance. (Also, because it isshared, every class uses the same code for this property, and returns thesame result.)
- When you retrieve the DefInstance property in your code, the Property Getprocedure checks the form’s private m_vb6FormDefInstance sharedvariable. This variable is designed to hold a reference to the current form(in our example, frmMain). If this variable hasn’t yet been initialized, theProperty Get procedure creates the form automatically, effectivelymimicking the VB 6 form behavior.
- The end result is that whenever a part of your program uses theDefInstance property, it gets the instance of the form stored in them_vb6FormDefInstance variable. If necessary, the form is loaded on thespot automatically.
This special block of “upgrade support” code is a trick that allows you touse forms in the VB 6 way, as long as you call the form’s default instance insteadof just the form’s class name. If you have the time, it may make sense to gothrough your code, remove the default instance logic, and recreate your formproperly. However, this isn’t strictly necessary.
TIP Incidentally, this code violates an important recommendation of object-oriented programming: namely, that retrieving information from a property procedure should never change the state of the object. In this case, retrieving the property of anuninitialized form causes it to be created.
The Wizard makes some other modifications. For example, it converts theform’s MousePointer property using the equivalent .NET code, shown here:
Me.Cursor = System.Windows.Forms.Cursors.WaitCursor
This trick is also fairly impressive. Clearly, the Wizard needs to draw on acomprehensive database of some sort that stores VB 6 properties, and their.NET equivalents, because the .NET class library really has little in common withthe traditional Forms engine.
Another change appears in the Resize event handler, which now usesslightly more cumbersome code to convert the hard-coded values to pixels, themeasurement used in VB .NET. A sample line is shown here:
grid.Width = VB6.TwipsToPixelsX(VB6.PixelsToTwipsX(Me.Width) - 350)
So far, the Upgrade Wizard has done a great job. If you try running theprogram, you’ll be surprised to find that it works perfectly. The old-fashionedresizing code and database code works without a hitch. Some better options areavailable for improving your program to take advantage of VB .NET, particularlywith the resizing code, but the original code still works as designed.
COM Components in .NET
Perhaps the most impressive aspect of this conversion is the fact that the ADOcomponents work exactly as they did in Visual Basic 6, even though they areCOM-based. If you look at the references for this project, you’ll find that anADO reference has been added (as shown in Figure 14-5). This reference usesthe ADODB.dll wrapper distributed with .NET.
Figure 14-5: The ADODB reference
This file isn’t a part of the original project-in fact, it’s part of the .NETframework. When you use an ADO object in your .NET code, you are reallyusing an object from the ADODB.dll file. This file is a normal, managed .NETassembly. Behind the scenes, it uses .NET’s COM Interop features to create thecorresponding COM object and pass along instructions according to your program’sactions. In other words, ADODB.dll provides a thin translation layerbetween your program and traditional ADO, bridging the gap between COMand .NET. In most cases, this trick works perfectly well, causing only a slight performancedegradation. The translation layer ensures that you can continueusing COM components for the foreseeable future.
Of course, you don’t need to migrate a project in order to use this built- incompatibility layer. Visual Studio .NET makes it just as easy as adding a reference.First, right-click on References in the Solution Explorer, and then choose AddReference. Next, select the COM tab (as shown in Figure 14-6). (There will probablybe a slight delay while Visual Studio .NET loads all the system information.)
Figure 14-6: Adding a COM reference
Double-click on an item to add it to the selected list. When you click onOK, .NET will search for a primary Interop assembly, which is a .NET translationassembly created by the appropriate vendor. If it can’t find one, you willreceive an ominous warning message and be given the option to create yourown wrapper automatically. The DLL wrapper file will then be added to yourproject’s support files in the obj subdirectory.
Believe it or not, this COM interoperability is almost seamless, and it willsave you a lot of headaches. Just remember that when you want to install yourprogram on another computer, you will need to copy these Interop files. (Youwill also need to install and registered the appropriate COM files; .NET setupprojects won’t detect these components or set them up automatically.)
TIP It’s perfectly reasonable to create your own Interop assemblies for third-party components when developing and testing an application. However, when you deploy the application, you should use the Interop assembly provided by the appropriate vendor (like the ADODB.dll assembly provided by Microsoft, and included in the .NET framework). Thisway, you are guaranteed that the .NET layer works properly with all aspects of thecomponent.
ActiveX Components in .NET
ActiveX components work in essentially the same way as COM objects, sinceevery ActiveX component is really just a special type of COM object. You’ll recallthat our original VB 6 test program uses an MSFlexGrid control, which is anActiveX control with no obvious .NET equivalent. The .NET framework doesnot include a primary Interop assembly for the MSFlexGrid control, so theUpgrade Wizard creates one automatically.
To witness what’s really happening, select Project Show All Files from themain menu. Now if you expand the bin or obj directory, you’ll find a file with aname like AxInterop.MSFlexGridLib_1_0.dll. The “Ax” at the beginning of thename identifies the fact that this Interop assembly derives from the System.Windows.Forms.AxHost class, as do all the Interop files for ActiveX controls. It’s anordinary .NET managed assembly that has the built-in smarts to talk to the originalActiveX component through COM (Figure 14-7).
Figure 14-7: The .NET wrapper for MSFlexGrid
TIP You can find more information about the generated wrapper assembly by double-clickingon it. Expand the tree until you reach the last (version) element, and double-click on thatfor information about the current version of the file:
This wrapper is slightly different from the ADO wrapper, because it representsthe actual .NET control that is placed on your Windows form. You can verifythis fact by examining the automatically generated designer code:
Public WithEvents grid As AxMSFlexGridLib.AxMSFlexGridMe.grid = New AxMSFlexGridLib.AxMSFlexGrid
This control quietly communicates with the original ActiveX control, andmimics its behavior on the form. You can see this in action by examining the followingdesigner code, in which the wrapper class retrieves state informationfrom the ActiveX control instance that it contains:
grid.OcxState = CType(resources.GetObject("grid.OcxState"), _System.Windows.Forms.AxHost.State)
Interestingly, this control is a blend of the old and the new. Because AxHostinherits from the base Control class, it supports all the properties that a typicalcontrol does. For example, the designer code sets the size and location like this:
Me.grid.Size = New System.Drawing.Size(297, 137)Me.grid.Location = New System.Drawing.Point(8, 8)
The dual nature of the control also explains why the properties and methodsin some portions of code now have slightly modified names:
grid.set_ColWidth(1, 3000) ' This was grid.ColWidth(1) = 3000grid.set_ColAlignment(0, 1) ' This was grid.ColAlightment(0) = 1
This syntax would obviously not be valid in Visual Basic 6. However, theAxMSFlexGrid control is a real .NET control. It just requires the use of anActiveX control behind the scenes. This also means that you can use .NETevent-handling procedures, including the Handles keyword and the AddHandlerstatement, without any difficulty. Of course, you might have trouble determiningthe correct parameter list, because there won’t be any information on thenewly created control in the .NET MSDN help library. To track down this information,select View / Other Windows / Object Browser. You can then take alook inside the corresponding AxInterop.MSFlexGridLib_1_0.dll assembly, andfind out exactly what .NET members Visual Studio .NET has created for you, asshown in Figure 14-8.
Figure 14-8: The Object Browser
The end result of our migration example is an MSFlexGrid control thatworks perfectly well. Keep in mind that in many cases, it might be better to usea true .NET control instead, to ensure best performance and easiest deployment.However, in this case the ActiveX control does not cause any obviousproblems.
In the future, there will certainly be a flood of .NET controls. Until then,you may need to rely on these built- in Interop features in order to reuse yourexisting ActiveX controls. For example, there are currently no equivalents to theold Microsoft Charting and Internet Explorer Browser components. In somecases, it might be easy to build better versions of these controls using the functionsand features available in the .NET class library, or even to substitute similar.NET controls. But having the Interop features is a huge advantage, especiallywhen using unusual or one-of-a-kind controls.
To add an ActiveX control to one of your projects, right- click on the toolboxand select Customize Toolbox (Figure 14-9). Then select the COM Componentstab, find the appropriate control on the list, and put a checkmark next to it.
Figure 14-9: Adding a wrapped ActiveX control
The Interop assembly won’t actually be created until you place the controlon a form. Then you can work with the control as though you were using VisualBasic 6. For example, when you resize the ActiveX control or change its properties,its appearance will be automatically updated. Similarly, you can configureproperties through a custom tabbed property window, if such a window is providedwith the control (Figure 14-10).
Figure 14-10: Design-time support for ActiveX controls remains
It’s important to remember that the COM and ActiveX Interop features canbe used even when you’re not migrating a project. In fact, it often makes mostsense for you to use these features in order to continue using as many elementsas you can from your existing programs without rewriting them. Creating a.NET program that makes heavy use of existing COM components will oftenwork much better than trying to migrate the code for all these components intothe .NET world.
Migrating a Sophisticated Project
The scenario we’ve worked with so far is ideal in many respects. While VisualStudio .NET’s migration features are impressive, they rarely work as well asdescribed with a real application. In fact, for many programs they will be essentiallyunworkable.
The next sample project is a typical midsize Visual Basic 6 program. It consistsof about three-dozen forms, most of which are used to provide differentproduct listings. The user selects any combination of products to create a pricedorder, and then either emails the order to the appropriate company (by meansof a built-in COM component for MAPI email), or creates a professional lookingprintout that can be faxed or kept for reference. Other options are available,such as the ability to preview the report with a RichText control, and the abilityto switch between different price sets (US dollars and UK pounds, for example).We’ll call this program VB6OrderMaker.
Figure 14-11 shows it in action:
Figure 14-11: The VB6OrderMaker
This program is far too long to be reproduced in its entirety, and youwouldn’t gain much from seeing the details. Overall, it’s an intricate program(as it has a lot of details), but it is fairly straightforward. Best practices are notalways followed, but the design is not overly haphazard. It doesn’t make use ofclasses or interfaces. Ultimately, it’s a good example of a typical, slightly old-fashioned Visual Basic 6 program.
Unfortunately, it’s difficult to tell exactly how much of the program’s functionalityhas made the jump to the .NET world. There are so many .NET incompatibilitieswoven into the fabric of the program that the migrated projectwould never compile, even if hours were spent reworking it. It’s safe to say thatre-creating this program from scratch would be far more successful than migratingthe existing program.
The migration report contains 360 errors, 2889 warnings, and 3249 totalissues, making for an average of about 80 issues per module (Figure 14-12shows a partial list). Even more amusing, the Upgrade Wizard has spent severalhours struggling to make the changes needed for .NET. The only file that hassurvived without any problems is the module used to run functions from theWindows API.
Rather than trying to step through this mess, it makes sense to consider themajor issues that have derailed this migration attempt, and examine how theycan be solved or avoided. (You’ll find that similar issues will occur with manycomplex migration projects.)
Figure 14-12: Partial list of VB6OrderMaker errors
Before we get into the details of this troublesome project, let’s review sometypes of programs that really are not fit for any type of migration to .NET. Generally,you shouldn’t (or can’t) even try to migrate a project if it falls under oneof these categories:
- A complex distributed application with several different layers of objectscommunicating through COM. Despite Microsoft’s optimism, this type ofprogram will rarely survive the transition. However, on the good side, youcan probably start by creating a .NET client that interacts with some or allof the other COM components, allowing you to ease into the migrationprocess slowly, and continue conversions one component at a time.
- A Visual Basic 5 program that hasn’t made the transition to Visual Basic 6.For all its differences, VB 6 is still one step closer to VB .NET than anyearlier release of Visual Basic. Make the change to Visual Basic 6 beforegetting more ambitious.
- An Internet project using Web Classes, ActiveX Documents, or DHTML.None of these development technologies is supported in .NET. Projectsbased on Web Classes can be upgraded to ASP .NET Internet projects, butare likely to provide many additional headaches.
- A database project based on the Data Environment, which is also no longersupported.
- A database project that uses significant data binding to other controls.These features can be upgraded under some circumstances, but willdefinitely fail with the DAO and RDO data access technologies.
- An ActiveX control or ActiveX DLL project. While you can create the .NETequivalents of these COM-based types of programs, you will lose all of theirexisting COM features. If you have controls or components that are stillbeing shared among numerous applications, it will probably be easier to usethem in .NET or to make additional .NET versions, rather than trying tomigrate them and replace the originals.
Part 2 will appear tomorrow.
Matthew MacDonald, an author, educator and MCSD developer, has worked with Visual Basic and ASP since their inceptions. He is the author of The Book of VB .NET (No Starch Press) and ASP.NET: The Complete Reference (Osborne McGraw-Hill) and co-author of Programming .NET Web Services (O’Reilly).