Site Server - Implementing Pipeline Interfaces in Site Server 3.0: Converting Existing COM Components

June 1999 

Summary

This article illustrates how to create a simple component with a simple COM object. Necessary steps are then presented to manually add and implement the interfaces required for a fully functional pipeline component. The scope of this article covers converting COM servers so that they expose pipeline-compliant interfaces.

Introduction

Many existing Component Object Model (COM) objects contain business rules that organizations wish to move into the Internet Commerce arena. This migration can be accomplished in several ways. Very often it is advisable to revisit the business rules and design the components with Internet Commerce architecture in mind. This implies re-architecting the solution from the beginning, bearing in mind what it would mean to add Microsoft® Internet Information Services (IIS), Microsoft Transaction Server (MTS), and Microsoft Site Server Commerce to the execution of the component.

However, sometimes one might wish to modify the component to make it a pipeline-compliant component. Several ways to approach this type of modification include—from external pipeline component wrappers, to component aggregation, to implementing the necessary interfaces in the component to make it pipeline-compatible.

In this article, we create a simple component with a simple COM object. Then we follow the steps necessary to manually add and implement the interfaces required for a fully functional pipeline component. We could have also explored writing a pipeline component that simply used the original COM object, or we could have added a COM object to the original component that encapsulated the original object. However, the scope of this article covers converting COM servers so that they expose pipeline-compliant interfaces.

Most of the information in this document comes directly from the Commerce Documentation and the Commerce SDK. For further information, see https://www.microsoft.com/siteserver/commerce/ .

Create the Initial Component

To keep things simple, we will create a simple component to calculate a tax amount:

  1. Create a new Active Template Library (ATL) COM AppWizard project, and name it "SimpleTax." 

  2. Create it as a DLL. MTS support is optional. 

  3. Add a simple ATL Object to the project, and name it "CityTax." 

  4. Right click the CityTax interface in the Microsoft Windows Explorer, and add a property named "Percent" of type float

  5. Right click again on the CityTax interface, and add a method named "CalculateTax." For parameters, add "float Amount, float *TotalTax." 

  6. Open the SimpleTax.idl file. Look for the idl of the CalculateTax method. It should look like this: 

    [id(2), helpstring("method CalculateTax")] HRESULT CalculateTax(float 
    

Amount, float* TotalTax);

Modify it to have a return value by adding the following: 

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">[id(2), helpstring("method CalculateTax")] HRESULT CalculateTax(float 

Amount, [out, retval] float* TotalTax);

  1. Add the following to the CCityTax constructor: 

    m_Percent = 0.0; 

  2. Add a private member to the CCityTax object name m_Percent of type float. 

  3. Modify the Percent property functions for CCityTax to be the following: 

    STDMETHODIMP CCityTax::get_Percent(float *pVal)
    

{ *pVal = m_Percent;

return S_OK; }

STDMETHODIMP CCityTax::put_Percent(float newVal) { m_Percent = newVal;

return S_OK; }

  1. Modify the CalculateTax method to look like the following: 

    STDMETHODIMP CCityTax::CalculateTax(float Amount, float *TotalTax)
    

{ *TotalTax = Amount * (m_Percent/100);

return S_OK; }

  1. Compile, and the COM DLL should be ready. Test its functionality using your favorite method, i.e., Microsoft® Visual J++®, Microsoft Visual C++®, Microsoft Visual Basic®, Microsoft FoxPro®, Microsoft Visual Basic Scripting Edition (VBScript), and so on. 

Converting a COM Component for the Pipeline

Implementing the Mandatory Interface IPipelineComponent

