Building Multitiered Client/Server Apps with Visual Basic 4.0 Enterprise Edition

Jenny Notestein

Jenny Notestein is an enterprise development consultant in the New York financial district. She can be reached at notestein@msn.com.

Click to open or copy the ARCHITEC project files.

Click to open or copy the PROTO project files.

This is the first of a three-part series on enterprise development in the corporate world using Microsoft¨ Visual Basic 4.0 Enterprise Edition. In this series, we will look at what's involved in building a robust, multitiered client/server application. We'll be developing the architecture and various components of a 32-bit end-to-end data-warehouse-type reporting system that demonstrates scalability, maintainability, and team development through a useful example of a reporting facility that's built on the Services model with a thin client, an SQL Serverª datamart, and fully decoupled, reusable ActiveX servers implementing business logic.

If you're a corporate developer who is relatively new to Visual Basic, you'll want to check out Joshua Trupin's article, "Visual Basic 4.0 Provides Easy Entry to the Advanced Features of Windows 95," MSJ October 1995, and "Exploring the Client/Server Capabilities of Visual Basic 4.0 Enterprise Edition," by Guy Eddon and Henry Eddon, MSJ March 1996.

This series will cover the lifecycle of a typical corporate application, without the meetings. This month we'll focus on application architecture, which first develops from end-user requirements, is then tested through a working prototype, and is then further refined through the demo-and-feedback process, which finally leads into building the actual application components. The second part of this series will focus on the development and implementation phase, including issues such as optimization, pool management, security, and internationalization. The third part of this series will wrap up the implementation and will cover rollout topics such as QA, audit, ongoing maintenance, and enhancement requests.

The Application

In many corporate environments today, data is stored in a wide variety of DBMSs across an equally wide variety of platforms: DB2 on MVS, RDB on VMS, Oracle on Unix, Sybase on NLM, Microsoft SQL Server on Windows NT¨, and on it goes. Information is commonly found across several of these systems, due to years of data accumulation and partitioning across domains. There is a real business need for a generic facility to access this data and produce reports for management.

Using the tools that Visual Basic 4.0 Enterprise Edition provides, we will be building such a generic facility: a data-warehouse-type decision support reporting system for line managers. While the component architecture that we'll be developing can be used to gather data and produce formatted reports for just about any department or function in the business world, we'll use the standard human resources name-and-salary report for our prototype. But keep in mind that the goal of this system is flexibility; it can just as easily hit seven different datasources for a full analysis of what salaries the company is paying Visual Basic developers (broken out by department and shoe size), or it can produce the reports necessary to determine the final cost of some primary product for the company, such as a grilled cheese sandwich, given that the fair market value of its subcomponents (bread, cheese, butter, labor costs, fire suppression system, and so on) are stored on various data servers. But you'll have to try this stuff at home.

OK, assume the problem has been defined (managers need information), and the requirements have been collected (where the information is stored, how the users want it to be presented, and what security mechanisms will be implemented); the next thing to consider is the architecture of the application. Like most aspects of the development process, the architecture will be iteratively determined, meaning it will be refined as we go through several passes of breaking the requirements down into component blocks that are then mapped to software objects. A first pass of the architecture might produce something like Figure 1.

Figure 1 Preliminary Architecture

User Services

The client application will be fairly lightweight, with a 32-bit Visual Basic 4.0 front end through which the user is presented with a listbox of options. The user can choose the desired report, enter any necessary parameters for producing the report in the fields presented, and select the desired presentation format. The entire selection process takes advantage of the popular wizard interface. For our prototype, the presentation format will be either a Microsoft Excel worksheet, a Word document, or a raw SQL statement that will be displayed in a human-readable format by a viewer. The client side is scalable to Internet access using, for example, a Microsoft Internet Explorer 3.0 (IE 3.0) Excel Viewer module. Client workstations will be running Windows NT to take advantage of integrated security.

Business Services

