Creating a control layer object (ActiveX DLL and class module)

We first identify what common code will be useful when dealing with the business object and reporting tools. In this example, I’ve chosen the OutputMedium property and the methods described in Table 3-6. The developer can set the OutputMedium property to indicate which report component will be used. In this example, we have three possibilities: Word, Excel, and rich text box.

Table 3-6

Methods in the ActiveX DLL

Method Description
Report Component Methods
OpenOutputMedium
Automatically opens the report component and provides the developer with a reference to the report component. Some of the components do not need to be opened; for example, a rich text box preexists on a form.
PrepareOutputMedium
Prepares the report component for data population.
PopulateCell
Populates a predefined cell with data. A cell in this example references a descriptor in an object that will be replaced with data.
DeleteRow
Removes a row within a report component.
InsertCopyRow
Duplicates a row within a report component.
Business Object Methods
ExtractCriteria
Extracts text from the report component that contains the report criteria.
StripData
Extracts data from the variant that holds data delimited by control characters.

Using the OutputMedium property and these methods, a developer can create detailed reports without having detailed knowledge of the business object or the report component being used. The following code uses the control layer object to produce a report in Microsoft Excel.

Sub Example()
' Set up the variables required; objOutput is a global object.

Dim cmUtils   As New CommonMethodsAndProperties
Dim csCompDet As New CompanyDetails
Dim vFrom     As Variant
Dim vList     As Variant
Dim nPos      As Integer

' Indicate that Excel should be used, with a particular template.
    cmUtils.OutputMedium = cmToExcel
    cmUtils.OpenOutputMedium objOutput, "Template.xlt" 

' Grab any criteria held within the template.
    vFrom = cmUtils.ExtractCriteria(objOutput, _
    ":FilterStart:", ":FilterEnd:")

' Pass this on to the business object to build the criteria.
    csCompDet.BuildCompanyCriteria vFrom, _
    vNoOfItems, vCompanyID, vCompanyDescription, _
    vCompanyInceptionDate, vCompanyCategory

' Tell the business object to build the result set.
    csCompDet.BuildCompanyList Limit:=vNoOfItems, _
    CompanyID:=vCompanyID,_
    CompanyDescription:=vCompanyDescription, _
    CompanyInceptionDate:=vCompanyInceptionDate, _
    CompanyCategory:=vCompanyCategory

' Get the result set.
    vList = csCompDet.GetCompanyList

' Prepare Excel for the data load.
    cmUtils.PrepareOutputMedium objOutput

' Loop through all company details, and populate the cells.
    For nPos = 1 To UBound(vList, 1)
        cmUtils.InsertCopyRow objOutput, ":CmpID:"
        cmUtils.PopulateCell objOutput, ":CmpDesc:", "" & _
            cmUtils.StripData(vList(nPos), 2, Chr(9))
        §
    Next nPos

' Delete the remaining row, a result of the InsertCopyRow method.
    cmUtils.DeleteRow objOutput, ":CmpID:"

' Now prepare Excel for viewing the result.
    cmUtils.PrepareOutputMedium objOutput

End Sub

The control layer simplifies the task considerably for the developer, especially when dealing with products that don’t have common object structures or code syntax, as the following code (used to find a string) demonstrates:

Rich Text Box

obj.SelStart = 0
obj.SelLength = 0
vFoundValue = obj.Find(striFind)

Microsoft Excel

obj.Range("A1").Activate
vFoundValue = obj.Cells.Find(What:= striFind)

Microsoft Word

obj.StartOfDocument
obj.EditFind striFind

It is far easier for the developer to remember one syntax rather than all three of these. For example, use v = cs.Find Text:=striFind, where the cs references the common ActiveX DLL containing the Find property.

Because VBCommonMethodsAndProperties.cls and CommonMethodsAndProperties.cls are similar in content, I concentrate on the latter, which is wrapped into an ActiveX DLL project. The project consists only of the CommonMethodsAndProperties class, which is set to GlobalMultiUse to ensure that we have only one DLL in existence for the application using it.

In the Declaration section, I set up two public properties, using Public and Enum to reference the type of report object we’re working with. I use nOutputMedium with the Property Get and Property Let routines to hold the current report type. The Class_Initialize routine ensures that the Excel report type is the default.

Option Explicit

Private nOutputMedium As Integer

Public Enum Constants
    cmToWord = 1            ' Produce output in Word.
    cmToExcel = 2           ' Produce output in Excel.
End Enum

Private Sub Class_Initialize()
    OutputMedium = cmToExcel
End Sub

Public Property Get OutputMedium()
    OutputMedium = nOutputMedium
End Property

Public Property Let OutputMedium(ByVal Method As Variant)
    nOutputMedium = CInt(Method)
End Property

The OpenOutputMedium method activates a user-selected object, in this case Word or Excel. In the Word section of the code, no error trap is required around the opening of a document because only one instance of Word.basic for Word 6/7 can exist at any one time. If no instance exists, one is automatically created, ensuring that no further checks are necessary to see whether we need to create the object.

