Building Business Objects

For the remainder of this chapter, we'll look at the implementation of three business objects from the Island Hopper application. We'll start by looking at the bus_PaymentC component, which is implemented in Visual Basic. This component uses several other components to implement its functionality and illustrates the use of the object context CreateInstance method. Next we'll look at the bus_InvoiceC component, which is implemented in Visual C++ using ATL and the COM compiler support. This component uses the object context DisableCommit method to implement a business rule. It also illustrates how to use the object context CreateInstance method from C++. Last we'll look at the util_TakeANumber component, which is implemented in Visual Basic and uses the SPM.

WARNING
Remember that if you are trying to implement these components on your own system, you need to take a few extra precautions to ensure that the rest of the Island Hopper application will continue to run without being rebuilt. These precautions will be noted prior to each step that might break the application.

Implementing bus_PaymentC in Visual Basic

Let's start by looking at the bus_PaymentC component, which processes payments for classified ads from customers. The bus_PaymentC component has the following two methods:

  • AddPayment records a payment from a customer in a database and updates the customer's account balance.

  • GetByCustID retrieves a list of payments for a given customer from the database.

The simplicity of the component's business rules lets us focus in on the mechanics of composing functionality.

As with any MTS component, the first step is to create a skeleton component, just as we did in Chapter 8. For bus_PaymentC, the methods can be implemented on the default interface defined by Visual Basic. To create skeleton methods, you simply define a public function in the class module for each method, as shown here:

 Public Function AddPayment(ByVal lngCustomerID As Long, _                            ByVal strMethod As String, _                            ByVal datPaymentDate As Date, _                            ByVal dblPaymentAmt As Double, _                            ByVal lngInvoiceID As Long, _                            ByVal strCardNumber As String, _                            ByVal datCardExpDate As Date) As Long Public Function GetByCustID(ByVal lngCustomerID As Long) _     As ADODB.Recordset 

This component does not need to retain per-object state across method calls, so the basic structure of each method, shown here, is the same as for our data objects:

 Public Function MyMethod On Error GoTo ErrorHandler     ' Do work.    .    .    .    GetObjectContext.SetComplete    Exit Function ErrorHandler:     ' Clean up local variables.    .    .    .    GetObjectContext.SetAbort    Err.Raise Err.Number, _       SetErrSource(modName, "PROCNAME"), Err.Description End Function 

Where things get interesting is in how AddPayment does its work. The business logic looks something like this, in pseudocode:

 1. Get a payment ID. 2. Add a payment record to the classified ads database. 3. Update the customer record in the classified ads database     with the new account balance. 

Naturally, we don't want the customer's account balance to be updated unless the payment is recorded in the database, or vice versa. Thus, the bus_PaymentC component should be marked as "requires a transaction" and the object context should be used to create any subordinate objects needed to implement the method's functionality. In this way, if any of the steps fail, the transaction can be aborted and no changes will be made to the database.

In the three-tier model, business objects do not directly access database tables. Instead, they use the services of data objects. Clearly steps 2 and 3 in the preceding pseudocode should be handled by data objects. Fortunately, Island Hopper has just the data objects we need, db_PaymentC and db_CustomerC.

To accomplish step 1, we'll use the util_TakeANumber component, which we'll look at in detail in the section "Implementing util_TakeANumber Using the SPM" later in this chapter.

In Visual Basic, the first step to using these components is to add references to their type libraries to the project. Then use the object context to create an instance of each component that's needed, as shown in the following example:

 Dim objTakeANumber As util_TakeANumber.TakeANumber Set objTakeANumber =      GetObjectContext.CreateInstance("util_TakeANumber.TakeANumber") 

That's all there is to it! The call to CreateInstance will ensure that information about the current context, such as its transaction identifier, flows to the new object. Once you have a reference to the object, you make method calls just as you would for any other object. The only time you really need to distinguish MTS objects from other COM objects is if you want to pass a reference to another object or client application. To do that, you must use the SafeRef function to create the reference. This technique ensures that the context wrapper reference, not the reference to the "real" object, is passed around.

The complete code for the bus_PaymentC Payment class is shown in Listing 9-1. Note that if an error occurs in any of the subordinate objects used by the AddPayment method, the error will be caught by the method's error handler. AddPayment calls SetAbort to abort the containing transaction if any errors occur in its subordinate objects. Also, AddPayment simply reraises the original error, identifying itself as the source of the error. A more sophisticated object might permit a transaction to commit even if some of the subordinate objects reported errors, or it might raise component-specific errors rather than passing on the lower-level error codes.