Business services will consist of a middle tier of remote automation servers known collectively as the Report Server. The components of the Report Server consist of one or more virtual data objects, one Request Broker, and one or more Report Builders. They will be written in 32-bit Visual Basic 4.0 and run on Windows NT Server, again using integrated security. They are out-of-process ActiveX servers (EXEs), which allows them to run in separate processes/machines. The business services will also be running Microsoft SQL Server 6.5 as a local datamart staging area for the data pulled from the various remote data repositories and for use by the Report Server components for storing programmatic and configuration information. Scalability can be handled in three ways: first, you could separate the Report Server components and the SQL Server onto separate machines, and, if necessary, separate the ActiveX servers across several machines; second, you could implement multiple instances of the Report Server components; and third, you could implement a second SQL Server for two-tier data cache, also if need dictates. For this prototype, all the servers are running on the same machine.

The Request Broker will monitor the relationship between the client and all of the components of the Report Server. When the client sends a request (Step 1 from Figure 1), this component will interpret the information and send it to the appropriate virtual data object (Step 2), depending on the report selected.

Each virtual data object corresponds to a particular way of looking at a specific set of data that will support a suite of reports. The appropriate virtual data object will receive the information about the report from the Request Broker, such as the parameters entered by the user. Using RDO, the virtual data object will retrieve the appropriate data from the remote data servers and will store it in the local SQL Server data cache (Steps 3–5). The virtual data object will then return control to the Request Broker (Step 6), along with information about where the data is stored in the local data cache.

The Request Broker will send the location of the data to the appropriate report builder (Step 7) depending on the formatting selected by the user. The Report Builder generates the report and places it in the datamart (Step 8). Next, the report builder will send the location of the formatted data to the client through the Request Broker (Steps 9 and 10). The client now communicates with the datamart to display the formatted data (Steps 11 and 12). When the user is done and closes the app, the client disconnects from the Request Broker and sends a message to the SQL Server to drop all cached data that relates to this client session.

Data Services

Data is retrieved from remote data repository servers, which are the above-described wide variety of DBMSs across the enterprise. As shown above, the client requests data retrieval through the virtual data object components of the Report Server, and it stays in the local cache only while the client remains connected to the application.

So here we have a basic system architecture that's maintainable in that it's fully modular; it's designed to be built with ActiveX servers that can be reworked or replaced as reporting requirements change within a department of users. It's also extensible and scalable in that new virtual data objects can be added as the application is rolled out to different departments with different reporting needs, for which remote data servers can be hooked-in or unhooked as necessary. The modular design permits additional hardware to maintain or increase performance as the system grows.

Finally, the architecture is designed for large-scale team development; different teams can write each collection of objects, such as the client, the Request Broker, and the virtual data objects. Other groups would include the report design and layout team.

Building the Prototype

The next step is to build a quick prototype. The prototype phase is an integral part of the development of a solid architecture. A working demo helps the users figure out what they need the application to do now and in the future. For the developers, the prototype lets us test our initial plan and get a more precise definition of how the application's components work together. The sooner these considerations are built into the plans, the more time, money, and nerves will be saved.

When building the prototype, as with any project in Visual Basic 4.0, one of the first things that we'll do is use Visual SourceSafeª. This can saw large blocks off the development time that would otherwise be spent coordinating the ongoing changes to shared code modules. It also saves time by providing the ability to roll back code changes and to track versions, which is important for debugging as well as handling upgrade and installation issues.

When setting up the Visual SourceSafe database for the prototype application files, selecting the "Use network name for automatic user login" option in the 32-bit Administrator will enable Windows NT integrated security (see Figure 2). The project and each module should be checked in as they are created.

Figure 2 Visual SourceSafe Security

The code for the major sections of the prototype is next. For each of the class modules that we'll be creating in the following section, set the instancing property to Creatable, and set MultiUse and the public property to True; this will allow one copy of the ActiveX server to service multiple clients. For the ActiveX server modules, set the startup form to Sub Main and the start mode to ActiveX Server in the Project Options dialog box.

