Everything you ever wanted to know about pipelines but were afraid to ask (Part III)

So far you should be comfortable working with pipeline files creating\modifying components and having a good understanding of basic concepts of creating a custom pipeline components and as promised lets dive deeper into advanced understanding of the pipeline interfaces.
Then we will dive into the Order System and learn about the heart of pipelines OrderForms.

Pipeline Interfaces

In order to create pipeline components you need to implement one or more interfaces. You must always implement the IPipelineComponent Interface, without this Interface the pipeline component will not execute. All other Interfaces and exposes other functionality to fit into the Commerce Pipeline architecture.

IPipelineComponent Interface

As have been noted this Interface must be implemented in order for the pipeline object to execute the component.

The IPipelineComponent interface supports the following methods:

  • Execute
    The entry point at which the pipeline begins executing the component.
    The following is the method signature for the Execute Method:

    int Execute(object pdispOrder, object pdispContext, int lFlags);

    The Execute method contains three parameters:
    pdispOrder - A pointer to the OrderForm object.
    pdispContext - A pointer to the Context object. This is a Dictionary object that contains pointers to the MessageManager object, Content object, and language information. Values can be passed through this parameter using the PipelineInfo object.
    lFlags - Reserved. This parameter should be zero (0).

    The Execute method also returns an integer parameter. A flag that indicates what level of errors is tolerated.
    1 Success. No errors, component executed successfully.
    2 Warning. Basket or purchase errors for this operation.
    3 Failure. Serious errors in execution of this component.

    The following sample shows how to pass Context values to the Pipeline object.

    Basket basket = CommerceContext.Current.OrderSystem.GetBasket(userID);PipelineInfo pipeinfo = new PipelineInfo("Basket");pipeinfo["OrderContext"] = CommerceContext.Current.OrderSystem;basket.RunPipeline(pipeinfo);

    The following code shows how to retrieve the Context passed into the Pipeline.

    using System;using System.Collections.Generic;using System.Text;using System.Runtime.InteropServices;using Microsoft.CommerceServer.Interop;using Microsoft.CommerceServer.Runtime;using Microsoft.CommerceServer.Interop.Orders;using Microsoft.CommerceServer.Runtime.Orders;namespace CustomPipeline{[ComVisible(true)][GuidAttribute ("5904C354-F1B8-485c-89DD-883D26BCE85D")]public class Class1 : IPipelineComponent { // Status codes for pipeline components private const Int32 StatusSuccess = 1; // success private const Int32 StatusWarning = 2; // warning private const Int32 StatusError = 3; // error #region IPipelineComponent Members public void EnableDesign(int fEnable){} public int Execute(object pdispOrder, object pdispContext, int lFlags) { Int32 ReturnValue = StatusSuccess; // How to get our Custom OrderContext IDictionary Context = (IDictionary)pdispContext; OrderContext myOrderContext = (OrderContext)Context["OrderContext"]; // How to get MessageManager Context IMessageManager MessageManager = (IMessageManager)Context["MessageManager"]; return ReturnValue; } #endregion }}}

    A best practice is always put a try\catch in Execute Method so when exceptions do occur you can set the correct return value, see the following sample (you may also want to log the exception):

    public Int32 Execute(Object pdispOrder, Object pdispContext, Int32 Flags) { Int32 ReturnValue = StatusSuccess; // Status level for component execution try { // perform some logic // you also can set a warning // ReturnValue = StatusWarning; } // an error has occured. Add the error message to the basket errors and // set the "error" as the pipeline component error level catch (Exception e) { ((ISimpleList)Order["_Basket_Errors"]).Add(ref e.Message); ReturnValue = StatusError; } return ReturnValue; }
  • EnableDesign
    Sets the design mode on the component. Use this method to prepare the component for execution in one of two modes: design mode or execution mode. In design mode, which is useful for running the Pipeline Editor, errors are more easily tolerated. Execution mode (the default) is analogous to production mode. Although you have to Implement the EnableDesign method you can leave it empty.

    void EnableDesign([In] int fEnable);

IPipelineComponentAdmin Interface

The IPipelineComponenetAdmin Interface allows you to save configuration values at design time and they are read at runtime to perform some action based on your logic. For example in the MinMaxShipping sample a UI is presented when editing the component in Pipeline Editor and you set thresholds of minimum shipping cost. Once this value is set during the design time and when the pipeline executes, this value can be used to make sure we don't fall below the shipping cost. In order to show the properties for MinMaxShipping the sample uses a WinForm that implements IPipelineComponentUI Interface, this is an alternative to using the ISpecifyPropertyPages Interface. After going over some concepts of the other Interfaces I will show you an example of how these Interfaces work together.

The IPipelineComponenetAdmin Interface supports the following methods:

  • GetConfigData
    Returns a Dictionary object that contains the configuration settings for a component.
    The following is the method signature for the GetConfigData Method:

    object GetConfigData();

    The GetConfigData method returns an object parameter. The object returned is actually an IDictionary pointer for the user interface to read the configuration data for the component.

  • SetConfigData
    Sets the configuration settings on a component using the contents of a Dictionary object.
    The following is the method signature for the SetConfigData.

    void SetConfigData(object pDict);

    The SetConfigData method contains one parameter:
    pDict - An interface pointer to the object containing the configuration information.

IPipelineComponentUI Interface

The IPipelineComponentUI Interface is implemented to allow a pipeline component to display a dialog box for configuration values. The interface is implemented by a separate Component Object Model (COM) object that displays the dialog box in .NET this can be a WinForm. In order to display the WinForm you must also implement the ISpecifyPipelineComponentUI. The ISpecifyPipelineComponentUI specifies the WinForm to launch which executes the ShowProperties method.

The IPipelineComponentUI interface supports the following method:

ShowProperties
Called by the Pipeline Editor to display the dialog box for the component.
The following is the method signature for the ShowProperties:

void ShowProperties(object pdispComponent);

The ShowProperties method contains one parameter:
pdispComponent - A pointer to an IPipelineComponentAdmin interface.

IPersistDictionary Interface

The IPersistDictionary Interface is implemented by an object that needs to read or write its data as a Dictionary object. When creating a UI for your pipeline may have need to save it's data if so then use the IPersistDictionary Interface. The configuration data is saved within the Pipeline Configuration File.

The IPersistDictionary Interface supports the following methods.

  • GetProgID
    The following is the method signature for the GetProgID.

    object GetProgID();

    Returns the ProgID of the object.

  • InitNew
    Allows the object to initialize values before saved values are loaded.
    The following is the method signature for the InitNew.

    void InitNew(object pDict);
  • IsDirty
    Indicates whether the values to be saved have changed since the last save.
    The following is the method signature for the IsDirty.

    void IsDirty(object pDict);
  • Load
    Loads saved data from a Dictionary object.
    The following is the method signature for the Load.

    void Load(object pDict);
  • Save
    Saves data to a Dictionary object.
    The following is the method signature for the Save.

    void Save(object pDict);

IPipelineComponentDescription Interface

The IPipelineComponentDescription Interface makes it possible for pipeline components to identify the values that they read from the pipeline context, and the keys and values that they read from or write to the OrderForm object.

Although pipeline components are not required to implement the IPipelineComponentDescription Interface, implementing this interface makes it possible for the Pipeline Editor to identify and display the elements that your component reads or writes. This information can help developers who are troubleshooting a pipeline configuration.

The IPipelineComponentDescription interface supports the following methods.

  • ContextValuesRead
    Returns a SAFEARRAY VARIANT that identifies the pipeline context values that the component reads.

    object ContextValuesRead();
  • ValuesRead
    Returns a SAFEARRAY VARIANT that identifies the values that the component reads.

    object ValuesRead();
  • ValuesWritten
    Returns a SAFEARRAY VARIANT that identifies the values that the component writes.

    object ValuesWritten();

    An Example of how to write SAFEARRAY VARIANT in .NET to work with the Pipeline Component:

    object ValuesWritten(){ object[] valuesWritten= new object[3]; valuesWritten[0] = "myValue_1"; valuesWritten[1] = "item.myValue_2"; valuesWritten[2] = "_myValue_3"; return valuesWritten;}

ISpecifyPipelineComponentUI Interface

The ISpecifyPipelineComponentUI Interface is used by a pipeline component to specify the Component Object Model (COM) object that implements the IPipelineComponentUI interface that gets and sets configuration values.

The ISpecifyPipelineComponentUI Interface supports the following method.

  • GetPipelineComponentUIProgID
    Used by the pipeline component to specify the component that displays the configuration dialog box by implementing the IPipelineComponentUI Interface.

    object GetPipelineComponentUIProgID();

    Here is an example of the above method:

    public string GetPipelineComponentUIProgID() { return "CustomPipeline.Form1"; }

ISpecifyPropertyPages Interface

The ISpecifyPropertyPages Interface is a standard Win32 OLE interface. You implement this interface to allow the pipeline administration tool to invoke the property page user interface of the component. For more information about the ISpecifyPropertyPages Interface, see the OLE Programmer's Reference.

The ISpecifyPropertyPages Interface supports the following method:

  • GetPages
    Fills in an array of class identifiers (CLSID) where each CLSID is the CLSID of a property page that can be displayed in the property sheet for this object.

Now let's put it all together with an example:

We are going to work on the sample component created for the last post.

  1. Launch Visual Studio 2005

  2. Open the CustomPipeline Project from our previous post

  3. Add a WinForm

    • Right Click on CustomPipeline Project and add a new Form

    • Name your Form something descriptive.

    • Add two buttons, a Lable and a TextBox to the Form
      This form will be used to store custom data during the design time and extract during the runtime for processing.

    • Set Form properties:
      StartPosition=CenterScreen
      FormBorderStyle=FixedDialog
      MaximizeBox=False
      MinimizeBox=False

  4. Add IPipelineComponentUI Interface to WinForm and implement it's interface

  5. Implement the IPipelineComponentUI Interface

    By selecting the Implement interface 'IPipelineComponentUI' you will get the following code:

    #region IPipelineComponentUI Members public object ShowProperties(object pdispComponent)() { throw new Exception("The method or operation is not implemented."); }#endregion
  6. Add a new variable to the Form

    bool OK = false;
  7. Add details to OK and Cancel Button's Click Event
    When Clicking the OK Button we want to save the configuration else do not save the Sample Data.

    private void button2_Click(object sender, EventArgs e){ SamlpleData = this.textBox1.Text; this.OK = false; this.Hide();}private void button1_Click(object sender, EventArgs e){ this.OK = true; this.Hide();}
  8. Now we need to add a property to the form.
    We need to get values out and in from the WinForm when values are entered.

    public string SamlpleData { get { return Convert.ToString(textBox1.Text); } set { textBox1.Text = value.ToString(); } }
  9. Add implementation details for IPipelineComponentUI's ShowProperties method:
    In order to retrieve and save values during design time we need to add code to the ShowProperties method.

    try{ // Use IPipelineComponentAdmin Interface to get configuration information IPipelineComponentAdmin pdispPCA = (IPipelineComponentAdmin)pdispComponent; // Populate a dictionary with component configuration Microsoft.CommerceServer.Runtime.IDictionary dictConfig = (Microsoft.CommerceServer.Runtime.IDictionary)pdispPCA.GetConfigData(); Form1 frmPipeCompUIObj = new Form1(); // Get the sample data frmPipeCompUIObj.SamlpleData = (String)dictConfig["SampleData"]; // Display the properties for the user to editbr frmPipeCompUIObj.ShowDialog(); if(frmPipeCompUIObj.Ok) { // Save entered values into the dictionary dictConfig["SampleData"] = frmPipeCompUIObj.SamlpleData; // Update the component with new configuration information pdispPCA.SetConfigData(dictConfig); }}catch(Exception e){ throw(new Exception(e.Message));}
  10. Repeat Step 4 and 5 with IPersistDictionary Interface for our Component (These Interfaces must be implemented in your component and not the form)

  11. Repeat Step 4 and 5 with ISpecifyPipelineComponentUI Interface Interface for our Component

  12. Repeat Step 4 and 5 with IPipelineComponentDescription Interface Interface for our Component

  13. Now we need to add details to IPipelineComponentUI Interface

    After implementing all of the methods your component should look like the code below:

    using System; using System.Collections.Generic; using System.Text; using System.Runtime.InteropServices; using Microsoft.CommerceServer.Interop; using Microsoft.CommerceServer.Interop.Orders; using Microsoft.CommerceServer.Runtime; using Microsoft.CommerceServer.Runtime.Orders; namespace CustomPipeline { [ComVisible(true)] [GuidAttribute ("5904C354-F1B8-485c-89DD-883D26BCE85D")] public class Class1 : IPipelineComponent, IPipelineComponentAdmin, IPersistDictionary, ISpecifyPipelineComponentUI, IPipelineComponentDescription { // Status codes for pipeline components private const Int32 StatusSuccess = 1; // success private const Int32 StatusWarning = 2; // warning private const Int32 StatusError = 3; // error // Sample Data string sampleData = null; bool isDirty = false; public Class1() { isDirty = false; this.InitNew(); } #region IPipelineComponent Members public void EnableDesign(int fEnable) { } public int Execute(object pdispOrder, object pdispContext, int lFlags) { Int32 ReturnValue = StatusSuccess; IDictionary Context = (IDictionary)pdispContext; OrderContext myOrderContext = (OrderContext)Context["OrderContext"]; return ReturnValue; } #endregion #region IPipelineComponentAdmin Members public object GetConfigData() { IDictionary Dictionary = new Dictionary(); this.Save(Dictionary, Convert.ToInt32(false)); return Dictionary; } public void SetConfigData(object pDict) { IDictionary Dictionary = (IDictionary)pDict; this.Load(Dictionary); } #endregion #region IPersistDictionary Members public string GetProgID() { return "CustomPipeline.Class1"; } public void InitNew() { sampleData = ""; } public int IsDirty() { return Convert.ToInt32(isDirty); } public void Load(object pdispDict) { IDictionary Dictionary = (IDictionary)pdispDict; //If the expected dictionary values do not exist use the defaults if (Dictionary["SampleData"] != null && Dictionary["SampleData"] != DBNull.Value) sampleData = Convert.ToString(Dictionary["SampleData"]); } public void Save(object pdispDict, int fSameAsLoad) { IDictionary Dictionary = (IDictionary)pdispDict; Dictionary["SampleData"] = sampleData; } #endregion #region ISpecifyPipelineComponentUI Members public string GetPipelineComponentUIProgID() { return "CustomPipeline.Form1"; } #endregion #region IPipelineComponentDescription Members public object ContextValuesRead() { object[] contextValuesRead = new object[0]; return contextValuesRead; } public object ValuesRead() { object[] valuesRead = new object[0]; return valuesRead; } public object ValuesWritten() { object[] valuesWritten = new object[0]; return valuesWritten; } #endregion } }
  14. Compile the sample

  15. Test the sample

  16. Congratulations you have successfully implemented a UI to your component. Notice that when you save the value in the form and open the form it remembers the value.

Debugging your .NET Form in Pipeline Editor

I know I said that I would go over the Pipeline Debugging later on but this is the right place to go over how to debug your .NET Form inside the Pipeline Editor.

  1. Launch the Pipeline Editor

  2. From the File Menu select New

  3. From the Choose a Pipeline Template select Empty.pct then select OK

  4. Right click on Empty Pipeline Template and a new Stage

  5. Right click on New Stage and insert your custom pipeline you just created

  6. Launch Visual Studio 2005 and open your Custom Pipeline Project

  7. From Visual Studio select the Debug menu then click Attach to Process...

  8. From the Add to Process dialog find pipeeditor.exe process and attach to it

  9. Put a break point in the ShowProperties method in your Form

  10. Next from the Pipeline Editor right click on CustomPipeline.class1 component and select Properties...

  11. From the Component Properties dialog navigate to Custom Properties tab and click Custom User Interface

  12. Notice that the break point will be hit (if you had placed your break point before the show method of the form)

  13. Now Continue and add a value to the text box

  14. Close the form and save your pipeline

  15. Open Pipeline Editor and navigate to your PCF file from step 14 and open it

  16. Open your form and noticed that the value you had save previously is now displayed

    A bit of explanation of the PCF file. The PCF file is a OLE Compound file which contain it's own file structure inside the file. The OLE Compound file has a root that is returned by OpenStorageFile method. The root may contain sub-storages or streams. I have a sample code on GotDotNET for Commerce Server called Pipeline Modifier. The methods used in the Pipeline Modifier is undocumented but if you search hard enough you can find information on it from Site Server. The Pipeline Modifier allows you to view the raw data of the PCF file and modified it on the fly. Since the Pipeline Editor does not allow you to use command lines to modify the PCF files I created this tool to help in the process of automating the daunting task of managing Pipelines manually. If you download this tool you will need to modify a few things. For example since Commerce Server 2007 has an interop for the Pipeline Interfaces you will need to reference this instead of PipeCompLib. Also you may encounter MDA warning which you can ignore. Now lets get back on track and learn about OrderForms.

OrderForms

An OrderForm represents a collection of items that a user placed in a shopping cart. Depending on how far the user has progressed through the purchasing process, an OrderForm might also contain the locations to ship the products to, the discounts that apply to the OrderGroup, and the ways that the user paid or will pay for the products.
There are two types of OrderForms a .NET OrderForm that is used during runtime and a COM OrderForm that is used in the Pipelines. The Commerce Server 2007 has good documentation of how to extend the Order System and there is even an example under the SDK folder OrdersExtensibility.

Orders Pipeline Adapters

So what happens when you create a .NET OrderForm then execute the basket Pipeline (or any other Pipeline)? Simply put the managed OrderForm is run through the Pipeline Adapter and mapped into COM OrderForm then the basket Pipeline is executed.

The Orders Pipeline Adapter marshals data between the managed-code object model and legacy Dictionary instances in your Orders System. The Pipeline Adapter uses mapping contained in an XML file to map data between the managed code object and the legacy object. Dictionary class instances are simply hierarchical name-value pair collections. Managed classes can be viewed the same way, with strongly-typed properties as the name-value pairing. A mapping between the two models is achieved with an XML format that specifies the managed classes and properties to map, and the target Dictionary and SimpleList layout for those instances in unmanaged code.

The default mapping file for the Orders System is called OrderPipelineMappings.xml. The default directory for OrderPipelineMappings.xml is %SystemDrive%\Inetpub\wwwroot\<ApplicationName>.

Summary

So you learned more about Pipeline Interfaces and should have an advanced understanding of them and how to use them as well as debugging the WinForm UIs created.

You also learned of the Pipeline Modifier tool to help you with automating the administration task of your PCF files.

You now know how the managed OrderForm is mapped to COM OrdrForm and for more reading on this I would suggest that you work with the sample OrdersExtensibility.

In the next on going post I will discuss how to debug your pipeline components as well as testing your components.