Listing 9-1. The bus_PaymentC Payment class source code.

 Option Explicit Const modName = "bus_PaymentC.Payment" '******************************************************************** ' AddPayment ' Purpose: Adds a payment to the database ' Inputs:  lngCustomerID  -- the unique customer ID number '          strMethod      -- the payment method '          datPaymentDate -- the payment date '          dblPaymentAmt  -- the payment amount '          lngInvoiceID   -- the invoice ID for this payment '          strCardNumber  -- the card or check number used for this payment '          datCardExpDate -- the credit card expiration date, if needed ' Returns: The new payment ID number '******************************************************************** Public Function AddPayment(ByVal lngCustomerID As Long, _     ByVal strMethod As String, ByVal datPaymentDate As Date, _     ByVal dblPaymentAmt As Double, ByVal lngInvoiceID As Long, _     ByVal strCardNumber As String, ByVal datCardExpDate As Date) As Long         Dim objTakeANumber As util_TakeANumber.TakeANumber    Dim lngPaymentID As Long    Dim dblTotal As Double    Dim objPayment As DB_PAYMENTCLib.db_Payment    Dim objCustomer As db_CustomerC.Customer         On Error GoTo ErrorHandler         ' Create an instance of the util_TakeANumber.TakeANumber object to    ' get the next payment number.    Set objTakeANumber = _     GetObjectContext.CreateInstance("util_TakeANumber.TakeANumber")    lngPaymentID = objTakeANumber.GetANumber("PaymentID")    ' Create an instance of the db_PaymentC.Payment object    ' to add a payment to the database.    Set objPayment = _     GetObjectContext.CreateInstance("db_PaymentC.Payment.1")    objPayment.AddPayment lngCustomerID, lngPaymentID, strMethod, _     datPaymentDate, dblPaymentAmt, lngInvoiceID, strCardNumber, _     datCardExpDate        ' Create an instance of the db_CustomerC.Customer object    ' to update the customer balance.    Set objCustomer = _     GetObjectContext.CreateInstance("db_CustomerC.Customer")    dblTotal = (-1) * dblPaymentAmt    objCustomer.UpdateBalance lngCustomerID, dblTotal         ' Allow the MTS transaction set to proceed.    GetObjectContext.SetComplete        ' Return the invoice number.    AddPayment = lngPaymentID        Exit Function ErrorHandler:    ' Roll back the MTS transaction set.    GetObjectContext.SetAbort         Err.Raise Err.Number, SetErrSource(modName, "AddPayment"), _     Err.Description      End Function '******************************************************************** ' GetByCustID ' Purpose: Retrieves invoices from the database for a particular  '          customer ID number ' Inputs:  lngCustomerID -- the unique customer ID number ' Returns: A Recordset object containing the invoice information '******************************************************************** Public Function GetByCustID(ByVal lngCustomerID As Long) As ADODB.Recordset         Dim objPayment As DB_PAYMENTCLib.db_Payment         On Error GoTo ErrorHandler       ' Create an instance of the db_PaymentC.Payment object     ' to retrieve payment information from the database.    Set objPayment = GetObjectContext.CreateInstance("db_PaymentC.Payment")         Set GetByCustID = objPayment.GetByCustID(lngCustomerID)         ' Allow the MTS transaction set to proceed.    GetObjectContext.SetComplete    Exit Function      ErrorHandler:    ' Roll back the MTS transaction set.    GetObjectContext.SetAbort         Err.Raise Err.Number, SetErrSource(modName, "GetByCustID"), _     Err.Description      End Function 

The GetByCustID method is even simpler than AddPayment. All it needs to do is call the db_PaymentC component's GetByCustID method, as shown here:

 Dim objPayment As DB_PAYMENTCLib.db_Payment Set objPayment = _     GetObjectContext.CreateInstance("db_PaymentC.Payment") Set GetByCustID = objPayment.GetByCustID(lngCustomerID) 

This is a fairly common pattern, and at first it might seem like unnecessary overhead. Why not just call the data object directly? The main issue is protecting your data. Consider a scenario in which a presentation layer application needs to retrieve a list of payments for a customer. This application probably runs on a remote user workstation, using the interactive user's identity. If the application calls the data object directly, you must grant access to the data object to every user who might run the application. Now, by default, the user has access to every method exposed by the data object. Data objects typically expose low-level methods, without consideration for business rules regarding how those methods should be used. Most application-level users should not have direct access to these methods.

By placing business objects between the presentation layer and the data objects, you can restrict the operations that users can perform and still write general-purpose data objects. Business objects also give you the option to apply business rules about how data objects should be used. Unless you are absolutely sure that there will never be any rules about how a particular data object should be used, you are probably better off providing a business object.

