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.
Launch Visual Studio 2005
Open the CustomPipeline Project from our previous post
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
Add IPipelineComponentUI Interface to WinForm and implement it's interface
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 Add a new variable to the Form
bool OK = false; 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();} 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(); } } 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));} Repeat Step 4 and 5 with IPersistDictionary Interface for our Component (These Interfaces must be implemented in your component and not the form)
Repeat Step 4 and 5 with ISpecifyPipelineComponentUI Interface Interface for our Component
Repeat Step 4 and 5 with IPipelineComponentDescription Interface Interface for our Component
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 } } Compile the sample
Test the sample
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.
Launch the Pipeline Editor
From the File Menu select New
From the Choose a Pipeline Template select Empty.pct then select OK
Right click on Empty Pipeline Template and a new Stage
Right click on New Stage and insert your custom pipeline you just created
Launch Visual Studio 2005 and open your Custom Pipeline Project
From Visual Studio select the Debug menu then click Attach to Process...
From the Add to Process dialog find pipeeditor.exe process and attach to it
Put a break point in the ShowProperties method in your Form
Next from the Pipeline Editor right click on CustomPipeline.class1 component and select Properties...
From the Component Properties dialog navigate to Custom Properties tab and click Custom User Interface
Notice that the break point will be hit (if you had placed your break point before the show method of the form)
Now Continue and add a value to the text box
Close the form and save your pipeline
Open Pipeline Editor and navigate to your PCF file from step 14 and open it
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.
Comments
Anonymous
September 17, 2006
Max Akbar [MSFT] has recently finished a series of really good posts about the Commerce Server Pipelines system.Anonymous
September 19, 2006
Hi Max,
Thank you for your supporting, now I believe to increase the Performance of my Pipeline.
Unfortunally I still getting an error. How I said in previous post I'm developing a Custom Update inventory but I can't write in the BackOrderedQuantity or PreOrderedQuantity fields in the end of the process.
I know that Commerce, by defaut, call Stored procedures to do this. Do you if there any other way to update this fields with call SPs?
Thank you so far!Anonymous
September 19, 2006
I would higly recomend that you only use Commerce APIs for updating inventory values that said here is a sample code from the docs (you are going to have to modify this):
public static void UpdateSKUs(InventoryContext inventoryContext, string inventoryCatalogName)
{
// Get the inventory catalog.
InventoryCatalog inventoryCatalog = inventoryContext.GetInventoryCatalog(inventoryCatalogName);
// Update the Memo to Temp for all SKUs.
UpdateClause[] updateClause = {new UpdateClause (InventorySkusDataSetSchema.Memo, "Temp")};;
catalog.UpdateSkus(updateClause);
// Update specific SKUs based on an expression.
// Create the expressions for columns to be updated.
// The first expression adds 10 to the OnHandQuantity.
// The second epression updates the "Memo" column to "Temp1".
UpdateClause[] updateclauses = { new UpdateClause(InventorySkusDataSetSchema.OnHandQuantity, "OnHandQuantity+10",true),
new UpdateClause(InventorySkusDataSetSchema.Memo, "Temp1")};
// Apply the update clauses to the inventory SKUs with
// OnHandQuantity equal to 10.
catalog.UpdateSkus(updateclauses, "OnHandQuantity=10");
}
If you need more help go here to see more details on how to work with the Inventory API:
http://msdn.microsoft.com/library/en-us/CS07Default/html/136be237-1d94-404d-b0c8-b9ef378e9be4.asp
Good luck,
-MaxAnonymous
September 19, 2006
The comment has been removedAnonymous
September 19, 2006
The comment has been removedAnonymous
October 08, 2006
Max Akbar [MSFT] has recently finished a series of really good posts about the Commerce Server...Anonymous
November 08, 2006
The comment has been removedAnonymous
November 08, 2006
Yes sorry I wasn't clear on my post you have to implement all of the interfaces in order to get it to save. -MaxAnonymous
November 09, 2006
The comment has been removedAnonymous
November 11, 2006
As previously discussed the pipeline system enables sequential workflow processing. It is a COM-based...Anonymous
May 09, 2007
The comment has been removedAnonymous
December 05, 2007
The comment has been removedAnonymous
August 12, 2008
We have developed a Commerce Server Website with out using Pipelines.Now we want to implement pipelines in our project.I have read the above document , but still i am getting the way how to start implementing Pipelines. Please guide me. Thank You..... Tushar Narayan...Anonymous
August 12, 2008
We have developed a Commerce Server Website with out using Pipelines.Now we want to implement pipelines in our project.I have read the above document , but still i am not getting the way how to start implementing Pipelines. Please guide me. Thank You.....Anonymous
August 18, 2008
If are having problems understanding the blog posts then I am not sure what else would help you. It would help if you had specific questions. Also post your questions on the Commerce Server Forum. http://forums.microsoft.com/msdn/default.aspx?forumgroupid=294&SiteID=1 -MaxAnonymous
June 13, 2009
PingBack from http://wheelbarrowstyle.info/story.php?id=1854