When building the ActiveX server executables, the EXE Options dialog box (see Figure 3) provides three levels of significance for entering version information—major, minor, and revision. This is the data that appears when File Properties is selected in the Windows¨ Explorer. Selecting the Auto Increment checkbox will automatically roll future revisions of the EXE forward.

Figure 3 Version Information

Prototype Code

In the prototype, a simple client interface will give the user a list of reports to choose from (see Figure 4). After selecting a report, the user will click a command button to continue. The code shown in Figure 5 will then be executed, passing the name of the chosen report and an empty requirements object, which has form-level scope, to the Request Broker.

Figure 4 Prototype Client Interface

Figure 5 Prototype Request Requirements


 Private Sub btnRequestRequirements_Click()

'Get the request requirements object by telling the Broker  
'what report we want
oRequestBroker.RequestRequirements cbolReports.Text,_oRequirements
    
'The gathering of the requirements is way simple here.  A 
'future version will have a structured meta information 
'methodology that will allow a local engine to generate a 
'wizard-like interface for requirements input.
lblDataRequired.Caption = oRequirements.Caption

End Sub

The request requirements method of the Request Broker, as detailed in Figure 6, receives the report name and the requirements object. It completes the requirements object based on the report name selected. The completed object contains the parameter options from which the user will choose the specific data to build the report on. The requirements class itself is shown in Figure 7.

Figure 6 Request Requirements Module


 Public Sub RequestRequirements(sReportName As String, 
                               oRequirementsOUT As Object)
'Build the requirements structure for the report requested by the 
'client. This will become a much more comprehensive structure 
'for use by a client-side wizard.

    'Set the Requirements write-once properties
    oRequirements.ReportName = sReportName
    oRequirements.ReportObjectName = RemoveSpaces(sReportName)
    
    'Simple data structure population. These will be stored in an 
    'odbc database when the wizard is on the scene
    Select Case sReportName
        Case asReports(0)
            oRequirements.Caption = "No Data Required"
        Case asReports(1)
            oRequirements.Caption = "Person's Name"
    End Select
    Set oRequirementsOUT = oRequirements
 End Sub

Figure 7 Stub Requirements Class

clsRequirements.cls


 Option Explicit

'Internal property values
Private sReportName As String
Private sRequirementCaption As String
Private sRequirementValue As String
Private sReportObjectName As String

Public Property Get ReportName()
    ReportName = sReportName
End Property

Public Property Let ReportName(vNewValue As Variant)
    'Only let it be set once
    If sReportName <> "" Then Exit Property
    sReportName = vNewValue
End Property

Public Property Get ReportObjectName()
    ReportObjectName = sReportObjectName
End Property

Public Property Let ReportObjectName(vNewValue As Variant)
    'Only let it be set once
    If sReportObjectName <> "" Then Exit Property
    sReportObjectName = vNewValue
End Property

Public Property Get Caption()
    Caption = sRequirementCaption
End Property

Public Property Let Caption(vNewValue)
    sRequirementCaption = vNewValue
End Property

Public Property Get Value()
    Value = sRequirementValue
End Property

Public Property Let Value(vNewValue)
    sRequirementValue = vNewValue
End Property

We'll also implement a callback class that can be instantiated on the client. This is the notification object that we pass to the Request Broker when we request a report, as shown in Figure 8. The Request Broker then invokes a method in the notification object that tells the client that it's finished, as shown in Figure 9.

Figure 8 Stub Notification Object

clsNotify.cls


 Option Explicit

'Our internal variables are private. They are set by properties 
'and methods
Private bFinished As Boolean
Private sReport As String

Public Function SetFinished(sReportIN As String) As Boolean
    'This is the function that the Broker calls to let us know
    'it is finished, and what the report is
    sReport = sReportIN
    bFinished = True
End Function

Private Sub Class_Initialize()
    'When the class is instantiated, we set default values
    bFinished = False