IPipelineComponent is the only mandatory interface needed for the component to behave as a pipeline component. It includes two methods: EnableDesign and Execute. The Execute method is where all of the processing takes place, and it is where we will be doing the majority of our work. All other interfaces, though needed, are optional.

  1. Copy the Computil.cpp file into your project directory. 

  2. Add Computil.cpp to your project. 

  3. Copy the following .H files into the project directory:

    Commerce.h 

    Contains the interface definitions for the Site Server 3.0 Commerce objects. 

    Comp_ids.h 

    Contains the class identifiers (CLSIDs) for the pipeline objects and interfaces. 

    Mspu_guids.h 

    Contains the CLSIDs for the order processing pipeline objects and interfaces. 

    Pipeline.h 

    Contains the definitions for the pipeline interfaces. 

    Pipecomp.h 

    Contains the proxy/stub definitions for the pipeline interfaces. 

    Pipe_stages.h 

    Contains the GUIDs for the OPP stages. 

    As an option, you could add the path the SDK includes for Microsoft Site Server 3.0 Commerce Edition (SSCE) to your project by adding a line similar to: 

    /I "C:\Microsoft Site Server\SiteServer\commerce\sdk\commerce\include" in the C/C++ tab. 

  4. Add this line to your COM DLL: 

    CPP file: #include "mspu_guids.h" 

  5. Build and resolve any build errors. 

  6. Add the following to your component's inheritance list: 

    public IpipelineComponent 

  7. Add the following to the component's .H file: 

    #include "computil.h"
    

#include "pipeline.h" #include "pipecomp.h" #include "pipe_stages.h"

  1. Ensure that the declarations for IPipelineComponent methods are added to the CCityTax declaration. Add the following to the class declaration for your component: 

    // IPipelineComponent
    

STDMETHOD(Execute)( IDispatch* pdispOrder, IDispatch* pdispContext, LONG lFlags, LONG* plErrorLevel); STDMETHOD (EnableDesign) (BOOL fEnable);

  1. Implement the methods in the CityTax.cpp file. Add the following to the CityTax.cpp file: 

    // IPipelineComponent Methods
    

// STDMETHODIMP CCityTax::Execute ( IDispatch* pdispOrder, IDispatch* pdispContext, LONG lFlags, LONG* plErrorLevel) { HRESULT hRes = S_OK;

// TODO: Add code that performs the main operations for this component return hRes; }

STDMETHODIMP CCityTax::EnableDesign(BOOL fEnable) { return S_OK; }

  1. Add the following to the COM Interface map for CityTax

    COM_INTERFACE_ENTRY(IPipelineComponent)
    
  1. Ensure that, when the component is registered, it is registered for Pipeline stage affinity. Therefore, create a custom registration call and add the following lines to the component's class declaration: 

    static HRESULT WINAPI UpdateRegistry(BOOL bRegister)
    

