Skip to content

Visual Report Writer and The Web (II)

As a follow-up story on Visual Report Writer and the web, this time we present the first report available on the demo website for Visual Report Writer. You might think, "only one?" but read on and discover that there is enough to tell about this report and the integration.

Invoices Report

The first report that can be started on the website is called Invoice. The report uses the Microsoft Adventure Works 2000 database as its data source and utilizes 14 tables to build the results. Notably, customer information needs to be collected from two tables. Customers can be shops or individuals. To collect the information, the report employs a left outer join between the customer table and the shop table, as well as a left outer join between the customer and individual tables. Both links originate from the same source column, which is impossible from a DataFlex table relationship as it would link to either table A or table B. The table relationship model is illustrated in the following image:

Database Relationship Model

Integration

The web report view allows visitors to browse through the customers. Because a normal relationship is not possible (the customer ID needs to relate to either a shop or an individual), the relate_main_file event in the customer data dictionary object is augmented to find either the store or the individual's record when locating a particular customer. This is feasible because the customer record contains a column that specifies the customer type (store or individual). The Relate_Main_File event is as follows:

Procedure Relate_Main_File    
    Forward Send Relate_Main_File
    If (SQLCustomer.CustomerType = 'I') Begin
        Clear SQLIndividual
        Move SQLCustomer.CustomerID to SQLIndividual.CustomerID
        Find Eq SQLIndividual by 3
    End
    Else Begin
        If (SQLCustomer.CustomerType = 'S') Begin
            Clear SQLStore
            Move SQLCustomer.CustomerID to SQLStore.CustomerID
            Find Eq SQLStore by 3
        End
    End
End_Procedure

To display either the name of the store or the name of the individual, the cWebForm control for the customer name is not directly connected to a table.column reference but instead utilizes the OnSetCalculatedValue event.

Object oCustomerNameForm is a cWebForm
    Set piColumnSpan to 10
    Set piColumnIndex to 3
    Set pbShowLabel to False
    Set pbEnabled to False

    Procedure OnSetCalculatedValue String ByRef sValue
        If (SQLCustomer.CustomerType = 'I') Begin
            Move SQLIndividual.LastName to sValue
        End
        Else Begin
            If (SQLCustomer.CustomerType = 'S') Begin
                Move SQLStore.Name to sValue
            End
            Else Begin
                Move '' to sValue
            End
        End
    End_Procedure
End_Object

Finally, to ensure the prompt object functions correctly, the peUpdateMode property of the prompt object is set to umPromptCustom, and the psPromptUpdateCallBack is assigned to a custom method named ShowAndFindCustomer. The code for this method is as follows:

Object oCustomerIDForm is a cWebForm
    Entry_Item SQLCustomer.CustomerID
    Set psLabel to "Customer:"
    Set piColumnSpan to 3
    Set peLabelAlign to alignRight

    WebPublishProcedure ShowAndFindCustomer

    Procedure ShowAndFindCustomer Handle hoPrompt
        Handle hoServer
        Integer iMainFile

        Get Server of hoPrompt to hoServer
        Get Main_File of hoServer to iMainFile
        If (iMainFile = SQLIndividual.File_Number) Begin
            Send Clear of oSQLCustomer_DD
            Get Field_Current_Value of hoServer Field SQLIndividual.CustomerID to SQLCustomer.CustomerID
            Send Find of oSQLCustomer_DD EQ 1
        End
        Else Begin
            Send Clear of oSQLCustomer_DD
            Get Field_Current_Value of hoServer Field SQLStore.CustomerID to SQLCustomer.CustomerID
            Send Find of oSQLCustomer_DD EQ 1
        End
    End_Procedure
End_Object

If you closely examine the selection list, you will see that the dialog contains two tab pages, each containing a cWebList object. The first tab page displays the store names, while the second tab page shows the names of the individuals. I experimented with combining both sets of data into one list using an ESQL statement but deemed it too slow due to the volume of data and the need to sort names. While not implemented, we could have created a view in the database that combines both tables and adjusted the table link to use the view.

The code for the double selection list is as follows:

Use cWebModalDialog
Use cWebPanel.pkg
Use cWebButton.pkg
Use cWebPromptList.pkg
Use cWebColumn.pkg
Use cWebTabContainer.pkg
Use cWebTabPage.pkg

Use cSQLIndividualDataDictionary.dd
Use cSQLStoreDataDictionary.dd

Object oSQLCustomerWebLookup is a cWebModalDialog
    Set piColumnCount to 8
    Set psCaption to "Select Customer"
    Set piWidth to 700
    Set piHeight to 400
    Object oSQLIndividual_DD is a cSQLIndividualDataDictionary
    End_Object

    Object oSQLStore_DD is a cSQLStoreDataDictionary
    End_Object

    Object oWebTabContainer is a cWebTabContainer
        Set pbFillHeight to True

        Object oStoresTabPage is a cWebTabPage
            Set psCaption to "Stores"
            Set piColumnCount to 8

            Object oStoresPromptList is a cWebPromptList
                Set pbFillHeight to True
                Set piColumnSpan to 8
                Set piOrdering to 4
                Set Server to oSQLStore_DD
                Set peUpdateMode to umPromptCustom
                Set psPromptUpdateCallback to "ShowAndFindCustomer"
                Set piUpdateColumn to 0

                Object oSQLCustomerIDForm is a cWebColumn
                    Entry_Item SQLStore.CustomerID
                    Set psCaption to "Nr"
                    Set piWidth to 30
                End_Object

                Object oSQLCustomerNameForm is a cWebColumn
                    Set psCaption to "Name"
                    Set piWidth to 150
                    Entry_Item SQLStore.Name
                End_Object
            End_Object

            Object oOkButton is a cWebButton
                Set psCaption to "OK"
                Set piColumnSpan to 1
                Set piColumnIndex to 5

                Procedure OnClick
                    Send Ok of oStoresPromptList
                End_Procedure
            End_Object 

            Object oCancelButton is a cWebButton
                Set psCaption to "Cancel"
                Set piColumnSpan to 1
                Set piColumnIndex to 6

                Procedure OnClick
                    Send Cancel of oStoresPromptList
                End_Procedure
            End_Object 

            Object oSearchButton is a cWebButton
                Set psCaption to "Search..."
                Set piColumnSpan to 1
                Set piColumnIndex to 7

                Procedure OnClick
                    Send Search of oStoresPromptList
                End_Procedure
            End_Object        

            Procedure SelectAndClose
                Send Ok of oStoresPromptList
            End_Procedure
        End_Object

        Object oIndividualsTabPage is a cWebTabPage
            Set psCaption to "Individuals"
            Set piColumnCount to 8

            Object oIndividualsPromptList is a cWebPromptList
                Set pbFillHeight to True
                Set piColumnSpan to 8
                Set piOrdering to 4
                Set Server to oSQLIndividual_DD
                Set peUpdateMode to umPromptCustom
                Set psPromptUpdateCallback to "ShowAndFindCustomer"
                Set piUpdateColumn to 0

                Object oSQLCustomerIDForm is a cWebColumn
                    Entry_Item SQLIndividual.CustomerID
                    Set psCaption to "Nr"
                    Set piWidth to 30
                End_Object

                Object oSQLCustomerNameForm is a cWebColumn
                    Set psCaption to "Name"
                    Set piWidth to 150
                    Entry_Item SQLIndividual.LastName
                End_Object
            End_Object 

            Object oOkButton is a cWebButton
                Set psCaption to "OK"
                Set piColumnSpan to 1
                Set piColumnIndex to 5

                Procedure OnClick
                    Send Ok of oIndividualsPromptList
                End_Procedure
            End_Object 

            Object oCancelButton is a cWebButton
                Set psCaption to "Cancel"
                Set piColumnSpan to 1
                Set piColumnIndex to 6

                Procedure OnClick
                    Send Cancel of oIndividualsPromptList
                End_Procedure
            End_Object 

            Object oSearchButton is a cWebButton
                Set psCaption to "Search..."
                Set piColumnSpan to 1
                Set piColumnIndex to 7

                Procedure OnClick
                    Send Search of oIndividualsPromptList
                End_Procedure
            End_Object        

            Procedure SelectAndClose
                Send Ok of oIndividualsPromptList
            End_Procedure
        End_Object
    End_Object

    Set pbServerOnSubmit to True

    Procedure OnSubmit 
        Handle hoCurrentCard

        Get CurrentCard of oWebTabContainer to hoCurrentCard
        Send SelectAndClose of hoCurrentCard
    End_Procedure 

    Set pbServerOnShow to True

    Procedure OnShow
        Send InitializePromptList of oStoresPromptList
        Send InitializePromptList of oIndividualsPromptList
    End_Procedure