End Sub

Public Property Get Finished()
    'This is the property that we keep checking to see if
    'the Broker is done.
    Finished = bFinished
End Property

Public Property Get Report()
    'This is the property that the Request generation form
    'will call after finish is set to get the report name
    Report = sReport
End Property

Figure 9 Stub Report Request Method


 Public Sub ReportRequest(oRequirementsIN As Object, 
                         oNotify As Object)
'Process the request for a report

    'Create a report object and class (lest we think there is too 
    'much stuff going on for what we are doing, let us not forget 
    'that this is the beginning structure of the app seeing its 
    'way through the prototype stage)
    Dim oReportObject As Object
    Dim oReport As New clsReport
    
    'Create the Report requested
    Set oReportObject = CreateObject(
                     oRequirementsIN.ReportObjectName & ".Report")
    
    'Request that the report be generated
    oReportObject.Request oRequirementsIN, oReport
    
    'Notify the client we are done, and pass the results
    oNotify.SetFinished oReport.FileName

End Sub

The prototype should appear to the users to have full functionality, even though some of it may be impossible to create at this point. The solution is to stub out these functions. Figure 10 shows the code for the report object to which we pass in the report requirements and the report name chosen by the user, and which, at this point, passes back a filename for displaying to the user without retrieving actual data.

Figure 10 Stub Report Object

Report.Cls


 Option Explicit

Public Sub Request(oRequirementsIN As Object, oReportIN As Object)

    'Processing will take place here (I've the strong urge to say
    '"This part is obvious and is left for the student to explore 
    'at home." hee hee). This will be the heart of the DataMart  
    'services in the coming articles.

    'Result File name ready for viewing
    oReportIN.SetFileName "REPORT1.XLS"

End Sub

The Architecture Develops

Now that the prototype is finished, the architecture can be refined (see Figure 11). This view gives us the breakdown of the Visual Basic 4.0 components involved. At runtime, it's designed to work as follows.

Figure 11 Application Architecture, Take Two

The client application, called the Reporter, will send out a request for a list of reports, which is then provided by the Request Broker/Dispatcher ActiveX server and displayed in the client application. The user selects a report and the request goes out to the construction and information requirements function of the Request Broker, which sends back a report build message object. The report build message object has two components, a header and a fields collection. The fields collection is where the action is; it contains the information required to build the wizard pages that the user sees, as shown in Figure 12.

Figure 12 Request Wizard

Operators (=, <, >, and so on) will be displayed for each field, depending on its data type. Each field object contains the information used to construct one page of the wizard, which the user will step through to enter the parameters for the report. The field objects are two-way messages used to dispatch and collect the data from the client. The messaging code is shown in Figure 13. The procedure shown in Figure 14, Display Field Data, walks the fields collection and builds the wizard pages.

Figure 13 Messaging Classes

Field.cls


 Option Explicit

'External handles
Public Filters As Filters

'Internal variables
Private sName As String
Private bAString As Boolean
Private sDescription As String
Private bRequired As Boolean
Private iSize As Integer
Private vValue As Variant
Private sSelectedFilter As String

Private Sub Class_Initialize()
    Set Filters = New Filters
End Sub

Public Property Get AString() As Boolean
'AString because String is a reserved word.
    AString = bAString
End Property

Public Property Let AString(bNewValue As Boolean)
    bAString = bNewValue
End Property

Public Property Get Description() As String
    Description = sDescription
End Property

Public Property Let Description(sNewValue As String)
    sDescription = sNewValue
End Property

Public Property Get Required() As Boolean
    Required = bRequired
End Property

Public Property Let Required(bNewValue As Boolean)
    bRequired = bNewValue
End Property

Public Property Get Size() As Integer
    Size = iSize
End Property

Public Property Let Size(iNewValue As Integer)
    iSize = iNewValue
End Property

Public Property Get Value() As Variant
    Value = vValue
End Property