Step by Step: Creating the bus_PaymentC Component

  1. Create a skeleton bus_PaymentC component, following the instructions from the sidebar "Step by Step: Creating the Skeleton db_CategoryC Component" in Chapter 8. The project name should be bus_PaymentC, and the class name should be Payment.

  2. Add the following skeleton method declarations:

     Public Function AddPayment(ByVal lngCustomerID As Long, _                            ByVal strMethod As String, _                            ByVal datPaymentDate As Date, _                            ByVal dblPaymentAmt As Double, _                            ByVal lngInvoiceID As Long, _                            ByVal strCardNumber As String, _                            ByVal datCardExpDate As Date) As Long Public Function GetByCustID(ByVal lngCustomerID As Long) _     As ADODB.Recordset 

  3. Add the following lines to the General Declarations section of the Payment class module to define the module name used in error reporting:

     Option Explicit Const modName = "bus_PaymentC.Payment" 

  4. Add the GLOBALS.BAS module from the \IslandHopper\Source\ Server_Components\Globals directory on the companion CD to the project, to provide access to Island Hopper's standard error reporting function.

  5. Add references to the following type libraries in your project:

     Microsoft ActiveX Data Objects Library Microsoft Transaction Server Type Library db_PaymentC 1.0 Type Library db_CustomerC util_TakeANumber 

    If you don't see db_PaymentC, db_CustomerC, or util_TakeANumber in the list of available references, you need to build or install those components.

  6. Add the following code to the AddPayment and GetByCustID methods to establish the basic framework for the methods:

     On Error GoTo ErrorHandler    ' TODO: Do work.    .    .    .    GetObjectContext.SetComplete    Exit Function ErrorHandler:    GetObjectContext.SetAbort    Err.Raise Err.Number, SetErrSource(modName, "PROCNAME"), _     Err.Description 

    Replace PROCNAME in the preceding code with AddPayment or GetByCustID, as appropriate.

  7. Replace the line TODO: Do work in the AddPayment method with the following code, which creates the subordinate objects and calls the appropriate methods to complete the AddPayment method's functionality:

     Dim objTakeANumber As util_TakeANumber.TakeANumber Dim lngPaymentID As Long Dim dblTotal As Double Dim objPayment As DB_PAYMENTCLib.db_Payment Dim objCustomer As db_CustomerC.Customer ' Create an instance of the util_TakeANumber.TakeANumber object ' to get the next payment number. Set objTakeANumber = _  GetObjectContext.CreateInstance("util_TakeANumber.TakeANumber") lngPaymentID = objTakeANumber.GetANumber("PaymentID") ' Create an instance of the db_PaymentC.Payment object ' to add a payment to the database. Set objPayment = _  GetObjectContext.CreateInstance("db_PaymentC.Payment.1") objPayment.AddPayment lngCustomerID, lngPaymentID, strMethod, _     datPaymentDate, dblPaymentAmt, lngInvoiceID, strCardNumber, _     datCardExpDate      ' Create an instance of the db_CustomerC.Customer object ' to update the customer balance. Set objCustomer = _  GetObjectContext.CreateInstance("db_CustomerC.Customer") dblTotal = (-1) * dblPaymentAmt objCustomer.UpdateBalance lngCustomerID, dblTotal 

  8. Following the call to GetObjectContext.SetComplete in the AddPayment method, add this line to return the payment ID to the caller:

     AddPayment = lngPaymentID 

  9. Replace the line TODO: Do work in the GetByCustID method with the following code, which creates a db_PaymentC object and calls its GetByCustID method to retrieve the list of payments:

     Dim objPayment As DB_PAYMENTCLib.db_Payment Set objPayment = _  GetObjectContext.CreateInstance("db_PaymentC.Payment") Set GetByCustID = objPayment.GetByCustID(lngCustomerID) 

    NOTE
    If you want to build the bus_PaymentC component at this point on a machine that has the Island Hopper application installed, you should select the Binary Compatibility option on the Project Properties Component tab and point to the \Source\Server_Components\CompatibleDlls\bus_PaymentC.dll directory in your Island Hopper installation. If you don't make this adjustment, components and applications that use bus_PaymentC will not operate correctly.

  10. (optional) Build the component. After building the component, you should select the Binary Compatibility option on the Project Properties Component tab. If you haven't already pointed to the prebuilt BUS_PAYMENTC.DLL that came with Island Hopper, copy the DLL you just built to a safe place and point to that. Doing so will help prevent incompatible modifications to the Payment public interface.

Implementing bus_InvoiceC in C++

Now let's turn our attention to a slightly more complex business object, bus_InvoiceC, which generates customer invoices for classified advertisements. This component is written in Visual C++ using ATL and the Visual C++ COM compiler support. It implements the following four business rules:

  • It must be possible to create invoices. An invoice is identified by a unique key and consists of a header and one or more detail records.

  • When an invoice is created, the customer's account balance must be updated to reflect the invoice amount.

  • Clients must be able to retrieve a specific invoice header, given the invoice identifier.

  • Clients must be able to retrieve a list of all the invoices associated with a particular customer. (It's not necessary to return the details of each invoice, just the header.)

The last three rules aren't much different from the rules enforced by the bus_PaymentC component. The method implementations in bus_InvoiceC will use the object context to create subordinate objects that do part of the work required by each method.

The first rule is much more interesting. It says that an invoice cannot be persisted until at least one detail record has been added to the invoice. As we've seen, defining separate AddHeader and AddDetail methods creates an interface that is much simpler to use than attempting to define a CreateInvoiceWithArbitraryNumberOfItems method. But this means that the invoice object must not permit any containing transaction to commit until both AddHeader and AddDetail have been called. Thus, bus_InvoiceC is our first stateful MTS component. Note in this case that the state information isn't stored anywhere. The state is implicit in the sequence of method calls made against the object.

Based on the preceding four business rules, we can define these four methods to expose from bus_InvoiceC:

  • AddHeader creates a new invoice with no detail records.

  • AddDetail adds a detail record to an invoice. AddDetail must be called at least once per call to AddHeader, within the same transaction.

  • GetByID retrieves a specific invoice from the database, given an invoice identifier.

  • GetByCustID retrieves a list of invoices for a given customer from the database.

The first step in implementing the bus_InvoiceC component is to create the skeleton component using the ATL COM AppWizard and ATL Object Wizard, as described in the sidebar "Step by Step: Creating the Skeleton db_PaymentC Component" in Chapter 8. The Visual C++ IDE can be used to define skeletons for each method, as shown here:

 HRESULT AddHeader([in] long lngCustomerID,                    [in, string] BSTR strDescription,                    [in] double dblTotal,                    [in] long lngReference,                    [out, retval] long *lngInvoiceID); HRESULT AddDetail([in] long lngInvoiceID,                    [in, string] BSTR strProduct,                    [in, string] BSTR strDescription,                    [in] long lngQty,                    [in] double dblAmount); HRESULT GetByID([in] long lngInvoiceID,                  [out, retval] _Recordset **pInvoice); HRESULT GetByCustID([in] long lngCustomerID,                      [out, retval] _Recordset **pInvoice); 

As for our data objects, the COM compiler support can be used to import the ADO type library and generate smart pointers for the ADO objects. In this case, all we need are the definitions for Recordset objects, which will be passed back to callers.

GetByID and GetByCustID are simple stateless methods that just call through to the equivalent method in the db_InvoiceC data object. Thus, we will want smart pointers for the _Invoice interface exposed by db_InvoiceC. To generate smart pointers, you can use the #import preprocessor directive to read in the db_InvoiceC type library, as shown in the following code, just as you do to generate smart pointers for ADO objects:

 #import "..\DLLs\db_InvoiceC.dll" raw_interfaces_only using namespace db_InvoiceC; 

Notice the use of the raw_interfaces_only attribute on the #import statement. This attribute indicates that error handling wrapper functions and property wrappers should not be generated. The lack of property wrappers doesn't really matter, since db_InvoiceC doesn't have any properties. However, when error handling wrappers aren't generated, you will need to retrieve and inspect the HRESULT returned by each method call instead of relying on a try/catch block to handle _com_error exceptions.

Once you have imported the type library, you can use the object context to create an instance of the db_InvoiceC component, as shown in the following example:

 _InvoicePtr pObjDB_Invoice; hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),                                         __uuidof(_Invoice),                                        (void**)&pObjDB_Invoice); if (FAILED(hr)) {     bErrorFlg = true;     goto errHandler; } 