{ HRESULT hr = _Module.UpdateRegistryClass(GetObjectCLSID(), _T("SimpeTax.CityTax.1"), _T("SimpeTax.CityTax"), IDS_PROJNAME, THREADFLAGS_BOTH, bRegister); if (SUCCEEDED(hr)) { // TODO: Add stage affinities here. hr = RegisterCATID(GetObjectCLSID(), CATID_MSCSPIPELINE_COMPONENT); hr = RegisterCATID(GetObjectCLSID(), CATID_MSCSPIPELINE_ANYSTAGE); } return hr; };

and comment the following line: 

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">DECLARE_REGISTRY_RESOURCEID(IDR_CITYTAX)
  1. Test our new converted component's functionality in the pipeline, and add some code to the body of the Execute method. Later on, you need to modify the functionality of this to use our persisted properties. However, for the moment, hardcode 10%. Also, pay particular attention to the code to manipulate the Commerce.Dictionary object. Helper functions for manipulating objects exposed by the Commerce Library can be found in the Computil.H and Computil.CPP files. The code you need to place in the Execute method is as follows: 

    IDictionary *pDictOrder = NULL;
    

HRESULT hr;

VARIANT var; VARIANT Price; float Tax;

OPP_ERRORLEV ErrorLevel = OPPERRORLEV_FAIL; //initialize variants VariantInit(&var); VariantInit(&Price);

if(pdispOrder == NULL) return E_INVALIDARG;

// Get the OrderForm Dictionary. if(SUCCEEDED(hr=pdispOrder->QueryInterface(IID_IDictionary, (void**)&pDictOrder))) { //Test Code hr = GetDictValue(pDictOrder, L"Test_Price", &var); VariantChangeType(&Price,&var,0,VT_R4);

//Call the original method for processing. m_Percent=.10; //This will be commented out when property persistence is working. hr = this->CalculateTax(Price.fltVal , &Tax);

var.fltVal = Tax; var.vt = VT_R4; hr = PutDictValue(pDictOrder, L"Test_Tax", var); }

if(SUCCEEDED(hr)) ErrorLevel = OPPERRORLEV_SUCCESS;

if(plErrorLevel) *plErrorLevel = ErrorLevel;

if(pDictOrder) pDictOrder->Release(); return hr;

  1. Compile the component at this point, and run it in a pipeline or a micropipe. When the component is executed, it will look for a "Test_Price" value in the pdispOrder dictionary, and write a "Test_Tax" value to the same dictionary. 

  2. Continue developing this component in the rest of this article by implementing the optional interfaces required for configuration. This completes the necessary augmentations to convert the original component into one that can be used by the Pipeline. 

Implementing IPipelineComponentDescription

The IPipelineComponentDescription is an optional interface that makes it possible for pipeline components to identify the values they read from the pipe context, and the name/value pairs they read or write from or to the OrderForm—for order processing pipeline components—or Dictionary—for Commerce Interchange Pipeline (CIP) components.

  1. Add the following declaration to the CCityTax class inheritance list: 

    public IDispatchImpl<IPipelineComponentDescription, 
    

&IID_IPipelineComponentDescription, &LIBID_SIMPLETAXLib>,

**IDispatchImpl** provides a default implementation for the **IDispatch** portion of any dual interface on your object. 
  1. Add the following to the COM_MAP: 

    COM_INTERFACE_ENTRY(IPipelineComponentDescription)
    
Also, make the following changes in the COM\_MAP:

1.  Comment out COM\_INTERFACE\_ENTRY(IDispatch) 

2.  Add COM\_INTERFACE\_ENTRY2(IDispatch, ICityTax) 

These modifications are needed, because our object is now derived from two branches of **IDispatch**. Using the COM\_INTERFACE\_ENTRY2 macro allows us to disambiguate the interfaces. 
  1. Add the following method declarations to the CCityTax class: 

    // IPipelineComponentDescription
    

STDMETHOD(ContextValuesRead)(VARIANT *pVarRead); STDMETHOD(ValuesRead)(VARIANT *pVarRead); STDMETHOD(ValuesWritten)(VARIANT *pVarWritten);

  1. Give the new methods bodies in the CityTax.cpp file. The code would look like this: 

    //
    

// IPipelineComponentDescription Methods // STDMETHODIMP CCityTax::ContextValuesRead(VARIANT pVarRead) { // TODO: Add your own values to the array int cEntries = 1; // allocate the safearray of VARIANTs SAFEARRAY psa = SafeArrayCreateVector(VT_VARIANT, 0, cEntries); // Populate the safearray variants VARIANT* pvarT = (VARIANT*)psa->pvData; V_BSTR(pvarT) = SysAllocString(L"None"); V_VT(pvarT) = VT_BSTR; // set up the return value to point to the safearray V_VT(pVarRead) = VT_ARRAY | VT_VARIANT; V_ARRAY(pVarRead) = psa; return S_OK; }

STDMETHODIMP CCityTax::ValuesRead(VARIANT pVarRead) { // TODO: Add your own values to the array int cEntries = 1; // allocate the safearray of VARIANTs SAFEARRAY psa = SafeArrayCreateVector(VT_VARIANT, 0, cEntries); // Populate the safearray variants VARIANT* pvarT = (VARIANT*)psa->pvData; V_BSTR(pvarT) = SysAllocString(L"Test_Price"); V_VT(pvarT) = VT_BSTR; // set up the return value to point to the safearray V_VT(pVarRead) = VT_ARRAY | VT_VARIANT; V_ARRAY(pVarRead) = psa;

return S_OK; }

STDMETHODIMP CCityTax::ValuesWritten(VARIANT pVarWritten) { // TODO: Add your own values to the array int cEntries = 1; // allocate the safearray of VARIANTs SAFEARRAY psa = SafeArrayCreateVector(VT_VARIANT, 0, cEntries); // Populate the safearray variants VARIANT* pvarT = (VARIANT*)psa->pvData; V_BSTR(pvarT) = SysAllocString(L"Test_Tax"); V_VT(pvarT) = VT_BSTR; // set up the return value to point to the safearray V_VT(pVarWritten) = VT_ARRAY | VT_VARIANT; V_ARRAY(pVarWritten) = psa; return S_OK; }

  1. Compile the component. Now if you open your test Pipeline, or create one and add City Tax Class to it, you should be able to look at the properties of the component and see "Values Read and Written." 

  2. This concludes implementation of IPipelineComponentDescription

Implementing ISpecifyPropertyPages

The ISpecifyPropertyPages interface is a standard Microsoft Win32® OLE interface. Implement it to allow the pipeline administration tool to invoke the component's property page user interface. For more information about ISpecifyPropertyPages, see the OLE Programmer's Reference.

  1. Add the following declaration to the CCityTax class inheritance list: 

    public ISpecifyPropertyPages,
    
  1. Add the following to the COM_MAP: 

    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    
  1. Add the following method declarations to the CCityTax class: 

    // ISpecifyPropertyPages
    

STDMETHOD (GetPages)(CAUUID *pPages);

  1. Give the GetPages method a default implementation in the CityTax.cpp file. 

    if (NULL == pPages)
    

return E_INVALIDARG; // TODO: Uncomment out the line below, and add the CLSID of a custom Property Page Control /* pPages->cElems = 1; pPages->pElems = (GUID*)CoTaskMemAlloc(1*sizeof(GUID)); if(!pPages->pElems){ pPages->cElems = 0; return E_OUTOFMEMORY; } memcpy(pPages->pElems, &CLSID_CityTaxPpg, sizeof(GUID));

return S_OK; */ return E_NOTIMPL;

  1. Create a property page. Right click on the project in the Windows Explorer ClassView tab, select New ATL Object, choose Controls, and then select Property Page. Use CityTaxPPg for the short name, and click OK

  2. Go to the resource view of Windows Explorer, and open the newly created dialog box. Add a static control with the words "Tax Rate Percentage:" for the caption. 

  3. Add an Edit control named "IDC_Percentage." 

  4. Uncomment the body of the GetPages method. It should be correct. If you named your property page something different, you need to change the CLSID_CityTaxPpg to reflect the appropriate name. 

  5. Compile and test in a pipeline. You should get a property page with the static and edit controls that you placed on the dialog box. 

  6. Make the page communicate with the control. This includes creating a IPropertyPageImpl::Activate method and modifying the IPropertyPageImpl::Apply method. You need to implement the Activate method first. 

  7. Add "STDMETHOD(Activate)(HWND hWndParent, LPCRECT pRect, BOOL fModal);" to the CCityTaxPpg class declaration. 

  8. Create an empty body for the method in the CCityTaxPpg.cpp by adding the following: 

    STDMETHODIMP CCityTaxPpg::Activate(HWND hWndParent, LPCRECT pRect, BOOL fModal)
    

{

return S_OK; }

  1. Obtain a pointer to the component in the Activate method so that you can retrieve property values from it. Add the following code to the body of the Activate method: 

    USES_CONVERSION; //this is needed for BSTR conversion macros
    
    

VARIANT Percentage; VARIANT varbstrPercentage; float Percent;

HRESULT hRes = IPropertyPageImpl<CCityTaxPpg>::Activate(hWndParent, pRect, fModal); if(SUCCEEDED(hRes)) { ATLTRACE(L"Created CityTax Property Page object"); CComQIPtr<ICityTax, &IID_ICityTax>pCityTax(m_ppUnk[0]);

VariantInit(&Percentage); VariantInit(&varbstrPercentage);

pCityTax->get_Percent(&Percent);

V_R4(&Percentage) = Percent; V_VT(&Percentage) = VT_R4;

hRes = VariantChangeType(&varbstrPercentage, &Percentage, 0, VT_BSTR); ATLTRACE(varbstrPercentage.bstrVal );

if(FAILED(hRes)) return hRes;

SetDlgItemText (IDC_PERCENTAGE, W2A(varbstrPercentage.bstrVal));

VariantClear(&varbstrPercentage); VariantClear(&Percentage); } return S_OK;

  1. Implement the Apply method now that Activate is complete. In the property page .H file, find the default implementation of Apply—it should look like this: 

    STDMETHOD(Apply)(void)
    

{ ATLTRACE(_T("CCityTaxPpg::Apply\n")); for (UINT i = 0; i < m_nObjects; i++) { // Do something interesting here // ICircCtl* pCirc; // m_ppUnk[i]->QueryInterface(IID_ICircCtl, (void**)&pCirc); // pCirc->put_Caption(CComBSTR("something special")); // pCirc->Release(); } m_bDirty = FALSE; return S_OK; }

Replace this in the .H file with: 

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">STDMETHOD(Apply)(void);
  1. Add an implementation of the Apply method in the property page .CPP file. The implementation should look like the following: 

    STDMETHODIMP CCityTaxPpg::Apply (void)
    

{ ATLTRACE(_T("CCityTaxPpg::Apply\n")); CComQIPtr<ICityTax, &IID_ICityTax>pCityTax(m_ppUnk[0]);

CComBSTR bstrPercentage; VARIANT varBSTRPercentage, varPercentage; HRESULT hRes;

VariantInit(&varBSTRPercentage); VariantInit(&varPercentage);

GetDlgItemText(IDC_PERCENTAGE,bstrPercentage.m_str);

V_BSTR(&varBSTRPercentage) = SysAllocString(bstrPercentage.m_str); V_VT(&varBSTRPercentage) = VT_BSTR; hRes = VariantChangeType(&varPercentage,&varBSTRPercentage,0,VT_R4);

if(FAILED(hRes)) return hRes; pCityTax->put_Percent(varPercentage.fltVal );

VariantClear(&varBSTRPercentage); VariantClear(&varPercentage);

m_bDirty = FALSE; return S_OK; }

  1. Ensure that everything you need for your property page is in place. The property page should initialized with the default value and maintain state of the property value. The next phase of this project is to implement property value persistence. 
Implementing IPersistStreamInit

The IPersistStreamInit interface is a standard Win32 OLE interface. This interface is typically implemented on any object that needs to support initialized stream-based persistence, regardless of whatever else the object does. In the context of the order processing pipeline (OPP), it is used to load/save the component configuration in the pipeline configuration file (the pipeline will call on this interface, as appropriate). For information about IPersistStreamInit, see the OLE Programmer's Reference.

  1. Add IPersistStreamInit to the class inheritance list: 

    public IpersistStreamInit,
    
At this point, your class declaration should like similar to this: 

<pre IsFakePre="true" xmlns="https://www.w3.org/1999/xhtml">class ATL_NO_VTABLE CCityTax : 

public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CCityTax, &CLSID_CityTax>, public IPipelineComponent, public ISpecifyPropertyPages, public IPersistStreamInit, public IDispatchImpl<IPipelineComponentDescription, &IID_IPipelineComponentDescription, &LIBID_SIMPLETAXLib>, public IDispatchImpl<ICityTax, &IID_ICityTax, &LIBID_SIMPLETAXLib> {

  1. Add it to the COM_MAP now, as with the previous interfaces: 

    COM_INTERFACE_ENTRY(IPersistStreamInit)
    
  1. Add the method declarations to the CCityTax class together with two variables for stream versions: 

    // IPersistStreamInit
    

const long m_lStreamVersionMajor; // major version number of the stream const long m_lStreamVersionMinor; // minor version number of the stream STDMETHOD(GetClassID)(CLSID *pClassID); STDMETHOD(IsDirty)(void); STDMETHOD(Load)(IStream *pStm); STDMETHOD(Save)(IStream *pStm, BOOL fClearDirty); STDMETHOD(GetSizeMax)(ULARGE_INTEGER *pcbSize); STDMETHOD(InitNew)(void);

  1. Add empty implementations for the IPersistStreamInit methods to the CityTax.cpp file. The implementations may look like this: 

    STDMETHODIMP CCityTax::IsDirty(void)
    

{ return S_OK; }

STDMETHODIMP CCityTax::Load(IStream *pStm) { HRESULT hRes = S_OK; return hRes; }

STDMETHODIMP CCityTax::Save(IStream *pStm, BOOL fClearDiry) { HRESULT hRes = S_OK; return hRes; }

STDMETHODIMP CCityTax::GetSizeMax(ULARGE_INTEGER *pcbSize) { return S_OK; }

STDMETHODIMP CCityTax::InitNew(void) { return S_OK; }

STDMETHODIMP CCityTax::GetClassID(CLSID *pClassID) { *pClassID = GetObjectCLSID(); return S_OK; }

  1. Compile your code; it should compile without errors. If errors are found, take this opportunity to resolve and compile the errors before adding code to the IPersistStreamInit methods. 

  2. Create a local variable. Originally we used a float type to store our value. However, to make things easy in persisting our data, create a local variable of type CcomVariant, and assign it with our property value. CComVariant implements the WriteToStream method, allowing an easy method of persistence. The code that you need to place in the Save method would look like this: 

    CComVariant l_Percent;
    
    

l_Percent = m_Percent; hRes = l_Percent.WriteToStream (pStm);

  1. Do the same for loading the data. Hence, in the Load method, add: 

    CComVariant l_Percent;
    
    

l_Percent = 0; l_Percent.ReadFromStream (pStm);

m_Percent = l_Percent.fltVal;

  1. Implement the GetMaxSize method. Add the following: 

    CComVariant l_Percent;
    
    

l_Percent = m_Percent;

pcbSize->LowPart = sizeof(l_Percent); pcbSize->HighPart = 0;

  1. Go back to the Execute method, and modify the code to use the original method for calculation: 

    //Call the original method for processing
    

//m_Percent=.10; hr = CalculateTax(Price.fltVal , &Tax); //m_Percent * Price;

  1. Compile your code. At this point you should have a fully functional Commerce Pipeline component—the only caveat being that you will want to remove the ATL_MIN_CRT flag from your compile options. 

Adding MTS Support

In addition to exporting the mandatory and optional interfaces that the pipeline component is expected to have, we should support the Microsoft Transaction Server. With the release of SSCE 3.0, the MTSTxPipeline was introduced. This pipeline implementation is transactional. SSCE still supports the original non-transacted pipeline, but it runs inside MTS. Often it is desired, if not necessary, that custom pipeline components participate in distributed transactions. The following instructions cover what is needed to add MTS support to our pipeline component.

  1. Add the following code to the CityTax.h file: 

    #include "mtx.h"
    
  1. Declare a variable for the object context: 

    IObjectContext *Context = NULL;
    
  1. Get the executing object transaction context. Hence, add the following call to the top of the CCityTax::Execute method: 

    //Get MTS Context
    

hr=GetObjectContext(&Context);

  1. Handle the response that you receive after the call to GetObjectContext. The following is a skeletal switch statement you might implement immediately after the GetObjectContext call: 

    //Handle MTS respone
    

switch(hr) { case S_OK: break; case E_INVALIDARG: break; case E_UNEXPECTED: break; case CONTEXT_E_NOCONTEXT: break; }

  1. Ensure that the additions are complete. Call the SetAbort method if anything fails and the MTS ObjectContext is valid. Otherwise, if the ObjectContext is valid and nothing fails, call SetComplete. The following code is an example of what you might add to the end of the CCityTax::Execute method implementation: 

    if(SUCCEEDED(hr))
    

{ ErrorLevel = OPPERRORLEV_SUCCESS; if(Context != NULL){hr=Context->SetComplete ();} } else { if(Context != NULL){hr=Context->SetAbort ();} }

  1. Ensure that changes necessary for adding MTS support are complete. It is necessary to register the component in MTS. Do not register the part of the COM server that serves up the property pages. One last caveat—if you wish to create other objects inside the current transaction context, use the following call: 

    //MTS: How to create an object within this transaction context.
    

hr = Context->CreateInstance(CLSID_MyID, IID_IMyObjInterface, (void**)&pMyObj);