Public Property Let Value(vNewValue As Variant)
    vValue = vNewValue
End Property

Public Property Get SelectedFilter() As String
    SelectedFilter = sSelectedFilter
End Property

Public Property Let SelectedFilter(sNewValue As String)
    sSelectedFilter = sNewValue
End Property

Public Property Get Name() As String
    Name = sName
End Property

Public Property Let Name(sNewValue As String)
    sName = sNewValue
End Property

Fields.cls


 Option Explicit

'Internal variables
Private coFields As Collection

Public Sub Create(sKey As String)
    'Make the name the same as the key
    Dim cField As New Field
    cField.Name = sKey
    coFields.Add Item:=cField, Key:=sKey
End Sub

Public Function Item(vKey As Variant)
    Set Item = coFields.Item(vKey)
End Function

Public Function Count() As Integer
    Count = coFields.Count
End Function

Private Sub Class_Initialize()
    Set coFields = New Collection
End Sub

Filter.cls


 'This class contains information on the filter type in question
'Later a rule property can be added that will be used to more 
'precisely validate data entry
Option Explicit

'Internal variables
Private sName As String
Private sDescription As String

Public Property Get Name() As String
    Name = sName
End Property

Public Property Let Name(sNewValue As String)
    sName = sNewValue
End Property

Public Property Get Description() As String
    Description = sDescription
End Property

Public Property Let Description(sNewValue As String)
    sDescription = sNewValue
End Property

Filters.cls


 Option Explicit

'Internal variables
Private coFilters As Collection

Public Sub Create(sKey As String)
    'Make the Name the same as the key
    Dim cFilter As New Filter
    cFilter.Name = sKey
    coFilters.Add Item:=cFilter, Key:=sKey
End Sub

Public Function Item(vKey As Variant)
    Set Item = coFilters.Item(vKey)
End Function

Public Function Count() As Integer
    Count = coFilters.Count
End Function

Private Sub Class_Initialize()
    Set coFilters = New Collection
End Sub

Header.cls


 Option Explicit

'External Handles
Private sReportID As String
Private sReportName As String

Public Property Get ReportID() As String
    ReportID = sReportID
End Property

Public Property Let ReportID(sNewValue As String)
    sReportID = sNewValue
End Property

Public Property Get ReportName() As String
    ReportName = sReportName
End Property

Public Property Let ReportName(sNewValue As String)
    sReportName = sNewValue
End Property

ReportBuildMessage.cls


 Option Explicit

'External handles
Public Header As Object
Public Fields As Object

Private Sub Class_Initialize()
    Set Header = New Header
    Set Fields = New Fields
End Sub

modMain.bas


 Option Explicit
Sub Main()
    'These classes are in their own project so all programs can use 
    'them. They are compiled into an OLE.DLL and can be referenced 
    'in the Tools.References to facilitate early binding.
End Sub

Figure 14 Display Field Data


 Private Sub DisplayFieldData(iFieldNumber As Integer)
'Move message classes information into the visual controls

    Dim oField As Object
    Dim iFilters As Integer
    Dim iFoundFilter As Integer
    
    Set oField = oMessage.Fields.Item(iFieldNumber)
    iFoundFilter = 0

    lblFieldName.Caption = oField.Name
    lblFieldDescription.Caption = oField.Description
    
    iFilters = oField.Filters.Count
    
    Dim iLoop As Integer
    For iLoop = 1 To iFilters
        If iLoop > 1 Then
            Load optFilterType(iLoop - 1)
            optFilterType(iLoop - 1).Top = _
                   optFilterType(iLoop - 2).Top + _
                   optFilterType(iLoop - 2).Height + 100
            optFilterType(iLoop - 1).Visible = True
        End If
        optFilterType(iLoop - 1).Caption = _ oFi
              eld.Filters.Item(iLoop).Name
        If oField.Filters.Item(iLoop).Name = oField.SelectedFilter 
            Then iFoundFilter = iLoop - 1
        End If
    Next iLoop
    
    optFilterType(iFoundFilter).Value = True
    
    txtValue.Top = optFilterType(iLoop - 2).Top + _  _
         FilterType(iLoop - 2).Height + 300
    txtValue.Visible = True
    txtValue.Left = optFilterType(iLoop - 2).Left
    txtValue.Text = oField.Value