With the interface pointer in hand, you can call whatever methods you want, as shown here:

 hr = pObjDB_Invoice->GetByCustID(lngCustomerID, pRecordSet); if (FAILED(hr)) goto errHandler; 

The complete source for the GetByID and GetByCustID methods is shown in Listing 9-2.

Listing 9-2. The source code for the bus_InvoiceC methods.

 #include "stdafx.h" #include "comdef.h" #include "bus_InvoiceC.h" #include "bus_Invoice.h" #include <mtx.h> // Import the VB DLLs. #import "..\DLLs\util_TakeANumber.dll" raw_interfaces_only using namespace util_TakeANumber; #import "..\DLLs\db_CustomerC.dll" raw_interfaces_only using namespace db_CustomerC; #import "..\Interfaces\IdbCustomer\IdbCustomer.dll" raw_interfaces_only using namespace IDBCUSTOMERLib; #import "..\DLLs\db_InvoiceC.dll" raw_interfaces_only using namespace db_InvoiceC;  ////////////////////////////////////////////////////////////////////////////// Cbus_Invoice STDMETHODIMP Cbus_Invoice::InterfaceSupportsErrorInfo(REFIID riid) {    static const IID* arr[] =     {       &IID_Ibus_Invoice,    };        for (int i=0; i <sizeof(arr)/sizeof(arr[0]); i++)    {       if (InlineIsEqualGUID(*arr[i],riid))          return S_OK;    }    return S_FALSE; } HRESULT Cbus_Invoice::Activate() {    HRESULT hr = GetObjectContext(&m_spObjectContext);    if (SUCCEEDED(hr))       return S_OK;    return hr; } BOOL Cbus_Invoice::CanBePooled() {    return TRUE; }  void Cbus_Invoice::Deactivate() { }  //******************************************************************* // AddHeader // Purpose: Adds a header to an invoice // Inputs:  lngCustomerID  -- the unique customer ID number //          strDescription -- the header description //          dblTotal       -- the total amount on the header //          lngReference   -- the invoice header reference number // Returns: Invoice number //******************************************************************* STDMETHODIMP Cbus_Invoice::AddHeader(long lngCustomerID,                                       BSTR strDescription,                                       double dblTotal,                                       long lngReference,                                       long * lngInvoiceID) {    // Check for an invalid return pointer.    if (lngInvoiceID == NULL) return E_POINTER;        HRESULT hr = S_OK;        // Pointer for util_TakeANumber    _TakeANumberPtr pObjTakeANumber;        // Pointer for db_Invoice    _InvoicePtr pObjDB_Invoice;        // Pointer for db_Customer    _CustomerPtr pObjDB_Customer;        // Invoice ID string    BSTR bstrTakeANumberType;        // Error message flag    bool bErrorFlg = false;    if (!m_spObjectContext)    {       AtlReportError(CLSID_bus_Invoice, "No object context",                       IID_Ibus_Invoice, hr);       return hr;    }        hr = m_spObjectContext->CreateInstance(__uuidof(TakeANumber),      __uuidof(_TakeANumber), (void**)&pObjTakeANumber);        if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }        // Get the next invoice number.    bstrTakeANumberType = ::SysAllocString(L"InvoiceID");    hr = pObjTakeANumber->GetANumber(&bstrTakeANumberType,lngInvoiceID);    ::SysFreeString(bstrTakeANumberType);        if(FAILED(hr)) goto errHandler;        // Add invoice header.        hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice);        if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }            hr = pObjDB_Invoice->AddHeader(*lngInvoiceID, lngCustomerID,      strDescription, dblTotal, lngReference, lngInvoiceID);        if(FAILED(hr)) goto errHandler;        // Update customer balance.        hr = m_spObjectContext->CreateInstance(__uuidof(Customer),      __uuidof(_Customer), (void**)&pObjDB_Customer);    if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }        hr = pObjDB_Customer->UpdateBalance(lngCustomerID, dblTotal);    if (FAILED(hr)) goto errHandler;        // We are finished and happy.    m_spObjectContext->DisableCommit();        return hr;     errHandler:        TCHAR *pErrMsg = NULL;        // We are finished and unhappy.    m_spObjectContext->SetAbort();        if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg, IID_Ibus_Invoice, hr);       LocalFree(pErrMsg);    }        return hr; } //******************************************************************* // AddDetail // Purpose: Adds a detail record to the invoice // Inputs:  lngInvoiceID   -- the unique invoice ID number //          strProduct     -- the name of the product //          strDescription -- the header description //          lngQty         -- quantity of items //          dblAmount      -- the amount on the detail // Returns: N/A //******************************************************************* STDMETHODIMP Cbus_Invoice::AddDetail(long lngInvoiceID,                                       BSTR strProduct,                                       BSTR strDescription,                                       long lngQty,                                       double dblAmount) {    HRESULT hr = S_OK;        // Class ID for db_Invoice    _InvoicePtr pObjDB_Invoice;        // Error message flag    bool bErrorFlg = false;            hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice);        if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }        hr = pObjDB_Invoice->AddDetail(lngInvoiceID, strProduct, strDescription,                                   lngQty, dblAmount);        if (FAILED(hr)) goto errHandler;        // We are finished and happy.    m_spObjectContext->EnableCommit();        return hr;     errHandler:        TCHAR *pErrMsg = NULL;        // We are finished and unhappy.    m_spObjectContext -> SetAbort();        if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg, IID_Ibus_Invoice, hr);    }    LocalFree(pErrMsg);        return hr; } //******************************************************************* // GetByID // Purpose: Gets an invoice by ID // Inputs:  lngInvoiceID -- the unique invoice ID number // Returns: A Recordset object containing invoices //******************************************************************* STDMETHODIMP Cbus_Invoice::GetByID(long lngInvoiceID,                                    _Recordset **pRecordSet) {    // Check for invalid return pointer.    if (pRecordSet == NULL) return E_POINTER;        HRESULT hr = S_OK;        // Class ID for db_Invoice    _InvoicePtr pObjDB_Invoice;        // Error message flag    bool bErrorFlg = false;        hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice);        if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }        hr = pObjDB_Invoice->GetByID(lngInvoiceID, pRecordSet);    if (FAILED(hr)) goto errHandler;        // We are finished and happy.    m_spObjectContext->SetComplete();    return hr;     errHandler:        TCHAR *pErrMsg = NULL;        m_spObjectContext->SetAbort();        if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg, IID_Ibus_Invoice, hr);       LocalFree(pErrMsg);    }        return hr; } //******************************************************************* // GetByCustID // Purpose: Adds an invoice customer ID // Inputs:  lngCustomerID -- the unique customer ID number // Returns: A Recordset object containing invoices //******************************************************************* STDMETHODIMP Cbus_Invoice::GetByCustID(long lngCustomerID,                                          _Recordset **pRecordSet) {    // Check for invalid return pointer.    if (pRecordSet == NULL) return E_POINTER;        HRESULT hr = S_OK;        // Class ID for db_Invoice    _InvoicePtr pObjDB_Invoice;        // Error message flag    bool bErrorFlg = false;        hr = m_spObjectContext->CreateInstance (__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice);    if (FAILED(hr))    {       bErrorFlg = true;       goto errHandler;    }        hr = pObjDB_Invoice->GetByCustID(lngCustomerID, pRecordSet);    if (FAILED(hr)) goto errHandler;        // We are finished and happy.    m_spObjectContext->SetComplete();        return hr;     errHandler:        TCHAR *pErrMsg = NULL;        m_spObjectContext->SetAbort();        if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg, IID_Ibus_Invoice, hr);       LocalFree(pErrMsg);    }        return hr; } 