End_Object

Special in the above code are the OnShow and OnSubmit routines. The OnShow is typically kept hidden for the developer. In this case, it is coded to address both selection lists and instructs them to load data. The OnSubmit is special because it specifies which selection list should return its information.

The cWebView also contains two cWebDateForm controls to select the date range, with the minimum and maximum date values from the table displayed in the controls. The dates are retrieved by executing an ESQL statement during the OnLoad event of the control.

Object oOrderFromDate is a cWebDateForm
    Set psLabel to "Order Date From:"
    Set piColumnSpan to 4
    Set peLabelAlign to alignRight

    Procedure OnLoad
        Handle hoSQL hoConnection hoStatement
        Integer iFetchResult
        String sDate
        Date dDate

        Forward Send OnLoad

        Get Create (RefClass (cSQLHandleManager)) to hoSQL
        Get SQLFileConnect of hoSQL SQLCustomer.File_Number to hoConnection
        Get SQLOpen of hoConnection to hoStatement
        Send SQLExecDirect of hoStatement "select MIN ([salesorderheader].[orderdate]) from [salesorderheader]"
        Repeat
            Get SQLFetch of hoStatement to iFetchResult
            If (iFetchResult <> 0) Begin
                Get SQLColumnValue of hoStatement 1 to sDate
                Get SQLDateToDFDate of hoStatement sDate to dDate
                Set psValue to dDate
            End
        Until (iFetchResult = 0)

        // Clean up
        Send SQLClose to hoStatement
        Send SQLDisconnect to hoConnection
        Send Destroy of hoSQL
    End_Procedure
End_Object

If you were to create a report without a customer selected and thus only based on the date range, you would generate a report with more than 14,000 pages of information. While Visual Report Writer can handle this, the web does not like to wait that long, so it is wise to limit the result set. When you click the button to print the invoices, a check for customer ID and a date range check is executed.

Object oRunReportMenuItem is a cWebMenuItem
    Set psCaption to "Print Invoices"
    Set psTooltip to "Print the invoices using the selections"
    Set psCSSClass to "VRWPrintReportButton"

    Procedure OnLoad
        String[] aParams
        Forward Send OnLoad
        Move "ConfirmResponse" to aParams[0]
        Move 2 to aParams[1]
        Send ClientAction "setActionMode" aParams
    End_Procedure

    Procedure ConfirmResponse Integer eConfirmMode
        If (eConfirmMode = cmYes) Begin
            Send GenerateReport of oReport
        End
    End_Procedure

    WebPublishProcedure ConfirmResponse

    Procedure OnClick
        DateTime dtFrom dtTo
        Integer iCustomerNumber
        WebGet psValue of oCustomerIDForm to iCustomerNumber
        If (iCustomerNumber = 0) Begin
            Send ShowInfoBox "A Customer Selection is required"
            Procedure_Return
        End
        WebGet psValue of oOrderFromDate to dtFrom
        WebGet psValue of oOrderToDate to dtTo
        If (SpanTotalDays (dtTo - dtFrom) > 100) Begin
            Send ShowYesNo of oWebApp (Self) (RefProc (ConfirmResponse)) "The date range exceeds 100 days. Are you sure you want to generate a report with this range? Reporting may take some time to generate." "Date Range Large!"
        End
        Else Begin
            Send GenerateReport of oReport
        End
    End_Procedure
End_Object