End Sub

While working through the wizard, the user will click on panels of a 32-bit status-bar control that are painted to look like the "Back" and "Next" buttons. Unlike command buttons that provide visual cues to indicate when they have been pushed, the panels in the status bar do not automatically show that they have been selected. I wrote some code (Figure 15) to make the panels look and feel like buttons. The 250-millisecond pause after the DoEvents redraw of the panel allows the pushed effect to be perceived. Figure 16 shows the code for the Panel Click event, which is where most of the work done by the wizard is found.

Figure 15 Beveling the Panels


 Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

            Panel.Bevel = 1
            DoEvents
            Sleep 250
            Panel.Bevel = 2

Figure 16 Panel Click Event


 Private Sub sbMain_PanelClick(ByVal Panel As Panel)
'Handle interaction with the status bar
    Select Case Panel.Index
        'Back button
        Case 2
            'Display button action
            Panel.Bevel = 1
            DoEvents
            Sleep 250
            Panel.Bevel = 2
            
            'Collect Field Data
            CollectFieldData pbMain.Value - 1
            
            'Move, use pb as locator
            pbMain.Value = pbMain.Value - 1
            
            'Turn Back off if we just entered the first screen
            If pbMain.Value = 1 Then
                Panel.Visible = False
                pnlFieldData.Visible = False
                pnlPickReport.Visible = True
            End If
            
            'If pbMain.Value > 1 then
            If pbMain.Value > 1 Then
                DisplayFieldData pbMain.Value - 1
            End If

            'Change Finish to Next on backing off last screen
            If pbMain.Value = pbMain.Max - 1 Then
                sbMain.Panels.Item(3).Text = "Next >>"
            End If
            
            'We are in the fields
            If pbMain.Value > 1 Then
            End If

        'Forward button
        Case 3
            'Display button action
            Panel.Bevel = 1
            DoEvents
            Sleep 250
            Panel.Bevel = 2
    
            'Finished
            If pbMain.Value = pbMain.Max Then
                Exit Sub
            End If
                    
            'Collect Field Data
            If pbMain.Value > 1 Then
                CollectFieldData pbMain.Value - 1
            End If
            
            'Turn Back on if we just left the first screen
            If pbMain.Value = 1 Then
                If cbReportList.Text = "" Then
                    Exit Sub
                Else
                    Set oMessage = Nothing
                    oRequestBroker.GetReportDescription _
                         cbReportList.Text, oMessage
                    pbMain.Max = oMessage.Fields.Count + 1
                End If
                sbMain.Panels.Item(2).Visible = True
                pnlPickReport.Visible = False
                pnlFieldData.Visible = True
            End If
             
            'Move, use pb as locator
            pbMain.Value = pbMain.Value + 1
           
            'If pbMain.Value > 1 then
            DisplayFieldData pbMain.Value - 1
            
            'Change Next to Finish on entering last screen
            If pbMain.Value = pbMain.Max Then
                sbMain.Panels.Item(3).Text = "Finished"

When the user enters information in the wizard pages, that data is stored back into the field object. After the user has stepped through the entire wizard, the report build message object is then sent back to the Request Broker.

The Request Broker, the code for which is shown in Figure 17, uses the virtual data object to utilize the Microsoft SQL Server data cache to store the metadata on report construction and availability. In its present version the report list and construction requirements are still stubs, much like the prototype, although the construction requirements now incorporate a more elaborate messaging structure. A later version might include a requirements-management editor that will allow the user to enter not only the requirements options for each report, but also the appropriate filter types for each specific requirement.

Figure 17 Request Broker