The AddHeader and AddDetail methods are somewhat more interesting. The source code for these methods is also shown in Listing 9-2. The basic structure is the same as for the Get methods: set up an error handler and then use the object context to create subordinate objects and call their methods. Unlike the methods we've looked at up to now, however, AddHeader and AddDetail should not call the object context SetComplete method when they finish their work successfully.

After AddHeader has been called, a new invoice exists without any detail records, which violates one of our business rules. The invoice cannot be persisted until AddDetail is called at least once for the invoice. To keep this from happening, you can call the object context DisableCommit method. Calling DisableCommit ensures that the object's transaction cannot be committed. It can abort, but it can't commit and cause the partial invoice to be persisted.

After AddDetail has been called, the business rule requiring at least one detail record for each invoice is satisfied, and the object's transaction can commit. You call the object context EnableCommit method to reenable the commit operation. When EnableCommit is called, MTS does not reclaim the object's state, which means that you can call AddDetail multiple times on the same "real" object. This capability isn't so important for the bus_InvoiceC component, since there isn't any per-object state, but it could be very important for a similar component that did maintain per-object state. When the root object calls SetComplete, the transaction will commit, the invoice will be persisted, and the bus_InvoiceC object will be deactivated.

Step by Step: Implementing the bus_InvoiceC Component

  1. Create a skeleton bus_InvoiceC component, following the instructions from the sidebar "Step by Step: Creating the Skeleton db_PaymentC Component" in Chapter 8. The project name should be bus_InvoiceC. The short name for the object should be bus_Invoice, and the ProgID should be bus_InvoiceC.Invoice.

  2. Add the following skeleton method implementations:

     HRESULT AddHeader([in] long lngCustomerID,                    [in, string] BSTR strDescription,                    [in] double dblTotal,                    [in] long lngReference,                    [out, retval] long *lngInvoiceID); HRESULT AddDetail([in] long lngInvoiceID,                    [in, string] BSTR strProduct,                    [in, string] BSTR strDescription,                    [in] long lngQty,                    [in] double dblAmount); HRESULT GetByID([in] long lngInvoiceID,                  [out, retval] _Recordset **pRecordset); HRESULT GetByCustID([in] long lngCustomerID,                      [out, retval] _Recordset **pRecordset); 

  3. Add the following statements to the STDAFX.H file, directly above the line //{{AFX_INSERT_LOCATION}} to create the COM compiler support wrapper classes for ADO:

     #import "msado15.dll" rename("EOF", "ADOEOF") using namespace std; 

    If the compiler is not able to locate MSADO15.DLL when you build the project, you should either specify the complete path to the DLL in the #import statement or add its location to your include path.

  4. Add the following statements at the beginning of the BUS_INVOICE.CPP file to import the type libraries for components used by bus_InvoiceC:

     #import "..\DLLs\util_TakeANumber.dll" raw_interfaces_only using namespace util_TakeANumber; #import "..\DLLs\db_CustomerC.dll" raw_interfaces_only using namespace db_CustomerC; #import "..\Interfaces\IdbCustomer\IdbCustomer.dll"      raw_interfaces_only using namespace IDBCUSTOMERLib; #import "..\DLLs\db_InvoiceC.dll" raw_interfaces_only using namespace db_InvoiceC; 

    Note that the type libraries for both db_CustomerC and the externally defined interfaces it implements, located in IDBCUSTOMER.DLL, must be imported. The type library for db_CustomerC does not contain the actual interface definitions.

  5. Add the following code to the GetByID and GetByCustID methods to establish the basic framework that calls the object context SetComplete method when all work has been completed successfully or calls the object context SetAbort method and raises an error if one occurs:

        // Check for invalid return pointer.    if (pRecordSet == NULL) return E_POINTER;    HRESULT hr = S_OK;    // Class ID for db_Invoice    _InvoicePtr pObjDB_Invoice;    // Error message flag    bool bErrorFlg = false;    // TODO: Do work.    // We are finished and happy.    m_spObjectContext->SetComplete();    return hr; errHandler:    TCHAR *pErrMsg = NULL;    m_spObjectContext->SetAbort();    if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg,                      IID_Ibus_Invoice, hr);       LocalFree(pErrMsg);    }    return hr; 

    Notice the use of the variable bErrorFlg. This value is true when a system error occurs, and false otherwise. If bErrorFlg is true, the method needs to fill in the ErrorInfo object. This is what the call to AtlReportError does. If bErrorFlg is false, a subordinate object will have already set the ErrorInfo object if an error has occurred, and all the method has to do is return the HRESULT.

  6. Replace the line // TODO: Do work in the GetByID method with the following code, which creates a db_InvoiceC object and calls its GetByID method:

     hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice); if (FAILED(hr)) {    bErrorFlg = true;    goto errHandler; } hr = pObjDB_Invoice->GetByID(lngInvoiceID, pRecordSet); if (FAILED(hr)) goto errHandler; 

  7. Repeat the preceding step for the GetByCustID method, except call the db_Invoice object's GetByCustID method, as shown here:

     hr = pObjDB_Invoice->GetByCustID(lngCustomerID, pRecordSet); 

  8. Add the following code to the AddHeader and AddDetail methods to establish the basic error handling framework:

        HRESULT hr = S_OK;    // Error message flag    bool bErrorFlg = false;    // TODO: Do work.    .    .    .    return hr; errHandler:    TCHAR *pErrMsg = NULL;    m_spObjectContext->SetAbort();    if (bErrorFlg)    {       FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |                      FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr,                     MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),                      (LPTSTR) &pErrMsg, 0, NULL);       AtlReportError(CLSID_bus_Invoice, pErrMsg,                      IID_Ibus_Invoice, hr);       LocalFree(pErrMsg);    }    return hr; 

  9. At the top of the AddHeader method, add this code to ensure that there is a place to write the return value:

     // Check for invalid return pointer. if (lngInvoiceID == NULL) return E_POINTER; 

  10. Replace the line // TODO: Do work in the AddHeader method with the following code, which declares the variables used by AddHeader and verifies that the object context exists:

     // Pointer for util_TakeANumber _TakeANumberPtr pObjTakeANumber; // Pointer for db_Invoice _InvoicePtr pObjDB_Invoice; // Pointer for db_Customer _CustomerPtr pObjDB_Customer; // Invoice ID string BSTR bstrTakeANumberType; if (!m_spObjectContext) {    AtlReportError(CLSID_bus_Invoice, "No object context",                    IID_Ibus_Invoice, hr);    return hr; } // TODO: Continue work. . . . 

    Without an object context, there is no way to guarantee that the AddHeader call will be followed by at least one call to AddDetail. Thus, we return an error if the object context is not available. This error would occur if you tried to use the component outside of the MTS environment.

  11. Replace the line // TODO: Continue work in the AddHeader method with the following code, which creates the subordinate objects, calls their methods to do the work required to create a new invoice, and then calls the object context DisableCommit method to ensure that the containing transaction does not commit without any calls to AddDetail:

     hr = m_spObjectContext->CreateInstance(__uuidof(TakeANumber),      __uuidof(_TakeANumber), (void**)&pObjTakeANumber); if (FAILED(hr)) {    bErrorFlg = true;    goto errHandler; } // Get the next invoice number. bstrTakeANumberType = ::SysAllocString(L"InvoiceID"); hr = pObjTakeANumber->GetANumber(&bstrTakeANumberType,                                  lngInvoiceID); ::SysFreeString(bstrTakeANumberType); if(FAILED(hr)) goto errHandler; // Add invoice header. hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice); if (FAILED(hr)) {    bErrorFlg = true;    goto errHandler; } hr = pObjDB_Invoice->AddHeader(*lngInvoiceID, lngCustomerID,      strDescription, dblTotal, lngReference, lngInvoiceID); if(FAILED(hr)) goto errHandler; // Update customer balance. hr = m_spObjectContext->CreateInstance(__uuidof(Customer),      __uuidof(_Customer), (void**)&pObjDB_Customer); if (FAILED(hr)) {    bErrorFlg = true;    goto errHandler; } hr = pObjDB_Customer->UpdateBalance(lngCustomerID, dblTotal); if (FAILED(hr)) goto errHandler; // We are finished and happy. m_spObjectContext->DisableCommit(); 

  12. Replace the line // TODO: Do work in the AddDetail method with the following code, which creates a db_InvoiceC component to write the detail record to the database and then calls EnableCommit to enable the containing transaction to commit:

     // Class ID for db_Invoice _InvoicePtr pObjDB_Invoice; hr = m_spObjectContext->CreateInstance(__uuidof(Invoice),      __uuidof(_Invoice), (void**)&pObjDB_Invoice); if (FAILED(hr)) {    bErrorFlg = true;    goto errHandler; } hr = pObjDB_Invoice->AddDetail(lngInvoiceID, strProduct,       strDescription, lngQty, dblAmount); if (FAILED(hr)) goto errHandler; // We are finished and happy. m_spObjectContext->EnableCommit(); 

  13. (optional) Build the component by selecting the Build bus_InvoiceC.dll command from the Build menu.

