Configuring WF Activity Properties by Morphing Activity Designers: Improved end-user (ITPro) experience in Workflow Designer

There are quite a few configuration steps to be completed on the Workflow Activity after it is dragged onto the workflow design surface. The complexity and the number of data entry points for configuring these Activities could lead to errors and poor user experience. 

A very common ask from the developers and ITPro users in the ISV community is to simplify the configuration effort for deploying ‘repeatable’ and ‘often used’ Workflow Activities. As an example, the Receive Activity requires quite a few properties (OperationName, ServiceContractName, CanCreateInstance and Content) to be configured before the solution is used. In this example, the Receive Activity property OperationName is the only ‘unique’ property. All of the other properties, (ServiceContractName. etc. ) arederived from the OperationName. A typical ISV Application can have 100’s of WCF/Workflow Services and each of these Services usually has several instances of use of the Receive Activity within the Service; offering immense opportunity to simplify the configuration experience.

The Solution

While there are many approaches to streamline the process, one approach that has excited the ISV community is creating a Morphing Custom Activity that at design-time is pre-configured with properties. An Activity Designer is applied to an Activity that either changes the properties of the Activity or replaces the Activity completely at design time, to create the Morphed Activity. The Morphed Activity can be designed appropriately to reduce the configuration steps and make the Workflow Designer experience simpler and error free.

This Blog post provides the WorkflowDesignerExtensibility solution that contains updates to both the ActivityLibrary (provides a new custom activity that shows how to use activity delegates) and ActivityLibrary.Design (provides a new activity designer that shows how recreate activity trees at design time) projects. It also includes a new project called DemoService that defines Service1.xamlx which can be used to test the LaunchOperation Activity.

LaunchOperation Activity

In this Activity Project, a new custom activity called LaunchOperation has been added that makes use of the InvokeFunc activity to “poke a hole” in the Activity design in which a Workflow developer can drop custom activities; e.g., Receive Activity. For this Activity, the OperationName is specified by the user, while all other configuration properties are specified by the Activity Designer. There are two classes of Delegate Activity that can be plugged in: those that only take zero or more input parameters with no return value; and those that take zero or more input parameters as well as have a return value. These map to ActivityAction and ActivityFunc respectively.

Figure 1 - PreConfigActivities

In this example, we want to create a “hole” with an enforced contract for an activity that will take no input parameters and return a string result. To do this we will create an ActivityFunc<string> at design time and drop it into the “hole” that is exposed by the body argument (of type ActivityFunc<string> as shown above).

Figure 2 - PreConfigActivities

The InvokeFunction<string> Activity, when it executes, will run what was plugged in to its Func property and write the output into OutArgument called Result (as shown above). In this case, our Func property is empty because the Activity Designer will fill it in when the user sets the OperationName on the LaunchOperation activity, but the Result is pre-configured to write its value into the Value output argument defined on LaunchOperation (see the Arguments screenshot), and thus will be accessible to Workflows hosting the LaunchOperation activity.

In our scenario, we have established that the Receive Activity, which will be contained within the ActivityFunc<string>, always receives a string input parameter, and we want to surface that string as an OutArgument on the LaunchOperation activity so that Workflows that use the LaunchOperation activity can get the string value passed as input to the operation; this is the Value OutArgument shown above.

LaunchOperationDesigner

Figure 3 - PreConfigActivities