Broker.cls


 Option Explicit

Public Sub GetReportsList(asReportList() As String)
'Test Harness/Stub for Reporter to develop against.
    ReDim asReportList(0) As String
    asReportList(0) = "How much are we paying for an Employee..."
End Sub

Public Sub GetReportDescription(sName As String, oMessage As
     ReportBuildMessage)
'Test Harness/Stub for Reporter to develop against.
    PopulateDummyData oMessage
End Sub

Private Sub PopulateDummyData(oMessage As ReportBuildMessage)
'Test Harness/Stub for Reporter to develop against.
    With oMessage
        .Header.ReportName = "The Test Report"
        .Header.ReportID = 819
        .Fields.Create "Employee"
            With .Fields.Item("Employee")
                .Name = "Employee"
                .AString = True
                .Description = "Employee's Name"
                .Required = True
                .Size = 35
                .Value = ""
                .SelectedFilter = "= (Equal)"
                .Filters.Create "= (Equal)"
                    With .Filters.Item("= (Equal)")
                        .Name = "= (Equal)"
                        .Description = "A Value that is Equal " & _
                            "the entered value."
                    End With
                .Filters.Create "Like"
                    With .Filters.Item("Like")
                        .Name = "Like"
                        .Description = "A Value that is similar" & _ 
                             " to the entered value.  Wild " & _
                             "cards can be used."
                    End With
            End With
        
        .Fields.Create "Total Yearly Compensation"
            With .Fields.Item("Total Yearly Compensation")
                .Name = "Total Yearly Compensation"
                .AString = True
                .Description = "The amount that the Employee " & _
                     "receives in a year as total compensation"
                .Required = False
                .Size = 6
                .Value = "25000"
                .SelectedFilter = ""
                .Filters.Create "= (Equal)"
                    With .Filters.Item("= (Equal)")
                        .Name = "= (Equal)"
                        .Description = "A Value that is Equal " & _
                             "to the entered value."
                    End With
                .Filters.Create "< (Less Than)"
                    With .Filters.Item("< (Less Than)")
                        .Name = "< (Less Than)"
                        .Description = "A Value that is Less " & _
                            "Than the entered value."
                    End With
                .Filters.Create "> (Greater Than)"
                    With .Filters.Item("> (Greater Than)")
                        .Name = "> (Greater Than)"
                        .Description = "A Value that is Greater" & _ 
                             " Than the entered value."
                    End With
            End With
            
        .Fields.Create "Job Description"
            With .Fields.Item("Job Description")
                .Name = "Job Description"
                .AString = True
                .Description = "Employee's Name"
                .Required = True
                .Size = 255
                .Value = "Building machines that go PIIING!"
                .SelectedFilter = "Like"
                .Filters.Create "= (Equal)"
                    With .Filters.Item("= (Equal)")
                        .Name = "= (Equal)"
                        .Description = "A Value that is Equal " & _
                             "to the entered value."
                    End With
                .Filters.Create "Like"
                    With .Filters.Item("Like")
                        .Name = "Like"
                        .Description = "A Value that is similar" & _
                             " to the entered value. Wild cards" & _
                             " can be used."
                    End With
            End With
    End With
End Sub

modMain.bas


 Option Explicit

Sub Main()
End Sub

When the report build message object is returned to the Request Broker after the wizard is completed, the deconstructor dispatcher function of the Request Broker takes the requirements data and determines which virtual data object ActiveX server will need to get the information next to fulfill the request. The deconstructor also generates a simple SQL statement of ANDs and sends it to the virtual data object that corresponds to the report selected. Only ANDs are used; ORs would require allowing the user to force operator precedence (such as parentheses), which would both complicate the UI and demand a higher level of expertise from the user. The AND-only SQL statement is more of a filter at this level than a logical interrelating of the fields.