Implementing util_TakeANumber Using the SPM

Last let's look at the util_TakeANumber component, which is used to generate unique numeric identifiers. The util_TakeANumber component contains these two COM classes:

  • TakeANumber, which retrieves the next available number of a particular type. Number types are implemented as shared property groups in the SPM. Any client of the TakeANumber class can define a new number type. TakeANumber has a single method, GetANumber.

  • TakeANumberUpdate, which is used by TakeANumber to get a block of available numbers of a given type. Information about the last block obtained for each type is persisted to a database table. TakeANumberUpdate has a single method, Update.

The util_TakeANumber component is implemented as two separate classes because the transaction requirements of each class are different. Once TakeANumberUpdate returns a block of numbers to TakeANumber and that block is placed in the SPM, the fact that the block was allocated absolutely, positively must be persisted. Otherwise, it would be possible for the block of numbers to be allocated twice and there would be no guarantee that the number returned by the GetANumber method would be unique. Thus, the TakeANumberUpdate class should be marked as "requires a new transaction." On the other hand, TakeANumber does not require a transaction to do its work correctly but it should support transactions so that clients can handle an error in GetANumber correctly.

The source code for TakeANumber is shown in Listing 9-3, and the source code for TakeANumberUpdate is shown in Listing 9-4.

Listing 9-3. The TakeANumber source code.

 Option Explicit Const incQty = 100 Const modName = "util_TakeANumber.TakeANumber" '******************************************************************** ' GetANumber ' Purpose: Gets the next available number from the appropriate  '          property group ' Inputs:  strPropGroupIn -- the property group ' Returns: A long value '******************************************************************** Public Function GetANumber(strPropGroupIn As String) As Long    On Error GoTo ErrorHandler    Dim spmMgr As New SharedPropertyGroupManager    Dim spmGroup As SharedPropertyGroup    Dim spmPropMaxNum As SharedProperty    Dim spmPropNextNum As SharedProperty    Dim objTakeUpdate As util_TakeANumber.TakeANumberUpdate    Dim bResult As Boolean    ' Attempt to create a new property group. If property group of the     ' same name already exists, retrieve a reference to that group.    Set spmGroup = spmMgr.CreatePropertyGroup(strPropGroupIn, _     LockSetGet, Process, bResult)    ' Attempt to create new properties for the above group.      ' This will set the individual property values to 0.      ' If the group already exists, this will retrieve the     ' current value for the properties.    Set spmPropMaxNum = spmGroup.CreateProperty("MaxNumber", bResult)    Set spmPropNextNum = _     spmGroup.CreateProperty("NextNumber", bResult)         ' If the property group did not exist, set the next number to 1.    If Not bResult Then       spmPropNextNum.Value = 1    End If    ' The following If clause creates a new block of ID numbers if the max     ' number has been reached. The max number represents the last number in     ' a block of ID numbers. The next number represents the current ID number.    ' If these values are the same, it's time to issue a new block of numbers.    If spmPropNextNum.Value >= spmPropMaxNum.Value Then       ' Create an instance of Util_takeANumber.       Set objTakeUpdate = _       GetObjectContext.CreateInstance("util_TakeANumber.TakeANumberUpdate")       ' Set the next number. See TakeANumberUpdate code for specifics.       spmPropNextNum.Value = objTakeUpdate.Update(incQty, strPropGroupIn)       'Set the new max number.       spmPropMaxNum.Value = spmPropNextNum.Value + incQty    End If         ' Increase the next number by 1.    spmPropNextNum.Value = spmPropNextNum.Value + 1    ' Clean up the object and commit the transaction if one exists.    GetObjectContext.SetComplete    ' Return the next number.    GetANumber = spmPropNextNum.Value    Exit Function      ErrorHandler:    ' Clean up object and abort the transaction if one exists.    GetObjectContext.SetAbort         Err.Raise Err.Number, SetErrSource(modName, "GetANumber"), _     Err.Description End Function 

