Creating Components in .NET
PC Productivity Solutions
Summary: Discusses the components you build in Microsoft .NET, which are based on object-oriented programming principles and go beyond the simple classes used in other applications. (25 printed pages)
- Understand the Microsoft® .NET component architecture
- Learn how to build .NET components
- Create both an in-process component and an out-of-process component in Microsoft Visual Basic® .NET
- Understand how .NET objects expose an interface
- You have written applications is some version of Visual Basic
- You understand basic object-oriented programming (OOP) concepts
- You know how to build classes in .NET
- You are familiar with the concept of a DLL
- You are familiar with the concept of a Web service
Understanding .NET Components
.NET Classes and Components
Implementing Error Handling
Using Multiple Visual Basic Projects in the Visual Studio .NET Environment
Adding a Constructor to the Component
Updating the Assembly Information
Specifying a Namespace
Using COM Components with Visual Basic .NET
What's Different From Visual Basic 6.0?
Microsoft .NET applications are built from components. All .NET objects expose important attributes, such as properties, methods, and events. These attributes form the foundation of object-oriented programming.
As the architect of Visual Basic .NET objects, you are also responsible for implementing the interface (that is, the properties, methods, and events) necessary for other programmers to use your application's services. Much of your development time will be spent designing objects and writing the code defining the objects and components exposed and used by your applications.
Typically, simple .NET object-oriented programming involves creating a class, adding the properties, methods, and events required by the class, and including the class in various applications. Component-based development takes this basic concept to a higher level. Although the components you build in .NET are based on object-oriented programming principles, they go beyond the simple classes you might use in multiple applications.
So What Is a .NET Component?
A component is a special type of executable built from a .NET project. After compilation the component is typically referenced by applications needing the services provided by the component. In many .NET Web environments, components run on the Web server and provide data and other services (such as security, communications, and graphics) to the Web Services operating on the server. In a Windows Form application a .NET component performs the same role as on a Web server, but on a reduced scale.
.NET components provide a programmable interface that is accessed by consumer applications (often called client applications). The component interface consists of a number of properties, methods, and events that are exposed by the classes contained within the component. In other words, a component is a compiled set of classes that support the services provided by the component. The classes expose their services through the properties, methods, and events that comprise the component's interface.
Simple .NET object-oriented programming involves not much more than creating a class, adding the properties, methods, and events required by the class, and including the class in different applications. A .NET component, however, is a pre-compiled class module with a .DLL (dynamically-linked library) extension. At run time, a .NET component is invoked and loaded into memory to be used by some consumer application. These .NET components are most often built and tested as independent .NET projects and are not necessarily part of another project. After compilation into a .NET DLL, these components can be added to many .NET applications as plug-in service providers.
Each .NET DLL component may contain multiple classes. This means that each DLL may expose a single class or a variety of classes. For instance, if you build a .NET DLL that supports remote data access, the DLL might need to expose separate classes for the database and for the variety of DataSets also produced by the DLL. Or, you may have a single DLL that supports error handling in your applications. In this case, the DLL exposes a single ErrorHandler class that performs all of the error handling required by consumer applications.
Note For more information about defining what a component is, because the term is sometimes used loosely to mean different things, refer to the other articles in the Upgrading to .NET series on MSDN.
What Does It Mean for a Component to Run in a Process?
All Microsoft Windows® applications run in the computer's memory, and use the computer's resources such as disk space, networking services, and graphics capabilities. Windows is a multi-tasking operating system, which simply means that multiple applications simultaneously share the computer's memory and other resources.
Windows manages each application, and the memory required to run that application, as a process. The memory occupied by a Windows application and its data is referred to as the application's process space. Among the important jobs performed by Windows are making sure that the process spaces occupied by applications do no overlap, and providing memory to each process as needed.
Under certain circumstances, Windows may allow process space to be shared among a number of applications. This is particularly true in the case of data or services (such as networking services) that must be shared between applications. Other times the memory occupied by an application is owned solely by the EXE running in that process space.
Many applications such as Microsoft Word and Microsoft Excel support dozens or hundreds of different features. Rather than distribute these application as very large single files, Microsoft has broken the basic functions supported by Word and Excel into multiple executables. The main program ends with an .EXE extension, while most of the other executable portions are files ending in .DLL.
When Word or Excel needs the functionality (perhaps a spell checker, or recalculation engine) contained within a DLL, Windows loads the DLL into the process space occupied by the main application. Windows then manages the main program and the executable code contained within the DLL as a single process. A DLL is called an in-process resource because it executes within the process space occupied by another application.
There are other situations where resources are not loaded in-process. For instance, when Word needs to print a document, Word issues a request to Windows to load a particular printer driver. That same printer driver may be needed by other applications, so Windows loads the driver as an out-of-process service so that more than one application can access the driver. This also means that Windows does not have to load multiple copies of the driver into memory.
In .NET, components built as DLLs run within the process space of the host application and share memory and processor time with their host applications. At run time, the component (which is part of the host application's assembly and is referenced by its manifest) is loaded from disk and added to the host application's process space. Because no remote procedure calls are generated to mediate communication between the component and its host, setting and reading property values, invoking methods, and responding to events raised by the component occurs very quickly.
An alternate architecture involves server applications that run as independent processes outside of the client application process space. These server applications usually (but not always) have an EXE file name extension. When Windows loads an out-of-process component, a separate process space is created for the component, and Windows manages the out-of-process component's resource requirements independently of the client application. Windows mediates the dialog between the server application (that is, the component) and the client (the consumer) by passing messages between them.
Differences Between Running Components In-Process vs. Out-of-Process in Visual Basic 6.0 Compared to Visual Basic .NET
There are several significant differences between in-process and out-of-process application architecture:
- In-process applications tend to run faster because remote procedure calls are not necessary. The client application has direct access to the component without Windows mediating the dialog.
- Out-of-process applications may, in some cases, prove to be more stable than applications built from in-process components. If an out-of-process component crashes, it does not take the consumer application with it. More often than not, if an in-process component stops running, its consumer also crashes.
- Out-of-process components are more easily shared among a number of consumer applications. When compiled as sharable resources, Windows loads a single copy of an out-of-process component and makes it available to as many consumers as request it. Serious performance degradation can occur, however, if the component is simultaneously requested by a large number of consumers.
Although the example you're about to build is not particularly sophisticated, it effectively demonstrates most of the principles of class and component development in .NET. First, you will build a simple .NET class. Once you have built the class, you will use the same code to build a component and include the component in a small .NET application.
A typical requirement of distributed database systems is to synchronize and reconcile updates performed at remote locations. In a .NET-hosted database application, the same record may be updated at multiple sites and submitted to the server for storage. Or, similar records may be created at multiple sites and sent to the server for processing. In such cases, it may be important to know exactly when each record was updated or created so that newer records may be treated differently than older records.
In most such cases, the application assigns a timestamp to database records as the data is modified or added. The server can compare the timestamp on one record with the timestamp on another record and process the records accordingly. However, when the data has been updated in different time zones, it is important to use an appropriate base time for the timestamps. This is often done by using the server computer's time as the timestamp rather than the local time for the data entry.
This is an ideal use of a .NET component. The .NET server can expose its time as a Web Service component that can be read and used by any consumer application. Providing the server time component as a Web Service helps reconcile data changes occurring at geographically distant locations.
One major difference between Visual Basic 6.0 classes and .NET classes is that each Visual Basic 6.0 class module (file) contains one and only one class. This means that a project exposing a lot of different classes contains a lot of different class modules (files).
In .NET, a class can be created just about anywhere in your code. This means that a file in a .NET application can contain one or many classes. Each class in a .NET code module is defined by a Public Class..End Class code block within the module.
This architecture makes it easy to segment the classes developed within a .NET project into logical groups. As an example, in the data access component described earlier, you might choose to put all of the basic data access classes (such as identifying and logging into the database) into one module and all of the DataSet creation and manipulation into a second module. In this scenario, because each module supports multiple classes, project management is easier than having a large number of modules, each containing a single class.
The timestamp example, however, is a bit different. All that's needed in this case is for the .NET server to expose its internal date and time as properties of a component. It's unlikely you'd split the code for such a simple component across multiple class modules.
Creating a Class
Follow these steps to build a simple class module that returns the host computer's date and time:
- Start Visual Studio .NET and open a new Windows Application project.
- Ignoring the default Windows Form in the project, on the Project menu, click Add Class to add a new file to the project.
- Change the name of the generated Public Class from Class1 to ServerTime.
- Enter the following code to define the ServerTime class.
The private variable named mdtTime is not entirely necessary in this class; you could simply return the current date and time without first assigning it to the mdtTime variable. But, because programming projects so often grow beyond their initial specification, there may be a need in the future to have a module-level variable shared by multiple properties and/or methods within the ServerTime class.
Next, use the Windows Form that was automatically created in the new project to test the functioning of the ServerTime class. Follow these steps to create an object from the ServerTime class and use it in the Windows Form:
- Rename the form from Form1 to frmConsumer to emphasize its role as the consumer of the class module.
- Add a TextBox to the Windows Form and name it txtServerTime.
- Delete the string in the Text property so that it is blank.
- Add a button to the Windows Form and name it btnGetServerTime.
- Set the Text property of this button to Get Server Time.
- Double-click the button to add the following code behind the Windows Form (continuation characters have been added to the event procedure's declaration for clarity):
Private Sub btnGetServerTime_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnGetServerTime.Click Dim st As ServerTime st = New ServerTime() txtServerTime.Text = st.TimeStamp End Sub
Because the ServerTime class is included in this project's assembly, there is no need to reference it from the consumer form's code module. .NET is able to locate the ServerTime class and instantiate the st object variable from the information in the class.
When this code has been added to the button's Click event procedure, the current date and time are written into txtServerTime each time the button is clicked.
Try It Out
Test out this application to see if you typed everything in correctly.
- Press F5 to run this application.
- Click Get Server Time and, in the text box, you should see a Date and Time.
Creating the Component
In the previous section, you created a simple class module that returns the current date and time, and tested the class module in a .NET Windows Form. Follow these steps to build a .NET DLL component from the code you used in the previous class module example:
- Start Visual Studio .NET and open a new Class Library project. In the New Project dialog box, name the project ServerTime.
- Change the name of the class from Class1 to ServerTime.
- Either copy the code out of the ServerTime class module you created in the previous example into the new ServerTime class module, or enter the following code into the new ServerTime class module:
You will now compile this ServerTimer class as a DLL by clicking Build on the Debug menu or by using the Ctrl+Shift+B keystroke combination.
The DLL that results from the build command is placed into the \bin directory immediately below your .NET project directory. By default, the DLL has the same name as your component project. For instance, if you named the project TimeStamp in the New Project dialog, the DLL produced by your project will be named TimeStamp.DLL. This name is also the default Namespace name for the classes contained within the project.
If you followed the steps in this document, your project is named ServerTime. You also named the only class within the project ServerTime. Therefore, your DLL name will be ServerTime.DLL and consumer applications of this DLL will reference ServerTime.ServerTime to create an object of this class.
Create a DLL Consumer Application
Once the DLL project has been compiled, its services are available to any Windows Form or WebForm .NET application. In this section, you will build a simple Windows Form consumer application that uses the ServerTime class to retrieve the computer's date and time.
Follow these steps to create the consumer application:
- Start Visual Studio .NET, select Windows Application as the new project type, and name the project DLLConsumer1.
- Set the Name property of the default Windows Form to frmConsumer.
- Add a button control to the default Windows Form and name it btnGetServerTime.
- Add a TextBox to the form and name it txtServerTime.
You need to set a reference to the ServerTime DLL so that this form will be able to consume the components services. Do this by following the steps below.
- To open the Add Reference dialog box as shown in Figure 1, on the Project menu, click Add Reference.
Figure 1. The .NET tab in the Add Reference dialog box
- Click the Projects tab and then click Browse to locate the component DLL built in the preceding section (see Figure 2).
Figure 2. Selecting a DLL reference
- Select the ServerTime.DLL file, click Open, and then click OK.
The Solution Explorer, as shown in Figure 3, now shows the ServerTime component added as a reference in your application. What this means is that all of the classes, along with their properties, methods, and events, are now available to your consumer application.
Figure 3. Solution Explorer showing all current references
Add the following code to btnGetServerTime's Click event:
Private Sub btnGetServerTime_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnGetServerTime.Click Dim st As ServerTime st = New ServerTime() txtServerTime.Text = st.TimeStamp End Sub
Notice that this code is identical to the code used earlier to test the operation of the ServerTime class module. In actual practice, a WebForm or Windows Form might use the ServerTime.TimeStamp property to update a timestamp field in a new or updated record in a DataSet object. Assuming that the ServerTime DLL was actually running on a server computer (whether LAN- or Web-based) the time returned by the TimeStamp property reflects the actual time on the server. This means that the local computer could be in a different time zone or have its time set incorrectly, yet use the same time base as other computers using the server.
An alternate way to write the previous code example is:
Private Sub btnGetServerTime_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnGetServerTime.Click Dim st As New ServerTime txtServerTime.Text = st.TimeStamp End Sub
In this case, the st object variable is declared and instantiated as a single step. Some developers routinely declare and instantiate at the same time to reduce the amount of typing they must do; others prefer to declare an object and instantiate it only when the object is actually needed by the application.
In most cases, it makes no difference whether you instantiate earlier or later. In some situations, it makes more sense to instantiate the object only when it is actually required by the application. For instance, consider the ServerTime component described in this paper. As the developer of an application, you may choose to instantiate only after the user adds or updates information in the database, when you'll need the correct server time. One of the objectives of distributed applications is to minimize the number of trips to the server. There's no need to bother the server by instantiating the ServerTime object until it is actually needed by the consumer application.
Distributed applications must be very reliable. You cannot allow errors and problems to bring down server-based applications such as a .NET Web Service. Because .NET DLLs run within the same process space as their host applications, an unhandled exception in a .NET DLL can crash the entire Web Service, disrupting all users working with that Web Service. To make matters worse, server-side crashes can disrupt the database engine's activities, leaving records locked or in indeterminate states.
For all these reasons, you must add adequate error handling to all of your .NET components.
The following code shows effective error handling added to the TimeStamp property in the ServerTime class. Notice the use of the Try, Catch, and End Try statements:
ReadOnly Property TimeStamp() As String Get Try mdtTime = Now() Return CStr(mdtTime) Catch e As Exception 'Return the exception to the 'consumer for processing: Throw e End Try End Get End Property
The Try..End Try statements wrap all of the property's logic and provide the error trap required to catch any errors raised by the property's code. Each property within the class must have its own Try..End Try block, and should throw an exception back to the consumer application if something goes wrong. It is then up to the consumer application to handle the error.
A more sophisticated approach to error handling within the SystemTime component is shown in the following code:
ReadOnly Property TimeStamp() As String Get Try mTime = Now() Return CStr(mTime) Catch e As Exception 'Return the exception to the 'consumer for processing: Throw e End Try End Get End Property
In this case, the Catch block returns the Exception object (e) to the consumer application. The Exception object contains information about the exception that has occurred and may be useful to the consumer application.
Visual Basic .NET error handling is much different than the error handling in Visual Basic 6.0. The Visual Basic .NET Try..End Try block is described as "structured error handling" and is a vast improvement over the Visual Basic 6.0 On Error statement.
In Visual Basic 6.0, when an error occurred, processing was unconditionally redirected to another part of a procedure. This sometimes made it difficult to exactly determine the path taken by an application when an exception occurred. The general approach to Visual Basic 6.0 exception handling was not rigorously enforced by the VBA compiler, so each programmer was pretty much free to adopt their own approach to handling errors. This resulted in a bewildering variety of error handling styles in Visual Basic 6.0 projects.
In Visual Basic .NET, the exception thrown by the component is captured by the Try..End Try block in the consumer application, as shown in the following code:
Private Sub btnGetServerTime_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnGetServerTime.Click Dim st As New ServerTime Try txtServerTime.Text = st.TimeStamp Catch e As Exception MessageBox.Show(e.Message) End Try End Sub
This is another way that .NET is different from Visual Basic 6.0. The components in a .NET application are tightly bound into the logic of the consumer application, and often you do not have to wait until run time before seeing problems that arise from the integration of consumer applications with their components.
Although Visual Basic.NET supports the obsolete On Error statement for backwards compatibility, you should adopt the Try..End Try structured error handling in your Visual Basic .NET projects. You'll soon find that adding Try..End Try blocks to your Visual Basic .NET code results in a much smoother error handling process than the old On Error GoTo statement.
Earlier, you built a consumer application to test the ServerTime class with both the consumer form and the class located in the same project. Later, you created a separate consumer application to test the compiled version of the class that was located in the ServerTime.DLL component. These various projects can lead to clutter and inefficiency as you develop .NET components based on the Visual Basic .NET language. In Visual Basic 6.0 you had to jump around among the various consumer and component projects to see how the code worked together.
A more efficient approach is to include a consumer project as part of a Visual Studio .NET solution. This consolidated project can invoke and run the component while allowing you to change and recompile the component project as necessary. Visual Studio .NET supports the concept of a solution that simultaneously includes two of more projects within the Visual Studio environment. Each project within the Visual Studio solution can be edited and compiled separately from the other projects in the same solution.
Note Although not covered in this document, a .NET solution may include projects written in different Visual Studio .NET languages such as C# or C++. This makes it easy to incorporate projects written by a developer with language skills other than Visual Basic.
Creating the Consumer Project as Part of Your Solution
Perform these steps to add the consumer project to the component project's solution:
- Close the component consumer application if it is still open and open the DLL component project (which should be named ServerTime).
- To open the Add Existing Project dialog box, on the File menu, click Add Project, and then click Existing Project to open the dialog box.
- Use the Add Existing Project dialog box to locate the consumer project file (which has a .vbproj extension), and then click Open to add it to the component solution.
After the consumer project has been added, the Solution Explorer displays both projects as shown in Figure 4. Notice that the ServerTime project appears in bold and the ClassConsumer project appears as normal text.
Figure 4. Solution Explorer shows all projects contained in the solution and the start-up project name appears in bold
The bold text indicates that the ServerTime project is the solution's start-up project. In fact, if you press F5 at this point, you'll get an error message indicating "A Project with an Output Type of Class Library cannot be started directly." This simply means that compiled class libraries (iDLLs) cannot run as independent applications.
In the Solution Explorer window, right-click the DLLConsumer1 project and select Set as Startup Project from the context menu to set it as the startup project.
Figure 5. DLLConsumer1 is the start-up project
This time when F5 is pressed, Visual Studio .NET starts up the consumer application, which, in turn, references ServerTime.DLL. Keep in mind that the most recently compiled DLL is loaded by .NET. If you make changes to the component's code, you must recompile the component before the consumer application will see the changes.
When multiple projects are loaded into Visual Studio .NET, the Build menu shows separate Build commands for whichever project is currently selected in the Solution Explorer window, as well as a Build Solution command that compiles all projects in a single step (see Figure 6).
Figure 6. The Build menu lists all of the projects to build when multiple projects are present in Solution Explorer
If the DLLConsumer1 project had been selected in the Solution Explorer window, Build DLLConsumer1 would appear instead of the Build ServerTime menu.
An exciting ability in Visual Studio.NET components is that you can pass arguments to an object as it is instantiated. This becomes important when you want to initialize the object to specific values as it is created.
Consider the timestamp component again. One particular consumer of the component may need to retrieve data from server A while another consumer needs data from server B. Using Visual Basic 6.0, the only way to change the server name was to wait until the object had been declared and instantiated, and then set the server name as a property of the object.
It may be more efficient, in some cases, to set the server name at the moment the object is created in memory rather than waiting until instantiation. For instance, the server may not be available. It is more logical for the instantiation step to fail (which means the object's value is Nothing) than to instantiate the object, then set the value of the server name, and have to read a property to see if the server is available.
Visual Studio .NET allows you to pass arguments to the object constructor by adding a New() method to the component's class module. The New() method you create becomes the constructor for objects created from the class.
Here is our class module with a New() constructor method added. Notice the parameter passed in as an argument to New():
Public Class ServerTime Public Enum tsFormat fLong = 0 fShort = 1 fMedium = 2 End Enum Private mdtTime As DateTime Private mstrFormat As String Public Sub New( _ Optional ByVal TimeStampFormat _ As tsFormat = tsFormat.fShort) Select Case TimeStampFormat Case tsFormat.fShort mstrFormat = "Short Date" Case tsFormat.fMedium mstrFormat = "Medium Date" Case tsFormat.fLong mstrFormat = "Long Date" Case Else mstrFormat = "Short Date" End Select End Sub ReadOnly Property TimeStamp() As String Get Try mTime = Now() Return Format(mTime, mstrFormat) Catch e As Exception 'Return the exception to the 'consumer for processing: Throw e End Try End Get End Property End Class
In this case, the code uses an enumerator to specify the acceptable values for the tsFormat argument. Very often, the properties you write will require very specific values. Rather than having another developer guess at these values, it's nice to take advantage of Microsoft IntelliSense® to display the acceptable values in a drop-down list as the developer references the property in code.
Use the Enum structure to specify the values that are acceptable for the property and assign the enumeration a descriptive name, such as tsFormat. These values appear in the .NET code editor (Figure 7) as an IntelliSense drop-down list:
Figure 7. IntelliSense in action
Notice that the enumeration does not use obvious values, such as Short and Long. These are reserved words and cannot be used as members of an enumeration. Instead, use something like fLong (standing for format long) that will still be meaningful to other developers.
The TimeStampFormat parameter passed into the New() constructor is used to set the module-level mstrFormat string variable. The TimeStampFormat argument is optional, and a default value is provided in the event that a consumer application does not specify a format parameter. The code in the TimeStamp property has been modified from the previous example. The revision uses the mstrFormat variable to set the format of the timestamp string returned as the property's value.
The consumer application's code has also been modified. The st object variable's declaration can now include a specification for the format to be used, as the TimeStamp property is returned:
Dim st As New ServerTime.ServerTime( _ ServerTime.ServerTime.tsFormat.fShort)
Although there are many ways to accomplish this simple task, using a New() constructor, with or without parameters, is a convenient and efficient technique. The additional benefit of using an enumeration to provide the developer using the component should not be overlooked.
Certain information is placed into the component by the compiler during the build step that is very useful at run time. For instance, there may be several versions of the ServerTime component installed on a single computer. A consumer application may need to know exactly which version it has loaded before it tries to use the component's properties and methods.
Each .NET project assembly is accompanied by an AssemblyInfo module. This module consists of declarations that can be changed at design time and are bound into the component during the compilation step. Figure 8 shows the AssemblyInfo module for the ServerTime component with certain of the assembly attributes (AssemblyTitle, AssemblyDescription, and AssemblyCompany) filled in.
Figure 8. Specifying assembly attributes
In Solution Explorer, open the assembly code window. Double-click the module named AssemblyInfo.vb and enter the values you want in each of the assembly's attributes.
Notice the AssemblyVersion attribute near the bottom of the AssemblyInfo.vb module. By default this value is set to "1.0.*". The asterisk indicates that you want Visual Basic .NET to automatically increment the version number (e.g., from "1.0.7" to "1.0.8") each time the DLL is compiled. You can elect to hard-code a complete version number by simply entering its value into this attribute.
After the DLL is compiled from the component's source code, this information can be read in Windows Explorer by right-clicking the DLL and clicking the Version tab to view the assembly attributes (see Figure 9).
Figure 9. Viewing assembly attributes in Windows Explorer
Another new and valuable feature in .NET applications is the ability to specify Namespaces within a component project. You'll recall that a single DLL may expose multiple classes. It's good to know that Visual Studio .NET provides a handy way for you to group these classes as Namespaces within the DLL. This can greatly simplify the task of referencing the various classes within the DLL.
Specify a Namespace by marking a region of code with the Namespace and End Namespace statements. For instance, the code for the ServerTime class and its properties might be wrapped within a Namespace called SystemInfo:
Namespace ServerInfo Public Class ServerTime Private mdtTime As DateTime ReadOnly Property TimeStamp() As String Get Try mdtTime = Now() Return Format(mdtTime, "Long Date") Catch e As Exception 'Return the exception to the 'consumer for processing: Throw e End Try End Get End Property End Class End Namespace
A statement referencing the TimeStamp property of the ServerTime class then becomes:
The ServerInfo Namespace may include other classes such as ServerDiskNames, ServerDatabases, and so on.
The same Namespace declaration statement (Namespace ServerInfo) may appear in multiple class modules within a project. Visual Studio .NET collates those class modules and binds them together as a single DLL at compilation time.
Namespaces provide the Visual Studio .NET developer with incredible flexibility when designing applications. As an example, you could have same-named classes inside of different Namespaces. You may have a clean-up class in each one of your Namespaces, where each clean-up class does the clean-up tasks (closing database files, releasing object variables, releasing record locks, etc.) for that Namespace. You could even have the same properties for each clean-up class, even though each class has different clean-up procedures.
Up to this point you've been learning about a particular type of software component. A .NET component always compiles to a dynamically linked library (DLL), which is invoked and loaded into memory at run time.
What has not been explained is that DLLs are always loaded into the same process space occupied by the host application (that is, the consumer). This means that Windows manages the consumer application and component as a single process. The component shares memory and other computer resources with the consumer.
In-Process vs. Out-Of-Process Components
The .NET in-process component architecture results in very fast and reliable communication between the consumer and component. Because they share the same process space, Windows does not have to pass messages between the consumer and component. The consumer is able to read and write the consumer's properties very quickly.
However, not all service providers run as in-process components. Thousands of legacy applications conforming to the Component Object Model (COM) architecture have been built over the years. These service providers use an entirely different mechanism (built on Microsoft's ActiveX specification) to communicate with their client applications. A COM-based ActiveX server publishes its interface as a type library, which is roughly equivalent to a .NET application's manifest. The type library contains the definitions of classes exposed by the COM server, including the properties, methods, and events supported by each exposed class.
Integrating COM Components with .NET
Integrating a Microsoft ActiveX® component with a .NET application is very similar to incorporating a .NET component into the same application. First, the COM component's type library is added to the .NET application's references. Next, objects are created and instantiated from the classes exposed by the COM component. Visual Basic .NET code then manipulates the COM object's properties, methods, and events.
COM components run in their own process thread, outside of the .NET application's process space. This means that Windows independently manages the COM component's resource requirements, and passes messages between the component and its consumer. Although the COM out-of-process integration architecture is bound to be a somewhat slower than .NET's in-process design, most users will never notice the difference.
Follow these steps to create a .NET Windows application that incorporates Microsoft Word as a COM-based ActiveX component (Microsoft Word must be installed on your computer in order for this example to work):
- Open Visual Studio .NET. On the File menu, click New, and then click Project.
- In the New Project dialog box, under Project Types, select Visual Basic Projects. Under Templates, select Windows application. Name the new Windows application project COMComponentIntegration and click OK.
- In Solution Explorer, right-click References, and on the context menu, click Add Reference.
- In the Add Reference dialog box, click the COM tab and locate Microsoft Word in the list of components. (The exact version of Word is relatively unimportant for this demonstration.)
- Highlight Microsoft Word, click Select to add it to the Selected Components list, and click OK.
- .NET responds by asking you whether you would like a wrapper (described below) generated for the Word object library. Click Yes to indicate that you want .NET to build the wrapper for you.
Understanding Interop Assemblies
Because a COM type library and a .NET assembly are very different internally, .NET applications cannot directly use the information provided by a COM component's library. .NET uses either a wrapper around the COM type library or a primary interop assembly built from the information in the type library to provide an interface between a .NET application and a COM component.
A primary interop assembly is a DLL prepared by the COM component's vendor and is intended to be used when the component is integrated with a .NET application. In the absence of an interop assembly, .NET will prepare a DLL that includes references to each of the classes exposed by the component's type library. The DLL is created by importing the class interfaces that .NET finds in the COM component's type library.
The COM type library wrapper is added to the Visual Basic .NET project as a new reference. In Solution Explorer, right-click the wrapper DLL and select Properties from the context menu to see a number of interesting property values. Among these properties is the path to the wrapper DLL. You'll see that .NET has placed the DLL in the same obj directory containing the other component libraries included in the consumer application's assembly.
You may see other wrapper DLLs in the obj folder in addition to the component's wrapper DLL. These other DLLs provide interfaces to secondary type libraries used by the COM component. In the case of Microsoft Word, you'll see both a Microsoft Office and a VBIDE interop DLL in addition to the DLL created for Word.
Programming COM Objects From Visual Basic .NET
The code required to integrate with COM components is very similar to working with .NET-built components. The following procedure (which is the Click event handler behind a button name btnCreateWordDocument) creates an object from the Application class exposed by the Word type library. It then creates a new document from the default Word document template (Normal.dot), adds a line of text to the document, then saves the document.
Private Sub btnCreateWordDocument_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnCreateWordDocument.Click Dim objWord As Word.Application Dim strPath As String objWord = New Word.Application() With objWord .Visible = True .Documents.Add( _ Template:="Normal.dot", _ NewTemplate:=False) .Selection.TypeText( _ Text:="this is a test") .ActiveDocument.SaveAs( _ FileName:="Test.doc") .Documents.Close( _ SaveChanges:=Word.WdSaveOptions.wdDoNotSaveChanges) .Quit() End With objWord = Nothing End Sub
Notice the value supplied as the SaveChanges argument to the Close method near the bottom of this code example. The value refers to an intrinsic constant (wdDoNotSaveChanges) that has been stored in the Word interop DLL. The Word interop DLL contains a large number of properties, methods, events, and constant values that are accessible to your .NET projects.
Managing COM Objects from .NET Applications
One major difference between a .NET in-process component and a COM-based ActiveX component is that the COM object's lifetime must be explicitly controlled by the Visual Basic .NET code. Notice the statement setting objWord to Nothing near the end of the listing above. When working with a .NET-created component, the component is disposed of by the .NET garbage collector. Because the .NET component consists of managed code, .NET takes responsibility to discard the memory occupied by the object when the last reference to the object goes out of scope.
This is not true of COM objects. Once a COM object has been instantiated, Windows normally leaves the object in memory until it is explicitly dismissed by setting its references (objWord, in the case of the listing above) to Nothing. When working with a large COM component such as Word, failure to explicitly dispose of the object at the conclusion of the application can result in enormous memory leaks.
For the most part, the DLL wrapper handles other differences between COM objects and .NET components. For instance, one of these differences occurs when Windows moves a component (.NET or COM) in memory. This often happens as Windows tries to optimize the memory used by applications. A .NET application has no trouble keeping track of the location of its own components because the component and its data are being managed by the common language runtime. However, because a COM object contains unmanaged code, there is no way for the .NET application to keep track of the COM object's reference if the object is moved.
The COM object's runtime wrapper prepared by .NET takes care of keeping memory references updated whenever the COM object is moved. From the consumer application's perspective, the address has not changed, even though the wrapper automatically translates the static memory reference to the updated location.
Distributing .NET Applications Containing COM Objects
For the most part, distributing a .NET application that contains COM objects is the same as distributing a pure .NET application. The only difference is that you must be sure to include the interop DLL created by .NET so that the common language runtime installed on the end-user's computer understands the interface to the COM component.
Also, the COM object must be properly installed and registered on the user's computer. It is difficult to provide generalized rules for COM installation success. Most COM servers (for instance, Microsoft Word) have their own installation programs that create the appropriate folder tree and registry keys. Generally speaking, if the COM server operates properly on the user's computer, it should work within the context of a .NET application.
There are many differences between building components with Visual Basic 6.0 and Visual Basic .NET. In most cases, these changes enhance the development experience by making it easier to build complex applications, test and deploy components, and share components between applications.
- .NET projects provide many features that make organizing the project's files easier. Instead of one class per module (a .CLS file),Visual Basic .NET code modules may contain multiple classes while in Visual Basic 6.0 each class occupied a separate .CLS file. The .NET approach allows you to logically group classes within a single code module to make it easier to recognize when classes are related (such as data access, financial, disk and file management, etc.)
- .NET components are not registered on the user's computer; Visual Basic ActiveX components must be installed and registered on each user's computer. Each .NET component carries all of the interface information necessary for consumer applications to use the component. A component's interface (classes, properties, methods, and events) are documented in the .NET Object Browser, and through IntelliSense.
- .NET error handling is far superior to Visual Basic 6.0's "On Error GoTo" model. It's easy to recognize a .NET error-handling construct, and .NET components can pass back the entire Exception object if necessary.
- Class constructors accept parameters. This allows you to specify start-up information at the moment an object is instantiated, rather than waiting for instantiation to be complete, and then setting initial property values.
- .NET classes can be logically organized within Namespaces. A Namespace can include classes with the same names as classes in another Namespace. A single DLL may include multiple Namespaces. Visual Basic 6.0 does not support Namespaces.
This document describes the process of designing and implementing simple .NET components as DLLs. Each .NET DLL may contain multiple classes, each exposing a number of properties, methods, and events. You learned how to develop a class module that performed a simple yet useful task, how to establish a component project based on that class module, and then how to incorporate that component into a .NET Windows Forms project.
There are some significant differences between the way you create classes and components in Visual Studio.NET versus how the same tasks are performed in Visual Basic 6.0. Exposing properties is a completely different operation than in Visual Basic 6.0. The error handling in Visual Studio .NET is highly structured, yet flexible. Visual Studio components do not need to be registered on the user's computer, yet the consumer assembly must have a reference to the component DLL in order to function.
About the Author
Michael Groh is president of PC Productivity Solutions in Ocala, Florida, and is a consultant, writer, and trainer specializing in Visual Basic and .NET topics. He is also the technical editor of Access/Visual Basic/SQL Advisor and has contributed to twenty different computer books and has written more than 150 magazine articles. He frequently speaks at database conferences around the United States. Previously Mike was the product director for Windows, database, and operating system books for New Riders Publishing.
About Informant Communications Group
Informant Communications Group, Inc. (www.informant.com) is a diversified media company focused on the information technology sector. Specializing in software development publications, conferences, catalog publishing and Web sites, ICG was founded in 1990. With offices in the United States and the United Kingdom, ICG has served as a respected media and marketing content integrator, satisfying the burgeoning appetite of IT professionals for quality technical information.
Copyright© 2002 Informant Communications Group and Microsoft Corporation
Technical editing: PDSA, Inc. or KNG Consulting