Each virtual data object needs to have very specific information on how the resultset is to be built for the report for which it is responsible. The appropriate virtual data object will do the hard work of building the complete SQL statement or statements, using whatever unions, joins, or decoding are required. The virtual data object will send the SQL to the appropriate remote data repository server or servers using RDO. This means that the box on which the virtual data objects reside—and not the end users' machines—will need to be configured with ODBC DSNs (datasource names) and the various components required for ODBC connectivity, including drivers, client resource libraries, and net libraries, for all of the diverse DBMSs and platforms on which they reside.

When the data comes back from the remote data repositories, the virtual data object uses part of the Microsoft SQL Server 6.5 data cache as a staging area to construct the resultset. The virtual data object will then send notification to the Request Broker. The Request Broker will receive this notification, which will include the name of the temporary object in the data cache. Using data from the construction and information requirements messaging system classes, the Request Broker will then determine to which Report Builder server the name of the resultset should be passed.

The Report Builders will be a series of ActiveX servers, with each one corresponding to a particular report format, such as Microsoft Word, Microsoft Excel, and so on. The appropriate Report Builder will build the resultset into a formatted report and insert it into the Microsoft SQL Server data cache. It will then notify the Request Broker that it's finished, passing the location of the finished report.

The Request Broker will send notification to the client Reporter with the location of the finished report. The Reporter will retrieve the report from the SQL Server using RDO and will display it on the client workstation, using the client machine's existing copy of Microsoft Excel or Word.

Design Considerations

Data and software distribution tends to have a pyramidal shape, as shown in Figure 18, in two respects. First, in the design phase, it is important to lock in the upstream (towards the servers) API first, building the pyramid from the top down. If changes in the API occur, maintenance becomes more of a nightmare the closer the pyramid is to completion. Second, in the implementation phase, if the API remains constant, then it's best to have the bulk of the complicated work done upstream, where components can be swapped in and out of one place with relative ease. This is why we're resisting the urge to store any information locally on the client, such as in a Microsoft Access database. This application is designed so that the lightweight Reporter that runs on the client might need to be upgraded only once or twice a year. Meanwhile, the middle-tier ActiveX servers, especially the virtual data objects, can multiply with abandon, with little or no distribution difficulty. All the new reports they represent will be immediately available to the clients, since a list of reports is sent downstream to the client each time the application runs.

Figure 18 Data Distribution

Another feature of the application design is data transport. Just about everything that's passed between the ActiveX servers of our application is notification messages, plus a small amount of additional information. There's not a lot of data being marshaled between the out-of-process ActiveX servers, since that would run significantly slower. Instead, we're using the SQL Server data cache to hold the actual data, and we're passing the location of that data between the ActiveX servers.

Although our application gathers data from all over the enterprise, the client Reporter connects with our SQL Server only. This means that the client machines need to be configured with only one ODBC DSN and one extremely small set of ODBC configuration files, since Microsoft SQL Server 6.5 natively supports ODBC. In addition to minimizing the ODBC client components, this design allows our application to use Windows NT integrated security if the clients are running Windows NT Workstation.

A well-designed application is scalable over multiple developers at design time and over multiple machines at runtime. A point of diminishing return will eventually be reached, where the amount of communications overhead will exceed the performance gained by breaking down the components any further.

Conclusion

In the past, a cynical developer could be justified in observing that with Visual C++¨ you can pursue a development path for days before realizing it's just not going to work that way; you have to scrap everything and start over. With Visual Basic 3.0, you'd spend only hours pursuing a development path before realizing that it just wasn't going to work at all.

Visual Basic 4.0 Enterprise Edition is different. With an intuitive language and IDE, with the tools to build reusable classes and ActiveX servers, and with many methods of data access, Visual Basic 4.0 provides the resources necessary to build large-scale corporate applications such as our reporting facility with relative ease.

In the second part of this series, we'll get more into the hardcore component development of our data-warehouse-type reporting system (read: lots more code), plus other exciting topics from the land of corporate client/server development, including optimization, pool management, security, and internationalization.

This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.