The use of ConfirmResponse would be sufficient to initiate the report, but the OnLoad event completes it by instructing the framework to display a loading indicator during report generation. If the report was started from a button, this would typically be activated automatically, but the use of the menu item control does not trigger that. With the code in OnLoad, the waiting circle appears.

In the code above, the report is generated by sending a message named GenerateReport. This is not a method defined in the cVRWReport class but is coded in the object to generate this specific report. In the method, the report needs to be located and opened, and it must be instructed to output the results to a file, as this report uses PDF for displaying results. For those familiar with Visual Report integration, it should be clear that OpenReport and ExportReport are the key messages to be sent.

During OpenReport, the event OnInitializeReport is triggered. In the following code, you can see that this event is used to set the value of a parameter named ImagePath. The report uses an image and needs to be informed where to locate the file with the image. We need to do this because the location will differ on the web server compared to my development machine. The image is stored in the same folder as the report, and thus psReportLocation can be used as the path.

Procedure OnInitializeReport
    String sReportLocation
    Integer iParameter

    Get psReportLocation to sReportLocation
    Get ParameterIdByName C_USEMAINVRWREPORTID 'ImagePath' to iParameter
    Set psParameterValue C_USEMAINVRWREPORTID iParameter to sReportLocation
    Send SetFilters
End_Procedure

To maintain code readability, a routine to set the filter information is created, named SetFilters. In this routine, the values of the customer ID, start date, and end date are retrieved. Notice that the values need to be retrieved via WebGet and not via a normal Get. Also, the dates are converted to a string value needed by SQL Server for selection via the function DateTimeToString, which is a function of cVRWReport.

Procedure SetFilters
    Integer iCustomerId
    Date dFrom dTo
    String sFrom sTo

    WebGet psValue of oCustomerIDForm to iCustomerId
    WebGet psValue of oOrderFromDate to dFrom
    WebGet psValue of oOrderToDate to dTo

    Send RemoveAllFilters C_USEMAINVRWREPORTID

    If (iCustomerId <> 0) Begin
        Send AddFilter C_USEMAINVRWREPORTID "{Customer.CustomerId}" C_VRWEqual iCustomerId
    End
    If (not (IsNullDateTime (dFrom))) Begin
        Get DateTimeToString dFrom to sFrom
        Send AddFilter C_USEMAINVRWREPORTID "{SalesOrderHeader.OrderDate}" C_VRWGreaterThanOrEqual sFrom
    End
    If (not (IsNullDateTime (dTo))) Begin
        Get DateTimeToString dTo to sTo
        Send AddFilter C_USEMAINVRWREPORTID "{SalesOrderHeader.OrderDate}" C_VRWLessThanOrEqual sTo
    End
End_Procedure

Finally, we arrive at the point of report output and display. As mentioned, the output needs to be spooled to a PDF file. To prevent two users/sessions from receiving the same or each other's results, the code creates a unique filename. It does this by calling the new ReportCacheFileName function in the VRW library. The generated results are stored in a folder that is not directly accessible over the web. A special function named DownloadURL (defined in the resource manager) converts the absolute path into a unique download URL that is only available for a limited time and only for the current session ID.

Procedure GenerateReport
    String sReportId sFile sUrl
    VRWPDFExportOptions PDFExportOptions
    Boolean bCanceled

    Get OpenReport to sReportId
    If (sReportId <> "") Begin
        Get DefaultPDFExportOptions to PDFExportOptions
        Set pPDFExportOptions to PDFExportOptions
        Get ReportCacheFileName ".pdf" to sFile
        If (sFile <> "") Begin
            Send ExportReport C_vrwPDF sFile
            Get pbCanceled to bCanceled
            If (not (bCanceled)) Begin
                Get DownloadURL of ghoWebResourceManager sFile to sUrl
                If (sUrl <> "") Begin
                    WebSet psUrl of oViewer to sUrl
                End
            End
        End
        Send CloseReport sReportId
    End
End_Procedure

The URL generated by DownloadURL is assigned to the oViewer object, which is an instantiation of the cWebIFrame class.

I hope this blog guides you on the path of using Visual Report Writer together with the DataFlex Web Application Framework.