Listing 9-4. The TakeANumberUpdate source code.

 Option Explicit Const modName = "util_TakeANumber.TakeANumberUpdate" Const fileDSN = "dbTakeANumber.DSN" '******************************************************************** ' Update ' Purpose: Gets the next available ID number for the appropriate table  '          (property group) ' Inputs:  lngInc       -- the increment value '          strPropGroup -- the property group ' Returns: A long value that is the next number from the table '******************************************************************** Public Function Update(lngInc As Long, strPropGroup As String) As Long         On Error GoTo ErrorHandler         Dim lngNextNumber As Long    Dim rs As New ADODB.Recordset    Dim strSQL As String    Dim conn As New ADODB.Connection         ' Get the starting point for the next block of numbers from the database.    strSQL = "Select NextNumber from TakeANumber " & _             "where PropertyGroupName = '" & strPropGroup & "'"    rs.Open strSQL, "FILEDSN=" & fileDSN, adOpenKeyset, _     adLockBatchOptimistic, -1    ' Update the starting point for the next block of numbers.    lngNextNumber = rs!NextNumber    rs.Close         strSQL = "UPDATE TakeANumber SET NextNumber = " & (lngNextNumber + _             lngInc) & " WHERE PropertyGroupName = '" & strPropGroup & "'"    conn.Open "FILEDSN=" & fileDSN    conn.Execute strSQL         ' Return the next number to the caller.    Update = lngNextNumber         ' Clean up object and commit the transaction if one exists.    GetObjectContext.SetComplete            Exit Function      ErrorHandler:       If Not rs Is Nothing Then       Set rs = Nothing    End If       GetObjectContext.SetAbort       Err.Raise Err.Number, SetErrSource(modName, "Update"), Err.Description End Function 

