Visual Basic 6 Business Objects
Now that we've looked at a fairly traditional 3-tier physical architecture, let's examine a couple of different architectures for Internet/intranet development. These architectures are not the typical Web browser-based designs that most people are familiar with. Instead, we're going to blur the browser approach together with the CSLA to demonstrate a multi-tier architecture with a browser interface.
The first architecture we'll look at is the closest to today's typical Web development. On the left is the physical layout, which is very typical of a Web environment. On the right, though, we're using our now familiar CSLA, with one exception:
One of the primary goals for Internet development is to keep the client as thin as possible to provide compatibility across all the different Web browsers out there. This means that we should avoid putting any processing on the client if at all possible.
Ideally, all we'd ever send to a client would be pure HTML, since that would let any browser act as a client to our program. Of course, HTML provides no ability to do any processing of program logic on the client side, and so this provides the ultimate in thin clients.
Since the Web browser client provides no real processing capabilities, we need a layer of code to run within Microsoft Internet Information Server (IIS) to act as a surrogate or proxy user-interface for the business objects.
Similar capabilities are available for other web servers, but well stick with IIS in this book because it is the easiest to work with from Visual Basic.
We could actually implement this layer using a variety of technologies, but we've shown it here using a new type of project in Visual Basic 6.0: an IIS Application. IIS Applications provide us with very powerful capabilities when it comes to building applications on the web server. They are a successor to Active Server Pages (ASP) based applications, providing similar capabilities, but within the context of a full-blown Visual Basic application.
Our newly added IIS Application interface layer accepts input from the browser and uses it to act like a traditional business object client. What this really means is that the Visual Basic code in this layer takes the place of the Visual Basic forms that we'd normally be using as an interface to the business objects.
Our IIS Application can access COM objects as easily as any other Visual Basic application. However, IIS Applications have special capabilities that make it very easy for us to send HTML out to the user's browser. Thus, IIS Applications make an excellent surrogate for a forms-based user interface since we can tap into the power of our UI-centric business objects and use that information to generate the appropriate interface for the user.
With this technology, and a good set of components containing business objects, we can build an application based on business objects and then use IIS Applications to create the user-interface. We'll cover this in more detail in Chapter 14, where we'll build an IIS Application interface. This interface will use the same underlying business objects as the Visual Basic form-based interface that we'll create in Chapter 7 and the Microsoft Excel interface that we'll create in Chapter 9.
The second design that we'll look at here is very similar to the first, but it's more scalable. In this design, we retain the application server from the 3-tier model that we discussed earlier to offload some of the processing from the web server:
In this diagram, we've moved the data-centric business objects off the Web server and back on to the application server. This can be particularly useful if we have a mixed environment where we're providing a browser interface for some users and a Visual Basic forms interface to others.
The code running in the IIS Application takes the place of the Visual Basic forms in a more traditional user-interface. This means that any code that would have been behind our Visual Basic forms, to format data for display, or to modify any user input, will be coded within the IIS Application generating HTML to be sent to the user's browser. Either way, this code should be pretty minimal, since any actual business logic should always be placed in the business objects or application services.
If we want to get real fancy, we can use the new DataFormat object capability of Visual Basic 6.0 to create an ActiveX DLL that contains objects that know how to format our data for display. These objects could then be used when we're developing our forms based interface as well as our IIS Application interface.
The IIS Application also needs to generate the HTML responses for the user, essentially creating a dynamic display of our data. Since IIS Applications are written in Visual Basic there's a very small learning curve to move from traditional Visual Basic development to developing Web pages using IIS Applications.
Most of the physical architectures that we've been looking at use DCOM (Distributed Component Object Model) for communication between machines on the network. But even with the speed improvements over Remote Automation, DCOM can still be pretty slow. In particular, there is substantial overhead on a per call basis.
Each time our program calls an object's property, or method, there's a speed hit. We get pretty much the same speed hit regardless of whether our call sends a single byte to the object or a thousand bytes. Sure, it takes a little longer to send a thousand bytes than a single byte, but the COM overhead is the same either way - and that overhead is far from trivial.
Calling Single Properties
From a high-level view, each time we access a property or call a method, COM needs to find the object that we want to talk to; and then it needs to find the property or method. Once it's done all that work, it moves any parameter data over to the other process, and calls the property or method. Once the call is done, it has to move the results back over to our process and return the values.
Take the following code, for example:
Set objObject = CreateObject("MyServer.MyClass") With objObject .Name = "Mary" .Hair = "Brown" .Salary = 31000 End With
This code has four cross-process or cross-network calls (depending on whether MyServer is on the same machine or across the network). The CreateObject call is remote and has overhead. Each of the three property calls is also remote, and each has similar overhead. For three properties, this might not be too bad; but suppose our object had 50 properties, or suppose that our program was calling these properties over and over in a loop. We'd soon find this performance totally unacceptable.
Passing Arguments to a Method
Passing multiple arguments to a method, rather than setting individual properties, is significantly faster. For example:
Set objObject = CreateObject("MyServer.MyClass") objObject.SetProps "Mary", "Brown", 31000
But too much overhead still remains, because of the way COM and DCOM process the arguments on this type of call. Furthermore, this technique doesn't allow us to design our business objects in the way we discussed in Chapter 3. With this technique, we'd end up designing our object interfaces around technical limitations.
Serialization of Data
Many programmers have tried the techniques we've just seen, and they've eventually given up, saying that COM is too slow to be useful. This is entirely untrue. Like any other object communication technology, COM provides perfectly acceptable performance, just as long as we design our applications using an architecture designed to work with it.
Due to COM's overhead, when we're designing applications that communicate across processes or across the network we need to make every effort to minimize the number of calls between objects. Preferably, we'll bring the number of calls down to one or two, with very few parameters on each call.
Instead of setting a series of parameters, or making a method call with a list of parameters, we should try to design our communication to call a method with a single parameter that contains all the data we need to send.
There are five main approaches we can take to move large amounts of data in a single method call:
- Directly passing user defined types
- Variant arrays
- User defined types with the LSet command
- ADO(R) Recordset with marshalling properties
- PropertyBag objects
In any case, what we're doing is serializing the data in our objects. This means that we're collecting the data into a single unit that can be efficiently passed to another object and then pulled out for use by that object.
Directly Passing User Defined Types
Visual Basic 6.0 provides us with a new capability, that of passing user defined types (UDTs) as parameters even between different COM servers. This means we can easily pass structured data from one object to another object, even if the objects are in different Visual Basic projects, running in different processes or even running on different computers.
For instance, suppose we create a class named SourceClass in an ActiveX server (DLL or EXE):
Option Explicit Public Type SourceProps Name As String BirthDate As Date End Type Private udtProps As SourceProps Public Property Let Name(ByVal Value As String) udtProps.Name = Value End Property Public Property Get Name() As String Name = udtProps.Name End Property Public Property Let BirthDate(ByVal Value As Date) udtProps.BirthDate = Value End Property Public Property Get BirthDate() As Date BirthDate = udtProps.BirthDate End Property Public Function GetData() As SourceProps GetData = udtProps End Function
This class is fairly straightforward, simply allowing a client to set or retrieve a couple of property values. Note how the UDT, SourceProps, is declared as Public. This is important, as declaring it as Public makes the UDT available for use in declaring variables outside the object. The other interesting bit of code is the GetData function:
Public Function GetData() As SourceProps GetData = udtProps End Function
Since the object's property data is stored in a variable based on a UDT, we can provide the entire group of property values to another object by allowing it to retrieve the UDT variable. The GetData function simply returns the entire UDT variable as a result, providing that functionality.
Now we can create another class named ClientClass:
Option Explicit Private udtProps As SourceProps Public Sub PrintData(ByVal Source As SourceClass) udtProps = Source.GetData Debug.Print udtProps.Name Debug.Print udtProps.BirthDate End Sub
This class simply declares a variable based on the same UDT from our SourceClass. Then we can retrieve the data in the SourceClass object by using its GetData function. Once we've retrieved the data and stored it in a variable within our new class we can use it as we desire. In this case we've simply printed the values to the Immediate window, but we could do whatever is appropriate for our application.
This mechanism allows us to pass an object's data to any other code as a single entity. By serializing our object's data this way, we can efficiently pass the data between processes or even across the network.
The Variant data-type is the ultimate in flexibility. A Variant variable can contain virtually any value of any data-type - including numeric, string, or even an object reference. As you can imagine, an array of Variants extends that flexibility so that a single array can contain a collection of values, each of a different data-type.
For instance, consider this code:
Dim vntArray(3) As Variant vntArray(0) = 22 vntArray(1) = "Fred Jones" vntArray(2) = 563.22 vntArray(3) = "10/5/98"
Inside the single array variable vntArray we've stored four different values, each of a different type. We can then pass this entire array as a parameter to a procedure:
In a single call, we've passed the entire set of disparate values to a procedure. Since methods of objects are simply procedures, we could also pass the array to a method:
The PrintValues procedure or method might look something like this:
Public Sub PrintValues(Values() As Variant) Dim intIndex As Integer For intIndex = LBound(Values) To UBound(Values) Debug.Print Values(intIndex) Next intIndex End Sub
This simple code just prints the values from the array to the immediate window in the Visual Basic development environment. It does, however, illustrate how easy it is to get at the array data within an object's method.
Of course, the Variant data-type is highly inefficient compared to the basic data-types, such as String or Long. Using a Variant variable can be many times slower than a comparable variable with a basic type. For this reason, we need to be careful about how and when we use Variant arrays to pass data.
The Variant data-type is generic, meaning that a Variant variable can hold virtually any piece of data we provide. The downside to this is that, each time we go to use the variable, Visual Basic needs to check and find out what kind of data it contains. If it isn't the right type of data then Visual Basic will try to convert it to a type we can use. All this adds up to a lot of overhead, and thus our performance can suffer.
If our object's data is stored in a Variant array, we'll incur this overhead every time we use any of our object's data from that array. The code in most objects work with data quite a lot, so we really do run the risk of creating objects with poor performance if we use Variant arrays.
Many of our objects will need data from a database. If we're going to use a Variant array to send this data across the network, we'll need some way to get the data from the database into the array. Usually, we'll get the data from the database in the form of an ADO Recordset.
Recordset objects provide us with an easy way to copy the database data into a Variant array. This is done using the GetRows method that's provided by the object. The GetRows method simply copies the data from the object into a two-dimensional Variant array.
The following code, for instance, copies the entire result of a query into a Variant array named vntArray:
Dim vntArray As Variant rsRecordset.MoveFirst vntArray = rsRecordset.GetRows(rsRecordset.RecordCount)
vntArray now contains the contents of the recordset as a two-dimensional array. The first dimension indicates the column or field from the recordset; the second dimension indicates the row or record:
vntMyValue = vntArray(intColumn, intRecord)
Of course, the subscripts for the column are numeric, so they aren't as descriptive as the field name would be if we had access to the actual recordset. Instead of the following:
vntValue = rsRecordset("MyValue")
We're reduced to using something like this:
vntValue = vntArray(2)
The order of columns is entirely dependent upon the field order returned in the recordset. This means that if we add a field to our SQL SELECT statement, in the middle of other fields that we're retrieving, then we'll have to change all of our programs that rely on Variant arrays to pass data.
Page 3 of 13