Sub OpenOutputMedium(obj As Object, _
    Optional sFileName As String)

    Select Case OutputMedium
        Case cmToWord
            If sFileName <> "" Then
                Set obj = CreateObject("Word.basic")
                obj.Fileopen sFileName
            Else
                Set obj = CreateObject("Word.basic")
            End If
        Case cmToExcel
            On Error Resume Next
            Set obj = GetObject(, "Excel.application")
            If Err > 0 Then
                Set obj = CreateObject("Excel.application")
            End If
            Err.Clear
            On Error GoTo 0

            If sFileName <> "" Then
                obj.Workbooks.Open filename:=sFileName
            End If
    End Select
End Sub

The PrepareOutputMedium routine is necessary to help with performance problems. It toggles the report object window between minimum (or as small as is feasible) and maximum states to avoid excessive screen refresh.

Public Static Sub PrepareOutputMedium(obj As Object)

Dim blnOnOff As Boolean

    blnOnOff = Not blnOnOff ' Toggle true/false
    Select Case OutputMedium
        Case cmToWord
            If blnOnOff = False Then
                obj.DocMaximize
                obj.StartOfDocument
            Else
                ' We must not minimize the window because we will
                ' lose focus to it.
                If obj.DocMaximize() Or obj.DocMinimize() Then
                    obj.DocRestore
                End If
                ' Make 1/20 of the application size to help keep
                ' screen refreshes of the active document to a
                ' minimum. An error would occur if
                ' Word had the document minimized.
                obj.DocSize (Val(obj.AppInfo$(6)) * _
                0.05), Val(obj.AppInfo$(7) * 0.05)
            End If

        Case cmToExcel
            If blnOnOff = False Then
                obj.ActiveWindow.WindowState = xlMaximized
                obj.Range("A1").Activate
            Else
                obj.ActiveWindow.WindowState = xlMinimized
            End If
    End Select

End Sub

The InsertCopyRow routine is used to copy a row that is identified by the contents of striFind. This routine provides us with a useful technique for copying the style of a row while preserving the original row, which will be populated with data. In the Word section of the Case statement, I’ve checked whether or not a table is being used. This is necessary because the statements to copy a row in a table are different from those to copy to a standard line of text.

Public Sub InsertCopyRow(obj As Object, striFind As String)

Dim nFoundPos As Integer

    Select Case OutputMedium

        Case cmToWord
            obj.StartOfDocument
            obj.EditFind striFind
            nFoundPos = obj.GetSelStartPos()

            ' Find out whether we are in a table.
            On Error Resume Next
            obj.TableSelectTable
            If Err > 0 Then
                ' No - copy line.
                obj.EndOfLine 1
                obj.EditCopy
                obj.EndOfLine
                obj.InsertPara
            Else
                ' Yes - reset selection point.
                obj.SetSelRange nFoundPos, nFoundPos
                obj.TableSelectRow
                obj.EditCopy
            End If
            obj.EditPaste

        Case cmToExcel
            obj.Range("A1").Activate

            obj.Cells.Find(What:=striFind, _
                After:=obj.ActiveCell, LookIn:=xlFormulas, _
                LookAt:=xlPart, SearchOrder:=xlByRows, _
                SearchDirection:=xlNext, MatchCase:=False).Activate

            nFoundPos = obj.ActiveCell.Row
            obj.Rows(nFoundPos & ":" & nFoundPos).Select
            obj.Selection.Copy
            obj.Rows(nFoundPos + 1 & ":" & nFoundPos _
                + 1).Select
            obj.Selection.Insert Shift:=0
    End Select

End Sub

The DeleteRow routine deletes the row identified by the contents of striFind. Again, we need to check whether or not we are in a table when using Word.

Public Sub DeleteRow(obj As Object, striFind As String)

Dim nFoundPos As Integer

    Select Case OutputMedium
        Case cmToWord
            obj.StartOfDocument
            obj.EditFind striFind
            nFoundPos = obj.GetSelStartPos()

            ' Find out whether we are in a table.
            On Error Resume Next
            obj.TableSelectTable
            If Err > 0 Then
                ' No - delete line.
                obj.EndOfLine 1
                obj.EditClear
            Else
                ' Yes - reset selection point.
                obj.SetSelRange nFoundPos, nFoundPos
                obj.TableDeleteRow
            End If
        Case cmToExcel
            obj.Cells.Find(What:=striFind, _
                After:=obj.ActiveCell, LookIn:=xlFormulas, _
                LookAt:=xlPart, SearchOrder:=xlByRows, _
                SearchDirection:=xlNext, _
                MatchCase:=False).Activate

            nFoundPos = obj.ActiveCell.Row
            obj.Rows(nFoundPos & ":" & nFoundPos).Select
            obj.Selection.Delete Shift:=xlUp
    End Select

End Sub