The basic structure of the TakeANumber and TakeANumberUpdate methods should be familiar by now, so we won't walk through the code in detail, but you should be aware of two significant factors. First, note that TakeANumberUpdate is a typical data access class. It updates a Properties table, which stores the next available number for each property group name. The Update method uses a syntax we haven't discussed yet to pull a value from an ADO Recordset, as shown here:

 lngNextNumber = rs!NextNumber 

This shortcut notation is equivalent to

 lngNextNumber = rs("NextNumber") 

in which NextNumber is the name of a Field in the Recordset.

Second, let's look at how to use the SPM. We need to store two values for each property group—the next available number (NextNumber) and the last number that has been allocated (MaxNumber). Whenever NextNumber reaches MaxNumber, TakeANumberUpdate should be called to get a new block of numbers.

To use the SPM, you must first create an instance of the shared property group manager. To do so, set a reference to the Shared Property Manager Type Library in your project. Then you can create a new SharedPropertyGroupManager object, as shown here:

 Dim spmMgr As New SharedPropertyGroupManager 

Now you can create the shared property group for the type of number the client wants. If a group with that name already exists, you get back a reference to the existing group. The code looks like this:

 Dim spmGroup As SharedPropertyGroup Dim bResult As Boolean Set spmGroup = spmMgr.CreatePropertyGroup(strPropGroupIn, _     LockSetGet, Process, bResult) 

The value returned in bResult is true if the group has been created, false if the group already existed. You can use this value to determine whether you need to initialize your shared properties.

Once you have a reference to the property group, you can create the shared properties by using the CreateProperty method. If the shared property already exists, you just get a reference to the existing property. The Value property gives you access to the data stored in the shared property. Here's what the code to create a new property and set its value looks like:

 Dim spmPropNextNum As SharedProperty Set spmPropNextNum = spmGroup.CreateProperty("NextNumber", bResult) If Not bResult Then    spmPropNextNum.Value = 1 End If 

As you can see, using the SPM from Visual Basic is very straightforward. Whenever you have data that needs to be shared across transaction boundaries or across multiple objects and persisting the data is either unnecessary or too slow, consider using the SPM.

Step by Step: Creating the util_TakeANumber Component

  1. Create a skeleton util_TakeANumber component, following the instructions in the sidebar "Step by Step: Creating the Skeleton db_CategoryC Component" in Chapter 8. The project name should be util_TakeANumber and the class name should be TakeANumber.

  2. Add a new class module, named TakeANumberUpdate, to the project. Verify that the class module's Instancing property is set to MultiUse.

  3. Add the following skeleton method implementations. GetANumber should be added to the TakeANumber class. Update should be added to the TakeANumberUpdate class.

     Public Function GetANumber(strPropGroupIn As String) As Long Public Function Update(lngInc As Long, strPropGroup As String) _     As Long 

  4. Add these lines to the General Declarations section of the TakeANumber class:

     Option Explicit Const incQty = 100 Const modName = "util_TakeANumber.TakeANumber" 

    and these lines to the General Declarations section of the TakeANumberUpdate class:

     Const modName = "util_TakeANumber.TakeANumberUpdate" Const fileDSN = "dbTakeANumber.DSN" 

    The incQty constant holds the number of values to allocate on each call to Update.

  5. Add the GLOBALS.BAS module from the \IslandHopper\Source\ Server_Components\Globals directory on the companion CD to the project, to get access to Island Hopper's standard error reporting function.

  6. Add references to the following type libraries in your project:

    • Microsoft ActiveX Data Objects Library

    • Microsoft Transaction Server Type Library

    • Shared Property Manager Type Library

  7. Use the code in Listing 9-3 to fill in the implementation of TakeANumber's GetANumber method.

  8. Use the code in Listing 9-4 to fill in the implementation of TakeANumberUpdate's Update method.

    NOTE
    If you want to build the util_TakeANumber component at this point on a machine that has the Island Hopper application installed, you should select the Binary Compatibility option on the Project Properties Component tab and select the UTIL_TAKEANUMBER.DLL file located in the Source\Server_Components\CompatibleDlls directory in your Island Hopper source installation. If you don't make this adjustment, components and applications that use util_TakeANumber will not operate properly.

  9. (optional) Build the component. After building the component, you should select the Binary Compatibility option on the Project Properties Component tab. If you haven't already pointed to the prebuilt UTIL_ TAKEANUMBER.DLL that came with Island Hopper, copy the DLL you just built to a safe place and select it. Doing so will help prevent incompatible modifications to the public interfaces.



Designing Component-Based Applications
Designing Component-Based Applications
ISBN: 0735605238
EAN: 2147483647
Year: 1997
Pages: 98
Authors: Mary Kirtland

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net