The LaunchOperation Activity Designer (associated with the LaunchOperation activity via the Register Method in the Metadata.cs file for ActivityLibrary.Design) works as follows: whenever a user changes the value of the OperationName text box and tabs out (such that the text box loses focus), a new Receive Activity is created under the covers with an OperationName as specified in the textbox. This receive is added to ActivityFunc<string> defining the LaunchOperation activity’s body. This effect can best be visualized by looking at the XAML for a Workflow containing a LaunchOperation Activity

    1: <WorkflowService …>
    2:   <p1:Sequence DisplayName="Sequential Service" sad:XamlDebuggerXmlReader.FileName="…Service1.xamlx" sap:VirtualizedContainerService.HintSize="298,200">
    3:     <p1:Sequence.Variables>
    4:       <p1:Variable x:TypeArguments="CorrelationHandle" Name="handle" />
    5:       <p1:Variable x:TypeArguments="x:String" Name="data" />
    6:     </p1:Sequence.Variables>
    7:     <sap:WorkflowViewStateService.ViewState>
    8:       <scg3:Dictionary x:TypeArguments="x:String, x:Object">
    9:         <x:Boolean x:Key="IsExpanded">True</x:Boolean>
   10:       </scg3:Dictionary>
   11:     </sap:WorkflowViewStateService.ViewState>
   12:     <a:LaunchOperation Handle="{x:Null}" Value="{x:Null}" DisplayName="New Launch Operation" sap:VirtualizedContainerService.HintSize="276,72" OperationName="DoWork2" mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces">
   13:       <a:LaunchOperation.Body>
   14:         <p1:ActivityFunc x:TypeArguments="x:String" Result="{x:Reference __ReferenceID0}">
   15:           <p1:Sequence>
   16:             <Receive CanCreateInstance="True" OperationName="DoWork2" ServiceContractName="p:IService">
   17:               <Receive.CorrelatesOn>
   18:                 <MessageQuerySet />
   19:               </Receive.CorrelatesOn>
   20:               <ReceiveMessageContent DeclaredMessageType="x:String">
   21:                 <p1:OutArgument x:TypeArguments="x:String">
   22:                   <p1:DelegateArgumentReference x:TypeArguments="x:String">
   23:                     <p1:DelegateOutArgument x:TypeArguments="x:String" x:Name="__ReferenceID0" />
   24:                   </p1:DelegateArgumentReference>
   25:                 </p1:OutArgument>
   26:               </ReceiveMessageContent>
   27:             </Receive>
   28:           </p1:Sequence>
   29:         </p1:ActivityFunc>
   30:       </a:LaunchOperation.Body>
   31:     </a:LaunchOperation>
   32:   </p1:Sequence>
   33: </WorkflowService>

The details of the Activity Designer are indexed as TODO’s (use Visual Studio’s task list to view) and described with inline comments.

Figure 4 - PreConfigActivities

The logic which accomplishes this is primarily defined in LaunchOperation.Designer.xaml.cs:

    1: public partial class LaunchOperationDesigner
    2: {
    3:     public LaunchOperationDesigner() 
    4:     {
    5:         InitializeComponent();
    6:     }
    7:  
    8:     private void UpdateOperationName(ModelItem originalLaunchOpModelItem, string operationName)
    9:     {
   10:         this.Dispatcher.BeginInvoke(DispatcherPriority.SystemIdle, (Action)(() =>
   11:         {
   12:             //Create a new reference activity with the desired structure, then copy over its properties to the original using morphing.
   13:             DelegateOutArgument<string> result = new DelegateOutArgument<string>();
   14:  
   15:             Receive receive = new Receive()
   16:             {
   17:                 OperationName = operationName,
   18:                 ServiceContractName = System.Xml.Linq.XNamespace.Get("https://sample.org/") + "IService",
   19:                 CanCreateInstance = true,
   20:                 CorrelatesOn = new System.ServiceModel.MessageQuerySet(),
   21:                 Content = ReceiveContent.Create(new OutArgument<string>(result), typeof(string))
   22:             };
   23:  
   24:             LaunchOperation act = new LaunchOperation()
   25:             {
   26:                 OperationName = operationName,
   27:                 Body = new ActivityFunc<string>()
   28:                 {
   29:                     Result = result,
   30:                     Handler = new Sequence
   31:                     {
   32:                         Activities =
   33:                                 {
   34:                                     receive
   35:                                 }
   36:                     }
   37:                 }
   38:             };
   39:  
   40:             ModelItem newLaunchOpModelItem = System.Activities.Presentation.Model.ModelFactory.CreateItem(this.Context, act);
   41:  
   42:             try
   43:             {
   44:                 MorphHelper.MorphProperties(newLaunchOpModelItem, originalLaunchOpModelItem);
   45:             }
   46:             catch (Exception ex)
   47:             {
   48:                 MessageBox.Show(ex.Message);
   49:             }
   50:         }
   51:         ));
   52:     }
   53:  
   54:     private void txtOperationName_LostFocus(object sender, RoutedEventArgs e)
   55:     {
   56:         UpdateOperationName(this.ModelItem, txtOperationName.Text);
   57:     }
   58: }

Using the Sample

Compile the solution and open up Service1.xamlx. Drop a new LaunchOperation on the sequence and change the OperationName. Run the DemoService project and either examine the WSDL or Call the Service using the WCF Test Client to verify that the operation name changes are as expected.

The source code around this sample is available for download from the Skydrive/Live site.

https://cid-a6f708b62435dea1.skydrive.live.com/browse.aspx/Sample%20Code?authkey=WopZByaPBUg%24