The PopulateCell routine finds a field descriptor and replaces it with the contents of striReplace. Notice that the routine always searches from the start of the object. This ensures that the first occurrence of the field descriptor is detected; keep in mind that there will always be two or more field descriptors while rows are being added to a report. The second occurrence of the descriptor is needed to keep the format and location of the cells that require population while the first copy is overwritten with text.

Sub PopulateCell(obj As Object, striFind As String, _
    striReplace As String)

Dim nFoundPos    As Integer
Dim nFoundLength As Integer

    Select Case OutputMedium

        Case cmToWord
            obj.StartOfDocument

            obj.EditFind striFind, _
                striReplace, 0, 0, 0, 0, 0, 0, 1

        Case cmToExcel
            obj.Range("A1").Activate

            obj.Cells.Find(What:=striFind, _
                After:=obj.ActiveCell, LookIn:=xlFormulas, _
                LookAt:=xlPart, SearchOrder:=xlByRows, _
                SearchDirection:=xlNext, MatchCase:=False).Activate

            nFoundPos = obj.ActiveCell.Row
            obj.Rows(nFoundPos & ":" & nFoundPos).Select

            obj.Selection.Replace What:=striFind, _
                Replacement:=striReplace, LookAt:=xlPart, _
                SearchOrder:=xlByRows, MatchCase:=False
    End Select

End Sub

The StripData routine shown on page 122 is used to extract text between the niPos - 1 and niPos occurrences of the contents of sCompare.

Public Function StripData(ByVal viData As Variant, _
    ByVal niPos As Integer, ByVal sCompare As String) _
    As Variant

Dim nStartPos As Integer
Dim nEndPos   As Integer
Dim nPos      As Integer

    nEndPos = 0
    For nPos = 1 To niPos
        nStartPos = nEndPos + 1
        nEndPos = InStr(nStartPos + 1, viData, sCompare)
    Next nPos

    If nEndPos = 0 Then nEndPos = Len(viData) + 1

    StripData = Mid(viData, nStartPos, nEndPos - nStartPos)

End Function

The ExtractCriteria routine extracts text from the report object identified by the contents of sCriteriaStart and sCriteriaEnd. These fields are used to determine the start and end points of the selection criteria entered by the user. You need to know where the criteria are located because you might have more than one set of criteria per report. The following routine is designed to extract the criteria entered in report output medium:

Public Function ExtractCriteria(obj As Object, _
    ByVal sCriteriaStart As String, _
    ByVal sCriteriaEnd As String) As Variant

Dim nFoundStartPos As Integer
Dim nFoundEndPos   As Integer
Dim sRTFText       As String
Dim vTempCriteria  As Variant

    vTempCriteria = ""

    Select Case OutputMedium

        Case cmToWord
            obj.StartOfDocument
            obj.EditFind sCriteriaStart, "", _
                0, 0, 0, 0, 0, 0, 1
            nFoundStartPos = obj.GetSelStartPos()
            obj.StartOfDocument
            obj.EditFind sCriteriaEnd, "", 0, 0, 0, 0, 0, 0, 1
            nFoundEndPos = obj.GetSelEndPos()
            vTempCriteria = _
                obj.GetText$(nFoundStartPos, nFoundEndPos)

            obj.SetSelRange nFoundStartPos, nFoundEndPos
            obj.EditClear

        Case cmToExcel
            obj.Range("A1").Activate
            obj.Cells.Find(What:=sCriteriaStart, _
                After:=obj.ActiveCell, LookIn:=xlFormulas, _
                LookAt:=xlPart, SearchOrder:=xlByRows, _
                SearchDirection:=xlNext, MatchCase:=False).Activate

            nFoundStartPos = obj.ActiveCell.Row
                obj.Cells.Find(What:=sCriteriaEnd, _
                After:=obj.ActiveCell, LookIn:=xlFormulas, _
                LookAt:=xlPart, SearchOrder:=xlByRows, _
                SearchDirection:=xlNext, MatchCase:=False).Activate

            nFoundEndPos = obj.ActiveCell.Row
                obj.Rows(nFoundStartPos & ":" & _
                nFoundEndPos).Select

            obj.Selection.Copy
            vTempCriteria = Clipboard.GetText
            obj.Selection.Delete Shift:=xlUp
            obj.CutCopyMode = False
            obj.Range("A1").Activate
    End Select

    ' Now remove the Start and Criteria statements
    ' if they still exist.
    If vTempCriteria & "" <> "" Then
        If InStr(vTempCriteria, sCriteriaEnd) > 0 Then
            vTempCriteria = Left(vTempCriteria, _
                Len(vTempCriteria) - Len(sCriteriaEnd))
        End If
        If InStr(vTempCriteria, sCriteriaStart) > 0 Then
            vTempCriteria = Right(vTempCriteria, _
                Len(vTempCriteria) - Len(sCriteriaStart))
        End If
    End If

    ExtractCriteria = vTempCriteria

End Function