Icon support for custom Windows Workflow activities

Creating a custom dependency resolution activity for WF gave me the opportunity to learn a lot about the latest version of Workflow Foundation. Adding support for custom icons is a simple requirement that often came up when creating new activities.

There are two places that provide this icon support. The first is in the [VS] toolbox and the second is on the activity design surface. I have been using images from the awesome famfamfam Silk collection for these activities.

[VS] toolbox

Adding an icon to the toolbox is done using the ToolboxBitmapAttribute on the activity class.

[ToolboxBitmap(typeof(ExecuteBookmark), "book_open.png")]
public sealed class ExecuteBookmark : NativeActivity

This attribute needs to identify the image file and a type reference that the IDE uses to locate the image. In the above scenario, the book_open.png file is the image to use for the toolbox. The image file in the project needs to have its Build Action property set to Embedded Resource.

image

The IDE uses the type reference to determine the namespace used as the prefix of the image name in the assembly resources list.

image

The IDE can then extract this image and use it in the toolbox.

image

Workflow designer

The workflow designer support for custom icons has a similar layout as the toolbox icon. The image file for the designer should be co-located with the designer xaml file. While this file should be the same image as the toolbox image, the file must be a different file reference in the [VS] solution/project. Adding the image file as a link to the other file should work if you want to have only one physical file for the image.

The reason for the separate file is that the Build Action for the designer image must be set to Resource instead of Embedded Resource that the ToolboxBitmapAttribute requires. Using two files for this purpose should not be a big issue as the designers are typically located in a separate *.Design.dll assembly (see here for the details).

image

The XAML in the designer for the activity then references this image file.

<sap:ActivityDesigner x:Class="Neovolve.Toolkit.Workflow.Design.Presentation.ExecuteBookmarkDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation">
  <sap:ActivityDesigner.Icon>
    <DrawingBrush>
      <DrawingBrush.Drawing>
        <ImageDrawing>
          <ImageDrawing.Rect>
            <Rect Location="0,0" Size="16,16" ></Rect>
          </ImageDrawing.Rect>
          <ImageDrawing.ImageSource>
            <BitmapImage UriSource="book_open.png" ></BitmapImage>
          </ImageDrawing.ImageSource>
        </ImageDrawing>
      </DrawingBrush.Drawing>
    </DrawingBrush>
  </sap:ActivityDesigner.Icon>
</sap:ActivityDesigner>

The designer should then be associated with the activity. This is done using a class that implements IRegisterMetadata so that there can be a separation of the activity assembly and the design assembly. Again, see this post for the details.

image

The designer will look at the design assembly and extract the image from the resource list. This image will then be used on the design surface.

image

The only other thing to keep in mind is that the design assembly must be co-located with the activity assembly in order to leverage the design-time experience provided by the custom activity designers.

Custom Windows Workflow activity for dependency resolution–Wrap up

I have been writing a series of posts recently about implementing a custom WF activity that will provide dependency resolution support for WF4 workflows.

image

The InstanceResolver activity caters for lazy loading dependencies, multiple resolutions on the one activity, workflow persistence (including support for non-serializable dependencies) and lifetime management of the resolved dependencies. The activity uses Unity as the backend IoC container however this could be modified to support a different container with minimal changes.

The following is a list of all the posts in this series.

This workflow activity and several others can be found in my Neovolve.Toolkit project which can be found here on Codeplex.

Neovolve.Toolkit 1.0 RTW

I have finally marked my Neovolve.Toolkit project as stable for version 1.0. It includes the recent work I have done for WF4. The toolkit comes with the binaries, a chm help file for documentation information and xml comment files for intellisense in [VS].

You can download the toolkit from the project on Codeplex.

The following tables outline the types available in the namespaces across the toolkit assemblies. The information here is copied from the compiled help file.

Neovolve.Toolkit.dll

Neovolve.Toolkit.Communication

Name Description
ChannelProxyHandler<T> The ChannelProxyHandler<T> class is used to provide a proxy implementation for a WCF service channel.
DefaultProxyHandler<T> The DefaultProxyHandler<T> class is used to provide a default handler for invoking methods on a type.
ErrorHandlerAttribute The ErrorHandlerAttribute class is used to decorate WCF service implementations with a IServiceBehavior that identifies IErrorHandler references to invoke when the service encounters errors.
ProxyHandler<T> The ProxyHandler<T> class is used to provide the base logic for managing the execution of methods on a proxy.
ProxyManager<T> The ProxyManager<T> class is used to manage invocations of proxy objects.

Neovolve.Toolkit.Communication.Security

Name Description
DefaultPasswordValidator The DefaultPasswordValidator class provides a user name password validation implementation that ensures that a user name and password value have been supplied.
OptionalPasswordValidator The OptionalPasswordValidator class provides a user name password validation implementation that ensures that a user name value has been supplied.
PasswordIdentity The PasswordIdentity class provides an IIdentity that exposes the password related to the username.
PasswordPrincipal The PasswordPrincipal class provides information about the roles available to the PasswordIdentity that it exposes.
PasswordServiceCredentials The PasswordServiceCredentials class provides a username password security implementation for WCF services. It will generate a PasswordPrincipal containing a PasswordIdentity that exposes the password of the client credentials.

Neovolve.Toolkit.Instrumentation

Name Description
ActivityTrace The ActivityTrace class is used to trace related sets of activities in applications.
ConfigurationResolver The ConfigurationResolver class is used to resolve a collection of TraceSource instances from application configuration.
MemberTrace The MemberTrace class is used to provide activity tracing functionality for methods that declare them.
RecordTrace The RecordTrace class is used to trace record information.
TraceSourceLoadException The TraceSourceLoadException class is used to identify scenarios where a TraceSource is not retrieved for use by a RecordTrace instance.
TraceSourceResolverFactory The TraceSourceResolverFactory class is used to create an instance of a ITraceSourceResolver.
IActivityWriter The IActivityWriter interface is used to define how instrumentation records are written.
IRecordWriter The IRecordWriter interface defines the methods for writing instrumentation records.
ITraceSourceResolver The ITraceSourceResolver interface is used to resolve a collection of TraceSource instances.
ActivityTraceState The ActivityTraceState enum is used to define the state of a ActivityTrace instance.
RecordType The RecordType enum is used to define the type of record created.

Neovolve.Toolkit.Reflection

Name Description
MethodResolver The MethodResolver class resolves MethodInfo instances of types and caches results for faster access.
TypeResolver The TypeResolver class is used to resolve types from configuration mapping information.

Neovolve.Toolkit.Storage

Name Description
AbsoluteExpirationPolicy The AbsoluteExpirationPolicy class is used to define an absolute time when a cache item is to expire.
AspNetCacheStore The AspNetCacheStore class is used to provide a ICacheStore implementation that leverages a Cache instance.
CacheStoreFactory The CacheStoreFactory class is used to create ICacheStore instances.
ConfigurationManagerStore The ConfigurationManagerStore class is used to provide a IConfigurationStore implementation based on the ConfigurationManager class.
ConfigurationStoreFactory The ConfigurationStoreFactory class is used to create IConfigurationStore instances.
DictionaryCacheStore The DictionaryCacheStore class is used to provide a ICacheStore implementation that leverages a Dictionary<TKey, TValue> instance.
ExpirationCacheStoreBase The ExpirationCacheStoreBase class is used to provide the base cache store implementation that handles expiration policies.
RelativeExpirationPolicy The RelativeExpirationPolicy class is used to define a relative time when a cache item is to expire.
ICacheStore The ICacheStore interface defines the methods used to read and write to a cache store.
IConfigurationStore The IConfigurationStore interface defines the methods used to read and write to a configuration store.
IExpirationPolicy The IExpirationPolicy interface is used to define how a cache item expiration policy is evaluated in order to determine whether the item should be removed from the cache.

Neovolve.Toolkit.Threading

Name Description
LockReader The LockReader class is used to provide thread safe read access to a resource using a provided ReaderWriterLock or ReaderWriterLockSlim instance.
LockWriter The LockWriter class is used to provide thread safe write access to a resource using a provided ReaderWriterLock or ReaderWriterLockSlim instance.

Neovolve.Toolkit.Unity.dll

Name Description
AppSettingsParameterValueElement The AppSettingsParameterValueElement class is used to configure a Unity injection parameter value to be determined from an AppSettings value.
ConnectionStringParameterValueElement The ConnectionStringParameterValueElement class is used to configure a Unity injection parameter value to be determined from a ConnectionStringSettings value.
DisposableStrategyExtension The DisposableStrategyExtension class is used to define the build strategy for disposing objects on tear down by a IUnityContainer.
ProxyInjectionParameterValue The ProxyInjectionParameterValue class is used to provide the parameter value information for a proxy injection parameter.
ProxyParameterValueElement The ProxyParameterValueElement class is used to configure a Unity parameter value to be determined from a proxy value created by ProxyManager<T>.
SectionExtensionInitiator The SectionExtensionInitiator class is used to initiate a SectionExtension with configuration element support for custom parameter injection values.
UnityContainerResolver The UnityContainerResolver class is used to resolve a IUnityContainer instance from configuration.
UnityControllerFactoryHttpModule The UnityControllerFactoryHttpModule class is used to build up ASP.Net MVC controller instances using an IUnityContainer.
UnityHttpModule The UnityHttpModule class is used to build up ASP.Net pages with property and method injection after they are created but before they are used for request processing.
UnityHttpModuleBase The UnityHttpModuleBase class is used to provide management of a global unity container for IHttpModule instances.
UnityServiceBehavior The UnityServiceBehavior class is used to provide a service behavior for configuring unity injection in WCF.
UnityServiceElement The UnityServiceElement class is used to provide configuration support for defining a unity container via a service behavior.
UnityServiceHostFactory The UnityServiceHostFactory class is used to create a ServiceHost instance that supports creating service instances with Unity.

Neovolve.Toolkit.Workflow.dll

Neovolve.Toolkit.Workflow

Name Description
ActivityFailureException The ActivityFailureException class is used to describe a failure in the execution of a workflow activity .
ActivityInvoker The ActivityInvoker class is used to invoke activities.
ActivityStore The ActivityStore class is used to cache activity instances for reuse.
InstanceHandler<T> The InstanceHandler<T> class is used to provide instance handling logic for a InstanceResolver<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> instance.
ResumeBookmarkContext The ResumeBookmarkContext class is used to resume a workflow from a bookmark.
ResumeBookmarkContext<T> The ResumeBookmarkContext<T> class is used to define the information required to resume a workflow bookmark.
GenericArgumentCount The GenericArgumentCount enum defines the number of generic arguments available for an activity type.

Neovolve.Toolkit.Workflow.Activities

Name Description
ExecuteBookmark The ExecuteBookmark class is a workflow activity that is used to process bookmarks.
ExecuteBookmark<T> The ExecuteBookmark<T> class is a workflow activity that is used to process bookmarks.
GetWorkflowInstanceId The GetWorkflowInstanceId class is used to obtain the instance id of the executing workflow.
InstanceResolver The InstanceResolver class is used to provide a resolved instance for a child activity.
InstanceResolver<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 > The InstanceResolver<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 > class is used to provide resolution of instances for workflow activities.
SystemFailureEvaluator The SystemFailureEvaluator class is used to evaluate a condition to determine whether a system failure has occurred.

The icons used for these activities come from the fabulous famfamfam Silk Icon collection.

Neovolve.Toolkit.Workflow.Extensions

Name Description
InstanceManagerExtension The InstanceManagerExtension class is used to manage instances resolved from a container.

Custom Windows Workflow activity for dependency resolution–Part 6

The previous post in this series provided a custom updatable generic type argument implementation for the InstanceResolver activity. This post will look at the the XAML designer support for this activity.

The designer support for the InstanceResolver intends to display only the number of dependency resolutions that are configured according to the ArgumentCount property. Each dependency resolution shown needs to provide editing functionality for the resolution type, resolution name and the name of the handler reference.

image

The XAML for the designer defines the activity icon, display for each argument and the child activity to execute. Each of the arguments is bound to an attached property that defines whether that argument is visible to the designer.

<sap:ActivityDesigner x:Class="Neovolve.Toolkit.Workflow.Design.Presentation.InstanceResolverDesigner"
                      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                      xmlns:s="clr-namespace:System;assembly=mscorlib"
                      xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
                      xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation"
                      xmlns:conv="clr-namespace:System.Activities.Presentation.Converters;assembly=System.Activities.Presentation"
                      xmlns:sadm="clr-namespace:System.Activities.Presentation.Model;assembly=System.Activities.Presentation"
                      xmlns:ComponentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
                      xmlns:ntw="clr-namespace:Neovolve.Toolkit.Workflow;assembly=Neovolve.Toolkit.Workflow"
                      xmlns:ntwd="clr-namespace:Neovolve.Toolkit.Workflow.Design">
    <sap:ActivityDesigner.Icon>
        <DrawingBrush> 
            <DrawingBrush.Drawing>
                <ImageDrawing>
                    <ImageDrawing.Rect>
                        <Rect Location="0,0"
                              Size="16,16">
                        </Rect>
                    </ImageDrawing.Rect>
                    <ImageDrawing.ImageSource>
                        <BitmapImage UriSource="brick.png"></BitmapImage>
                    </ImageDrawing.ImageSource>
                </ImageDrawing>
            </DrawingBrush.Drawing>
        </DrawingBrush>
    </sap:ActivityDesigner.Icon>
    <sap:ActivityDesigner.Resources>
    <conv:ModelToObjectValueConverter x:Key="modelItemConverter"
                                          x:Uid="sadm:ModelToObjectValueConverter_1" />
        <conv:ArgumentToExpressionConverter x:Key="expressionConverter" />
        <ObjectDataProvider MethodName="GetValues"
                            ObjectType="{x:Type s:Enum}"
                            x:Key="EnumSource">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="ntw:GenericArgumentCount" />
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>
        <DataTemplate x:Key="Collapsed">
            <TextBlock HorizontalAlignment="Center"
                       FontStyle="Italic"
                       Foreground="Gray">
                Double-click to view
            </TextBlock>
        </DataTemplate>
        <DataTemplate x:Key="Expanded">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>
        <StackPanel Orientation="Vertical">
          <StackPanel Orientation="Horizontal"
                            Margin="2">
            <TextBlock VerticalAlignment="Center">Argument Count:</TextBlock>
            <ComboBox x:Name="ArgumentCountList"
                              ItemsSource="{Binding Source={StaticResource EnumSource}}"
                              SelectedItem="{Binding Path=ModelItem.Arguments, Mode=TwoWay}"/>
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2" Visibility="{Binding Path=ModelItem.ArgumentVisible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="false"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument1.Name}"
                             MinWidth="80" />
            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>
            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName1, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2" Visibility="{Binding Path=ModelItem.Argument1Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="false"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType1, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument1.Name}"
                             MinWidth="80" />
            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>
            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName1, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument2Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType2, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument2.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName2, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument3Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType3, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument3.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName3, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument4Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType4, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument4.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName4, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument5Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType5, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument5.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName5, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument6Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType6, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument6.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName6, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument7Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType7, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument7.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName7, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument8Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType8, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument8.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName8, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument9Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType9, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument9.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName9, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument10Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType10, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument10.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName10, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument11Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType11, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument11.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName11, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument12Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType12, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument12.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName12, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument13Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType13, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument13.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName13, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument14Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType14, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument14.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName14, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument15Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType15, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument15.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName15, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

          <StackPanel Orientation="Horizontal"
                            Margin="2"
                            Grid.Row="2" Visibility="{Binding Path=ModelItem.Argument16Visible}">
            <sapv:TypePresenter Width="120"
                                        Margin="5"
                                        AllowNull="true"
                                        BrowseTypeDirectly="false"
                                        Label="Target type"
                                        Type="{Binding Path=ModelItem.ArgumentType16, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
                                        Context="{Binding Context}" />
            <TextBox Text="{Binding ModelItem.Body.Argument16.Name}"
                             MinWidth="80" />

            <TextBlock VerticalAlignment="Center"
                               Margin="2">
                        with name
            </TextBlock>

            <sapv:ExpressionTextBox Expression="{Binding Path=ModelItem.ResolutionName16, Converter={StaticResource expressionConverter}}"
                                            ExpressionType="s:String"
                                            OwnerActivity="{Binding ModelItem}"
                                            Margin="2" />
          </StackPanel>

        </StackPanel>
        
                <sap:WorkflowItemPresenter Item="{Binding ModelItem.Body.Handler}"
                                           HintText="Drop activity"
                                           Grid.Row="1"
                                           Margin="6" />
            </Grid>
        </DataTemplate>
        <Style x:Key="ExpandOrCollapsedStyle"
               TargetType="{x:Type ContentPresenter}">
            <Setter Property="ContentTemplate"
                    Value="{DynamicResource Collapsed}" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding Path=ShowExpanded}"
                             Value="true">
                    <Setter Property="ContentTemplate"
                            Value="{DynamicResource Expanded}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </sap:ActivityDesigner.Resources>
    <Grid>
        <ContentPresenter Style="{DynamicResource ExpandOrCollapsedStyle}"
                          Content="{Binding}" />
    </Grid>
</sap:ActivityDesigner>

There is a lot of duplication in this XAML for each of the argument definitions and I am quite embarrassed by this poor implementation. It is a result of having next to zero WPF experience and needing to trade-off my desire for perfection with the demand for my time to work on other projects. I attempted a UserControl to reduce this duplication but hit many hurdles around binding the expression of the ResolutionName properties through to the ExpressionTextBox in the UserControl. Hopefully greater minds will be able to contribute a better solution for this part of the series.

The code behind this designer detects a new ModelItem being assigned and then attaches properties to it that are bound to in the XAML.

namespace Neovolve.Toolkit.Workflow.Design.Presentation
{
    using System;
    using System.Diagnostics;
    using Neovolve.Toolkit.Workflow.Activities;

    public partial class InstanceResolverDesigner
    {
        [DebuggerNonUserCode]
        public InstanceResolverDesigner()
        {
            InitializeComponent();
        }

        protected override void OnModelItemChanged(Object newItem)
        {
            InstanceResolverDesignerExtension.Attach(ModelItem);

            base.OnModelItemChanged(newItem);
        }
    }
}

The InstanceResolverDesignerExtension class creates attached properties to manage the InstanceResolver.ArgumentCount property and the set of properties that control the argument visibility state.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities.Presentation;
    using System.Activities.Presentation.Model;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Windows;

    public class InstanceResolverDesignerExtension
    {
        internal const String Arguments = "Arguments";

        private const String ArgumentCount = "ArgumentCount";

        private readonly Dictionary<String, AttachedProperty> _attachedProperties = new Dictionary<String, AttachedProperty>();

        private GenericArgumentCount _previousArguments;

        public static void Attach(ModelItem modelItem)
        {
            EditingContext editingContext = modelItem.GetEditingContext();
            InstanceResolverDesignerExtension designerExtension = editingContext.Services.GetService<InstanceResolverDesignerExtension>();

            if (designerExtension == null)
            {
                designerExtension = new InstanceResolverDesignerExtension();

                editingContext.Services.Publish(designerExtension);
            }

            designerExtension.AttachToModelItem(modelItem);
        }

        private static AttachedProperty<Visibility> AttachArgumentVisibleProperty(
            AttachedPropertiesService attachedPropertiesService, ModelItem modelItem, Int32 argumentNumber)
        {
            String propertyName;

            if (argumentNumber > 0)
            {
                propertyName = "Argument" + argumentNumber + "Visible";
            }
            else
            {
                propertyName = "ArgumentVisible";
            }

            AttachedProperty<Visibility> attachedProperty = new AttachedProperty<Visibility>
                                                            {
                                                                IsBrowsable = false, 
                                                                Name = propertyName, 
                                                                OwnerType = modelItem.ItemType, 
                                                                Getter = delegate(ModelItem modelReference)
                                                                         {
                                                                             Int32 argumentCount =
                                                                                 (Int32)modelReference.Properties[Arguments].ComputedValue;

                                                                             if (argumentNumber == 0 && argumentCount == 1)
                                                                             {
                                                                                 return Visibility.Visible;
                                                                             }

                                                                             if (argumentNumber != 0 && argumentCount > 1 &&
                                                                                 argumentNumber <= argumentCount)
                                                                             {
                                                                                 return Visibility.Visible;
                                                                             }

                                                                             return Visibility.Collapsed;
                                                                         }, 
                                                                Setter = delegate(ModelItem modelReference, Visibility newValue)
                                                                         {
                                                                             Debug.WriteLine("Visibility updated to " + newValue);
                                                                         }
                                                            };

            attachedPropertiesService.AddProperty(attachedProperty);

            return attachedProperty;
        }

        private void AttachArgumentsProperty(ModelItem modelItem, AttachedPropertiesService attachedPropertiesService)
        {
            AttachedProperty<GenericArgumentCount> argumentsProperty = new AttachedProperty<GenericArgumentCount>
                                                                       {
                                                                           Name = Arguments, 
                                                                           IsBrowsable = true, 
                                                                           OwnerType = modelItem.ItemType, 
                                                                           Getter = delegate(ModelItem modelReference)
                                                                                    {
                                                                                        return
                                                                                            (GenericArgumentCount)
                                                                                            modelReference.Properties[ArgumentCount].ComputedValue;
                                                                                    }, 
                                                                           Setter = delegate(ModelItem modelReference, GenericArgumentCount newValue)
                                                                                    {
                                                                                        _previousArguments =
                                                                                            (GenericArgumentCount)
                                                                                            modelReference.Properties[ArgumentCount].ComputedValue;

                                                                                        modelReference.Properties[ArgumentCount].ComputedValue =
                                                                                            newValue;

                                                                                        // Update the activity to use InstanceResolver with the correct number of arguments
                                                                                        UpgradeInstanceResolverType(modelReference);
                                                                                    }
                                                                       };

            attachedPropertiesService.AddProperty(argumentsProperty);
        }

        private void AttachToModelItem(ModelItem modelItem)
        {
            // Get the number of arguments from the model
            GenericArgumentCount argumentCount = (GenericArgumentCount)modelItem.Properties[ArgumentCount].ComputedValue;
            EditingContext editingContext = modelItem.GetEditingContext();
            AttachedPropertiesService attachedPropertiesService = editingContext.Services.GetService<AttachedPropertiesService>();

            // Store the previous argument count value to track the changes to the property
            _previousArguments = argumentCount;

            // Attach updatable type arguments
            InstanceResolverTypeUpdater.AttachUpdatableArgumentTypes(modelItem, (Int32)argumentCount);

            AttachArgumentsProperty(modelItem, attachedPropertiesService);

            // Start from the second argument because there must always be at least one instance resolution per InstaceResolver
            for (Int32 index = 0; index <= 16; index++)
            {
                // Create an attached property that calculates the designer visibility for a stack panel
                AttachedProperty<Visibility> argumentVisibleProperty = AttachArgumentVisibleProperty(attachedPropertiesService, modelItem, index);

                _attachedProperties[argumentVisibleProperty.Name] = argumentVisibleProperty;
            }
        }

        private void UpgradeInstanceResolverType(ModelItem modelItem)
        {
            Type[] originalGenericTypes = modelItem.ItemType.GetGenericArguments();
            Type[] genericTypes = new Type[16];

            originalGenericTypes.CopyTo(genericTypes, 0);

            if (originalGenericTypes.Length < genericTypes.Length)
            {
                Type defaultGenericType = typeof(Object);

                // This type is being upgraded from InstanceResolver to InstanceResolver
                // Initialize the generic types with Object
                for (Int32 index = originalGenericTypes.Length; index < genericTypes.Length; index++)
                {
                    genericTypes[index] = defaultGenericType;
                }
            }

            Type newType = RegisterMetadata.InstanceResolverT16GenericType.MakeGenericType(genericTypes);

            InstanceResolverTypeUpdater.UpdateModelType(modelItem, newType, _previousArguments);
        }
    }
}

An attached property is used for the ArgumentCount property in order to track changes to the property. Updates to this property then cause an update to the ModelItem.There did not seem to be any other way to detect changes to this property from the designer code when binding directly to the property on the activity.

There were a few other designer quirks discovered throughout this exercise.

  1. The binding to the visibility attached properties did not correctly update in the designer when the values were changed. The property is actually only a Getter in its functionality as it is a calculation based on the current argument number against the total number of displayed arguments. The way to get the binding to be updatable was to provide a Setter that did nothing.
  2. Attached properties marked as IsBrowsable = false are not available via the ModelItem.Properties collection. The solution here was to cache these attached properties against the extension (service) stored against the services of the associated EditingContext.
  3. Attached properties cannot be removed once they are attached. This affected the desired outcome for the property grid experience.
  4. Attached properties cannot have custom attributes assigned to them so there is no support for Description or Category values in the property grid.

The designer support using this implementation is usable but far from perfect. The biggest issue is synchronisation between the ModelItem on the designer and the property grid. The property grid can get a little out of sync especially with changes to the Arguments property. The problem here is the ability to refresh/update the contents of the property grid when certain events occur on the ModelItem in the designer. Interacting with the activity on the design surface directly seems to provide the most reliable result.

This post has covered the designer support for the InstanceResolver activity and provided information on some gotchas for working with activity designers. The posts in this series have covered all aspects of implementing a custom activity for resolving dependencies within WF4 workflows.

Custom Windows Workflow activity for dependency resolution–Part 5

The previous post covers the initial background for designer support of custom Windows Workflow activities. This post outlines a customised version of the updatable generic type support outlined in this post that is specific to the InstanceResolver activity.

One of my original design goals for this custom activity was to provide adequate designer support. The initial version of this custom activity resolved a single dependency. This was clearly limited as I often have multiple instance resolutions that I use in a workflow. A simple workaround would be to nest several of these activities to achieve the desired result however this would result in a very messy workflow design.

The implementation of the InstanceResolver activity avoids this scenario by supporting up to 16 dependency resolutions. This presents a usability issue with the designer support for the activity. The activity will provide 16 potential dependency resolutions even when just one or two are used. The activity designer addresses this by leveraging the ArgumentCount property of InstanceResolver that determines how many arguments are used by the activity. One area that this property value is used is in the behaviour of the updatable generic type support.

The InstanceResolverTypeUpdater class shown below is very similar to the GenericTypeUpdater provided in this post.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Activities.Presentation.Model;
    using System.Linq;
    using Neovolve.Toolkit.Workflow.Activities;
 
    public static class InstanceResolverTypeUpdater
    {
        private const String DisplayName = "DisplayName";

        public static void AttachUpdatableArgumentTypes(ModelItem modelItem)
        {
            AttachUpdatableArgumentTypes(modelItem, Int32.MaxValue);
        }

        public static void AttachUpdatableArgumentTypes(ModelItem modelItem, Int32 maximumUpdatableTypes)
        {
            Type[] genericArguments = modelItem.ItemType.GetGenericArguments();

            if (genericArguments.Any() == false)
            {
                return;
            }

            Int32 argumentCount = genericArguments.Length;
            Int32 updatableArgumentCount = Math.Min(argumentCount, maximumUpdatableTypes);
            EditingContext editingContext = modelItem.GetEditingContext();
            AttachedPropertiesService attachedPropertiesService = editingContext.Services.GetService<AttachedPropertiesService>();

            for (Int32 index = 0; index < updatableArgumentCount; index++)
            {
                AttachUpdatableArgumentType(modelItem, attachedPropertiesService, index, updatableArgumentCount);
            }
        }

        public static void UpdateModelType(ModelItem modelItem, Type newType, GenericArgumentCount previousArguments)
        {
            EditingContext editingContext = modelItem.GetEditingContext();
            Object instanceOfNewType = Activator.CreateInstance(newType);
            ModelItem newModelItem = ModelFactory.CreateItem(editingContext, instanceOfNewType);

            using (ModelEditingScope editingScope = newModelItem.BeginEdit("Change type argument"))
            {
                MorphHelper.MorphObject(modelItem, newModelItem);
                MorphHelper.MorphProperties(modelItem, newModelItem);

                Type itemType = modelItem.ItemType;

                if (itemType.IsSubclassOf(typeof(Activity)) && newType.IsSubclassOf(typeof(Activity)))
                {
                    GenericArgumentCount argumentCount =
                        (GenericArgumentCount)modelItem.Properties[InstanceResolverDesignerExtension.Arguments].ComputedValue;

                    if (DisplayNameRequiresUpdate(modelItem, previousArguments))
                    {
                        // Update to the new display name
                        String newDisplayName = InstanceResolver.GenerateDisplayName(newType, argumentCount);

                        newModelItem.Properties[DisplayName].SetValue(newDisplayName);
                    }
                }

                DesignerUpdater.UpdateModelItem(modelItem, newModelItem);

                editingScope.Complete();
            }
        }

        private static void AttachUpdatableArgumentType(
            ModelItem modelItem, AttachedPropertiesService attachedPropertiesService, Int32 argumentIndex, Int32 argumentCount)
        {
            String propertyName = "ArgumentType";

            if (argumentCount > 1)
            {
                propertyName += argumentIndex + 1;
            }

            AttachedProperty<Type> attachedProperty = new AttachedProperty<Type>
                                                      {
                                                          Name = propertyName, 
                                                          OwnerType = modelItem.ItemType, 
                                                          IsBrowsable = true
                                                      };

            attachedProperty.Getter = (ModelItem arg) => GetTypeArgument(arg, argumentIndex);
            attachedProperty.Setter = (ModelItem arg, Type newType) => UpdateTypeArgument(arg, argumentIndex, newType);

            attachedPropertiesService.AddProperty(attachedProperty);
        }

        private static Boolean DisplayNameRequiresUpdate(ModelItem modelItem, GenericArgumentCount previousArgumentCount)
        {
            String currentDisplayName = (String)modelItem.Properties[DisplayName].ComputedValue;

            // Sometimes the display name is empty
            if (String.IsNullOrWhiteSpace(currentDisplayName))
            {
                return true;
            }

            // The default calculation of a generic type does not include spaces in the generic type arguments
            // However an activity might include these as the default display name
            // Strip spaces to provide a more accurate match
            String defaultDisplayName = InstanceResolver.GenerateDisplayName(modelItem.ItemType, previousArgumentCount);

            currentDisplayName = currentDisplayName.Replace(" ", String.Empty);
            defaultDisplayName = defaultDisplayName.Replace(" ", String.Empty);

            if (String.Equals(currentDisplayName, defaultDisplayName, StringComparison.Ordinal))
            {
                return true;
            }

            return false;
        }

        private static Type GetTypeArgument(ModelItem modelItem, Int32 argumentIndex)
        {
            return modelItem.ItemType.GetGenericArguments()[argumentIndex];
        }

        private static void UpdateTypeArgument(ModelItem modelItem, Int32 argumentIndex, Type newGenericType)
        {
            Type itemType = modelItem.ItemType;
            GenericArgumentCount previousArgumentCount =
                (GenericArgumentCount)modelItem.Properties[InstanceResolverDesignerExtension.Arguments].ComputedValue;
            Type[] genericTypes = itemType.GetGenericArguments();

            // Replace the type being changed
            genericTypes[argumentIndex] = newGenericType;

            Type newType = itemType.GetGenericTypeDefinition().MakeGenericType(genericTypes);

            UpdateModelType(modelItem, newType, previousArgumentCount);
        }
    }
}

This implementation is different to the GenericTypeUpdater in that it provides some specialised support regarding the ArgumentCount property of the ModelItem. The ArgumentCount property affects this updatable generic type support in two ways.

Firstly it only attaches updatable generic type properties for as many generic types as is defined by ArgumentCount. If ArgumentCount = One, only one AttachedProperty<Type> is attached to the ModelItem in the designer. If ArgumentCount = Two, then the activity has two AttachedProperty<Type> attached to the ModelItem. As so it goes on.

image[10]

Secondly the support limits updating the default display name of the activity to the number of types defined by ArgumentCount even though the InstanceResolver class defines 16 generic arguments. This means that the display name is for example InstanceResolver<String, ITestInstance> where ArgumentCount = Two rather than InstanceResolver<String, ITestInstance, Object, Object, Object……T16>.

image[12]

This post has provided a custom updatable generic type support specific to the InstanceResolver activity. The next post will look at the XAML designer support for the InstanceResolver activity and further uses of attached properties.

Custom Windows Workflow activity for dependency resolution–Part 4

The posts in this series have looked at providing a custom activity for dependency resolution in Windows Workflow. The series will now take a look at providing designer support for this activity. This post will cover the IRegisterMetadata interface and support for custom morphing.

Designer Support

The first action to take when creating WF4 activity designer support is to create a new [VS] project. The name of this project should be prefixed with the name of the assembly that contains the activities related to the designers. The project should have the suffix of “Design”. In the case of my Toolkit project, the assembly that contains the custom activities is called Neovolve.Toolkit.Workflow.dll and the designer assembly is called Neovolve.Toolkit.Workflow.Design.dll.

I’m not a fan of this restriction as my original intention was to group the designers in the same assembly as the activities. I wanted this for the purpose of portability so as to minimise the number of assemblies that developers needed to reference in order to leverage my toolkit.

The restriction of this project segregation and specific naming is because of the IRegisterMetadata implementation. In addition to the project segregation, these assemblies must be in the same directory for the IRegisterMetadata implementation to be picked up and executed. These restrictions are not part of the MSDN documentation but are provided in this forum post. I have found that adding the following post build script to the designer project is very useful to ensure that the assemblies are co-located.

copy "$(TargetDir)Neovolve.Toolkit.Workflow.Design.*" "$(SolutionDir)\Neovolve.Toolkit.Workflow\$(OutDir)"

This script is helpful for testing your activities and their designers with a separate [VS] instance. You will obviously need to change the project names to match your own projects for this script to work.

While this setup is restrictive in a sense, the segregation of these projects does have a benefit. A developer may use your custom activities in the activity assembly. Their usage is supplemented with any design time experience provided by the designer assembly. They will not need to redistribute your designer assembly with their own assembly once their development has completed.

It is important to note that the activity assembly should never reference the designer assembly. This is one of the reasons that IRegisterMetadata exists.

IRegisterMetadata

An implementation of IRegisterMetadata provides the ability to describe metadata for an activity type in a way that is decoupled from the activity itself. This is the way that an activity designer is associated with an activity because the activity assembly does not have any reference to the designer assembly.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities;
    using System.Activities.Presentation.Metadata;
    using System.Activities.Presentation.Model;
    using System.ComponentModel;
    using Neovolve.Toolkit.Workflow.Activities;
    using Neovolve.Toolkit.Workflow.Design.Presentation;

    public class RegisterMetadata : IRegisterMetadata
    {
        private static readonly Type _activityActionGenericType =
            typeof(
                ActivityAction
                    <Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object>).
                GetGenericTypeDefinition();

        private static readonly Type _instanceResolverT16GenericType =
            typeof(
                InstanceResolver
                    <Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object>).
                GetGenericTypeDefinition();

        public void Register()
        {
            AttributeTableBuilder builder = new AttributeTableBuilder();

            builder.AddCustomAttributes(typeof(ExecuteBookmark), new DesignerAttribute(typeof(ExecuteBookmarkDesigner)));
            builder.AddCustomAttributes(typeof(ExecuteBookmark<>), new DesignerAttribute(typeof(ExecuteBookmarkTDesigner)));
            builder.AddCustomAttributes(typeof(GetWorkflowInstanceId), new DesignerAttribute(typeof(GetWorkflowInstanceIdDesigner)));
            builder.AddCustomAttributes(_instanceResolverT16GenericType, new DesignerAttribute(typeof(InstanceResolverDesigner)));
            builder.AddCustomAttributes(typeof(SystemFailureEvaluator), new DesignerAttribute(typeof(SystemFailureEvaluatorDesigner)));

            MetadataStore.AddAttributeTable(builder.CreateTable());

            MorphHelper.AddPropertyValueMorphHelper(_activityActionGenericType, MorphExtension.MorphActivityAction);
        }

        internal static Type InstanceResolverT16GenericType
        {
            get
            {
                return _instanceResolverT16GenericType;
            }
        }
    }
}

The RegisterMetadata class seen here is the IRegisterMetadata implementation for my custom workflows. This class does two things. Firstly it associates activity designers with their activities. Secondly it takes the opportunity to add a custom morph action into the MorphHelper class.

[VS] searches for the designer assembly when it loads the assembly containing the custom activities. It will then look for a class that implements IRegisterMetadata and execute its Register method.

MorphHelper

The previous post provided the implementation for supporting updatable generic activity types. Part of the implementation of this process is a reference to the MorphHelper class.

MorphHelper is used to copy information between ModelItems that are used to describe an activity in the designer. It is used by updatable generic activity types to copy properties and child activity information (among other things) from the original activity type to the new activity type.

Consider what happens when you change ParallelForEach<String> to ParallelForEach<Boolean>. Any property information assigned to the activity (like the enumerable reference and child activity definition) is copied between the activity definitions even though the two activity types are not the same. This is the power of MorphHelper.

Understandably MorphHelper will not know how to transform any possible data/type structure between ModelItem instances. Thankfully the helper class is extensible as it allows custom morph actions to be added via the AddPropertyValueMorphHelper method. Reflector shows that this is how WF4 configures MorphHelper for the morph actions that come out of the box. These are wired up in the WF4 IRegisterMetadata implementation defined in the System.Activities.Core.Presentation.DesignerMetadata class.

The DesignerMetadata class contains the following default morph actions.

MorphHelper.AddPropertyValueMorphHelper(typeof(InArgument<>), new PropertyValueMorphHelper(MorphHelpers.ArgumentMorphHelper));
MorphHelper.AddPropertyValueMorphHelper(typeof(OutArgument<>), new PropertyValueMorphHelper(MorphHelpers.ArgumentMorphHelper));
MorphHelper.AddPropertyValueMorphHelper(typeof(InOutArgument<>), new PropertyValueMorphHelper(MorphHelpers.ArgumentMorphHelper));
MorphHelper.AddPropertyValueMorphHelper(typeof(ActivityAction<>), new PropertyValueMorphHelper(MorphHelpers.ActivityActionMorphHelper));

There is support for morphing InArgument<>, OutArgument<>, InOutArgument<> and ActivityAction<> properties between ModelItem types.

The issue I had with creating the updatable type support for InstanceResolver was that the 16 handlers defined against ActivityAction<T1…T1> for the activity were not copied from the old ModelItem to the new ModelItem. The reason for this turned about to be that the morph action for ActivityAction<> only supports a single generic argument whereas the InstanceResolver activity has 16. This is another scenario where the Microsoft implementation is limited to a single generic argument like the limitations of the DefaultTypeArgumentAttribute (as indicated in this post).

The extensibility support for MorphHelper does however mean that a custom implementation can be provided for ActivityAction<T1…T16>.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities;
    using System.Activities.Presentation.Model;

    internal static class MorphExtension
    {
        public static Object MorphActivityAction(ModelItem originalValue, ModelProperty newModelProperty)
        {
            Type newActivityActionType = newModelProperty.PropertyType;
            ActivityDelegate newActivityDelegate = (ActivityDelegate)Activator.CreateInstance(newActivityActionType);
            ModelItem newModelItem = ModelFactory.CreateItem(originalValue.GetEditingContext(), newActivityDelegate);
            Type[] genericArguments = newActivityActionType.GetGenericArguments();

            for (Int32 index = 1; index <= genericArguments.Length; index++)
            {
                String argumentName = "Argument" + index;
                ModelItem argumentItem = originalValue.Properties[argumentName].Value;

                if (argumentItem != null)
                {
                    Type[] delegateTypeList = new[]
                                              {
                                                  genericArguments[index - 1]
                                              };
                    DelegateInArgument argument =
                        (DelegateInArgument)Activator.CreateInstance(typeof(DelegateInArgument<>).MakeGenericType(delegateTypeList));

                    argument.Name = (String)argumentItem.Properties["Name"].Value.GetCurrentValue();
                    newModelItem.Properties[argumentName].SetValue(argument);
                }
            }

            const String HandlerName = "Handler";
            ModelItem handerItem = originalValue.Properties[HandlerName].Value;

            if (handerItem != null)
            {
                // Copy the activity of the activity action
                newModelItem.Properties[HandlerName].SetValue(handerItem);
                originalValue.Properties[HandlerName].SetValue(null);
            }

            return newModelItem;
        }
    }
}

This code is modelled from the Microsoft implementation of ActivityAction<> morphing. This implementation however has full support for multiple generic types.

With this method registered in MorphHelper via the above RegisterMetadata class, changing a generic type in my InstanceResolver class now correctly morphs the internals of the activity from the old type to the new type.

This post has covered support for IRegisterMetadata and MorphHelper. The next post will look at specialised implementation for updatable generic type support for the InstanceResolver activity.

Creating updatable generic Windows Workflow activities

This post is a segue from the current series on building a custom activity for supporting dependency resolution in Windows Workflow (here, here, here and here so far). This post will outline how to support updating generic type arguments of generic activities in the designer. This technique is used in the designer support for the InstanceResolver activity that has been discussed throughout the series.

The implementation of this is modelled from the support for this functionality in the generic WF4 activities such as ForEach<T> and ParallelForEach<T>. Unfortunately the logic that drives this is marked as internal and is therefore not available to developers who create custom generic activities.

In the case of the ForEach<T> activity, the default generic type value used is int.

image

This can be changed in the property grid of the activity using the TypeArgument property.

image

Changing this value will update the definition of the activity with the new type argument. For example, the type could be change to Boolean.

image

This post will use my ExecuteBookmark<T> activity to demonstrate this functionality. This activity provides the reusable structure for persisting and resuming workflows.

namespace Neovolve.Toolkit.Workflow.Activities
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.ComponentModel;
    using System.Drawing;
    using System.Globalization;
 
    [ToolboxBitmap(typeof(ExecuteBookmark), "book_open.png")]
    [DefaultTypeArgument(typeof(String))]
    public sealed class ExecuteBookmark<T> : NativeActivity<T>
    {
        protected override void Execute(NativeActivityContext context)
        {
            String bookmarkName = context.GetValue(BookmarkName);

            if (String.IsNullOrWhiteSpace(bookmarkName))
            {
                throw new ArgumentNullException("BookmarkName");
            }
            
            context.CreateBookmark(bookmarkName, BookmarkResumed);
        }

        private void BookmarkResumed(NativeActivityContext context, Bookmark bookmark, Object value)
        {
            T newValue = (T)Convert.ChangeType(value, typeof(T), CultureInfo.CurrentCulture);

            Result.Set(context, newValue);
        }

        [RequiredArgument]
        [Category("Inputs")]
        [Description("The name used to identify the bookmark")]
        public InArgument<String> BookmarkName
        {
            get;
            set;
        }
        
        protected override Boolean CanInduceIdle
        {
            get
            {
                return true;
            }
        }
    }
}

This activity defines the default type of String. Designer support for changing this type is required after dropping the activity on the designer because the DefaultArgumentTypeAttribute avoids the developer having to define the generic type up front. It has the additional benefit of allowing the developer to change the activity type once it is is already on the designer as the workflow is developed and refactored.

The ArgumentType property does not exist on the ExecuteBookmark<T> class. It is an AttachedProperty<Type> instance attached to the ModelItem that represents the activity on the design surface. The setter of this property provides the notification that the type is being changed. The designer attaches the property to the ModelItem in the activity designer when a new ModelItem instance is assigned.

namespace Neovolve.Toolkit.Workflow.Design.Presentation
{
    using System;
    using System.Diagnostics;
    using Neovolve.Toolkit.Workflow.Activities;

    public partial class ExecuteBookmarkTDesigner
    {
        [DebuggerNonUserCode]
        public ExecuteBookmarkTDesigner()
        {
            InitializeComponent();
        }

        protected override void OnModelItemChanged(Object newItem)
        {
            base.OnModelItemChanged(newItem);

            GenericArgumentTypeUpdater.Attach(ModelItem);
        }
    }
}

The designer calls down into a custom GenericArgumentTypeUpdater class to attach the updatable type functionality to the ModelItem. Unlike the internal Microsoft implementation, this class supports multiple generic type arguments.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Activities.Presentation.Model;
    using System.Linq;

    public static class GenericArgumentTypeUpdater
    {
        private const String DisplayName = "DisplayName";

        public static void Attach(ModelItem modelItem)
        {
            Attach(modelItem, Int32.MaxValue);
        }

        public static void Attach(ModelItem modelItem, Int32 maximumUpdatableTypes)
        {
            Type[] genericArguments = modelItem.ItemType.GetGenericArguments();

            if (genericArguments.Any() == false)
            {
                return;
            }

            Int32 argumentCount = genericArguments.Length;
            Int32 updatableArgumentCount = Math.Min(argumentCount, maximumUpdatableTypes);
            EditingContext context = modelItem.GetEditingContext();
            AttachedPropertiesService attachedPropertiesService = context.Services.GetService<AttachedPropertiesService>();

            for (Int32 index = 0; index < updatableArgumentCount; index++)
            {
                AttachUpdatableArgumentType(modelItem, attachedPropertiesService, index, updatableArgumentCount);
            }
        }

        private static void AttachUpdatableArgumentType(
            ModelItem modelItem, AttachedPropertiesService attachedPropertiesService, Int32 argumentIndex, Int32 argumentCount)
        {
            String propertyName = "ArgumentType";

            if (argumentCount > 1)
            {
                propertyName += argumentIndex + 1;
            }

            AttachedProperty<Type> attachedProperty = new AttachedProperty<Type>
                                                      {
                                                          Name = propertyName, 
                                                          OwnerType = modelItem.ItemType, 
                                                          IsBrowsable = true
                                                      };

            attachedProperty.Getter = (ModelItem arg) => GetTypeArgument(arg, argumentIndex);
            attachedProperty.Setter = (ModelItem arg, Type newType) => UpdateTypeArgument(arg, argumentIndex, newType);

            attachedPropertiesService.AddProperty(attachedProperty);
        }

        private static bool DisplayNameRequiresUpdate(ModelItem modelItem)
        {
            String currentDisplayName = (String)modelItem.Properties[DisplayName].ComputedValue;

            // Sometimes the display name is empty
            if (String.IsNullOrWhiteSpace(currentDisplayName))
            {
                return true;
            }

            // The default calculation of a generic type does not include spaces in the generic type arguments
            // However an activity might include these as the default display name
            // Strip spaces to provide a more accurate match
            String defaultDisplayName = GetActivityDefaultName(modelItem.ItemType);

            currentDisplayName = currentDisplayName.Replace(" ", String.Empty);
            defaultDisplayName = defaultDisplayName.Replace(" ", String.Empty);

            if (String.Equals(currentDisplayName, defaultDisplayName, StringComparison.Ordinal))
            {
                return true;
            }

            return false;
        }

        private static String GetActivityDefaultName(Type activityType)
        {
            Activity activity = (Activity)Activator.CreateInstance(activityType);

            return activity.DisplayName;
        }

        private static Type GetTypeArgument(ModelItem modelItem, Int32 argumentIndex)
        {
            return modelItem.ItemType.GetGenericArguments()[argumentIndex];
        }

        private static void UpdateTypeArgument(ModelItem modelItem, Int32 argumentIndex, Type newGenericType)
        {
            Type itemType = modelItem.ItemType;
            Type[] genericTypes = itemType.GetGenericArguments();

            // Replace the type being changed
            genericTypes[argumentIndex] = newGenericType;

            Type newType = itemType.GetGenericTypeDefinition().MakeGenericType(genericTypes);
            EditingContext editingContext = modelItem.GetEditingContext();
            Object instanceOfNewType = Activator.CreateInstance(newType);
            ModelItem newModelItem = ModelFactory.CreateItem(editingContext, instanceOfNewType);

            using (ModelEditingScope editingScope = newModelItem.BeginEdit("Change type argument"))
            {
                MorphHelper.MorphObject(modelItem, newModelItem);
                MorphHelper.MorphProperties(modelItem, newModelItem);

                if (itemType.IsSubclassOf(typeof(Activity)) && newType.IsSubclassOf(typeof(Activity)))
                {
                    if (DisplayNameRequiresUpdate(modelItem))
                    {
                        // Update to the new display name
                        String newDisplayName = GetActivityDefaultName(newType);

                        newModelItem.Properties[DisplayName].SetValue(newDisplayName);
                    }
                }

                DesignerUpdater.UpdateModelItem(modelItem, newModelItem);

                editingScope.Complete();
            }
        }
    }
}

The class determines how many generic type arguments on the activity will be updatable. It then loops through this number and creates an attached property on the ModelItem for each of these. The AttachedProperty is marked as IsBrowsable = true so that it is displayed in the property grid.

The getter Func<T> of the attached property simply returns the generic type argument of the current activity type for the index related to the attached property. The setter is where all the action happens. It is the logic behind the attached property that was copied from Microsoft internal implementation.

Updating the type involves calculating what the new type will be. For example, SomeActivity<String, Boolean> could be updated to SomeActivity<String, Int32>. This new type is determined and an instance of it is created. The instance of the new type is used to create a new ModelItem for the designer.

An ModelEditingScope is used at this point in order to group a set of designer changes into one unit. This means that there will be one Undo/Redo command in [VS] rather than one for each individual designer change detected in this process.

The editing scope uses a MorphHelper to create the new type from the old type. This process will copy across all the supported changes from the old type to the new type (properties, child activities etc).

The next job is to detect if the activity has the default display name value. If this is the case, then the display name will be updated to the default display name of the new activity type. This is done because the display name of generic activities is normally calculated as TypeName<TypeName, TypeName, etc, etc>.

Lastly, the class makes a call into a DesignerUpdater helper class that is used to ensure that the updated activity is selected.

namespace Neovolve.Toolkit.Workflow.Design
{
    using System;
    using System.Activities.Presentation;
    using System.Activities.Presentation.Model;
    using System.Activities.Presentation.View;
    using System.Windows.Threading;

    internal sealed class DesignerUpdater
    {
        public static void UpdateModelItem(ModelItem originalItem, ModelItem updatedItem)
        {
            DesignerUpdater class2 = new DesignerUpdater(originalItem, updatedItem);

            Action method = class2.UpdateDesigner;

            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, method);
        }

        internal DesignerUpdater(ModelItem originalItem, ModelItem newItem)
        {
            _originalModelItem = originalItem;
            _newModelItem = newItem;
        }

        private readonly ModelItem _originalModelItem;

        private readonly ModelItem _newModelItem;

        public void UpdateDesigner()
        {
            EditingContext editingContext = _originalModelItem.GetEditingContext();
            DesignerView designerView = editingContext.Services.GetService<DesignerView>();

            if ((designerView.RootDesigner != null) && (((WorkflowViewElement)designerView.RootDesigner).ModelItem == _originalModelItem))
            {
                designerView.MakeRootDesigner(_newModelItem);
            }

            Selection.SelectOnly(editingContext, _newModelItem);
        }
    }
}

The final piece of the puzzle is support for changing the type within the designer surface itself. This is modelled from the InvokeMethod activity that allows for custom types to be defined in the designer.

image

The way to get this to work is to add the following into the XAML of the activity designer.

<sap:ActivityDesigner.Resources>
    <conv:ModelToObjectValueConverter x:Key="modelItemConverter"
        x:Uid="sadm:ModelToObjectValueConverter_1" />
</sap:ActivityDesigner.Resources>

<sapv:TypePresenter Width="120"
    Margin="5"
    AllowNull="false"
    BrowseTypeDirectly="false"
    Label="Target type"
    Type="{Binding Path=ModelItem.TypeArgument, Mode=TwoWay, Converter={StaticResource modelItemConverter}}"
    Context="{Binding Context}" />

This will provide the dropdown list of types for the designer. The first important item to note is that the TypePresenter is bound to the attached property created by GenericArgumentTypeUpdater. The second important item is the binding of the EditingContext. Without the editing context, the dropdown list and associated dialog support will not display references to assemblies and types related to the current workflow.

Using these techniques will allow a custom activity to provide updatable generic type support as part of its design time experience.

Custom Windows Workflow activity for dependency resolution–Part 3

My previous post provided the base framework for resolving dependencies, handling persistence and tearing down resolved dependencies in Windows Workflow. This post will provide the custom activity for exposing resolved dependencies to a workflow.

The original implementation of this activity supported resolving a single dependency. It has slowly evolved into one that can support up to 16 dependencies. The reason for this specific number is that the activity leverages the ScheduleAction method on the NativeActivityContext class. This method has overloads that support up to 16 generic arguments. This avoids the developer needing to use nested activities to achieve the same result if only one dependency was supported.

The ScheduleAction method provides the ability for a child activity to be scheduled for execution with one or more delegate arguments. This is the way that ForEach<T> and ParallelForEach<T> activities work. In these cases the argument defines the item being provided in the iterator of the loop behind the activity. This is seen below being defined as the variable “item”.

image

The custom activity defined here has a concept of the number of arguments that it supports at runtime. This is defined at design time using a GenericArgumentCount enum definition. In part this enum is used to support the design-time experience. The activity also uses this value to ensure that only the intended number of generic arguments are provided to the child activity.

namespace Neovolve.Toolkit.Workflow
{ 
    public enum GenericArgumentCount
    {
        One = 1, 
        Two, 
        Three, 
        Four, 
        Five, 
        Six, 
        Seven, 
        Eight, 
        Nine, 
        Ten, 
        Eleven, 
        Twelve, 
        Thirteen, 
        Fourteen, 
        Fifteen, 
        Sixteen
    }
}

The custom activity for providing dependency resolution is a generic InstanceResolver class. It defines the 16 generic arguments for the type definitions of the 16 possible dependencies to resolve. It inherits from NativeActivity in order to get access to the NativeActivityContext for scheduling child activity execution and for hooking into activity lifecycle events.

namespace Neovolve.Toolkit.Workflow.Activities
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Drawing;
    using System.Linq;
    using System.Windows;
    using System.Windows.Markup;
    using Neovolve.Toolkit.Workflow.Extensions;

    [ToolboxBitmap(typeof(InstanceResolver), "brick.png")]
    [ContentProperty("Body")]
    public class InstanceResolver<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> : NativeActivity, IActivityTemplateFactory
    {
        private readonly Variable<List<Guid>> _handlers = new Variable<List<Guid>>();

        public InstanceResolver()
        {
            ArgumentCount = GenericArgumentCount.One;
            DisplayName = InstanceResolver.GenerateDisplayName(GetType(), ArgumentCount);
            Body = new ActivityAction
                <InstanceHandler<T1>, InstanceHandler<T2>, InstanceHandler<T3>, InstanceHandler<T4>, InstanceHandler<T5>, InstanceHandler<T6>, 
                    InstanceHandler<T7>, InstanceHandler<T8>, InstanceHandler<T9>, InstanceHandler<T10>, InstanceHandler<T11>, InstanceHandler<T12>, 
                    InstanceHandler<T13>, InstanceHandler<T14>, InstanceHandler<T15>, InstanceHandler<T16>>
                   {
                       Argument1 = new DelegateInArgument<InstanceHandler<T1>>("handler1"), 
                       Argument2 = new DelegateInArgument<InstanceHandler<T2>>("handler2"), 
                       Argument3 = new DelegateInArgument<InstanceHandler<T3>>("handler3"), 
                       Argument4 = new DelegateInArgument<InstanceHandler<T4>>("handler4"), 
                       Argument5 = new DelegateInArgument<InstanceHandler<T5>>("handler5"), 
                       Argument6 = new DelegateInArgument<InstanceHandler<T6>>("handler6"), 
                       Argument7 = new DelegateInArgument<InstanceHandler<T7>>("handler7"), 
                       Argument8 = new DelegateInArgument<InstanceHandler<T8>>("handler8"), 
                       Argument9 = new DelegateInArgument<InstanceHandler<T9>>("handler9"), 
                       Argument10 = new DelegateInArgument<InstanceHandler<T10>>("handler10"), 
                       Argument11 = new DelegateInArgument<InstanceHandler<T11>>("handler11"), 
                       Argument12 = new DelegateInArgument<InstanceHandler<T12>>("handler12"), 
                       Argument13 = new DelegateInArgument<InstanceHandler<T13>>("handler13"), 
                       Argument14 = new DelegateInArgument<InstanceHandler<T14>>("handler14"), 
                       Argument15 = new DelegateInArgument<InstanceHandler<T15>>("handler15"), 
                       Argument16 = new DelegateInArgument<InstanceHandler<T16>>("handler16")
                   };
        }

        [DebuggerNonUserCode]
        public Activity Create(DependencyObject target)
        {
            return new InstanceResolver<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>();
        }

        protected override void Abort(NativeActivityAbortContext context)
        {
            DestroyHandlers(context);
        }

        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            metadata.RequireExtension<InstanceManagerExtension>();
            metadata.AddDelegate(Body);
            metadata.AddDefaultExtensionProvider(() => new InstanceManagerExtension());
            metadata.AddImplementationVariable(_handlers);

            base.CacheMetadata(metadata);
        }

        protected override void Cancel(NativeActivityContext context)
        {
            context.CancelChildren();

            DestroyHandlers(context);
        }

        protected override void Execute(NativeActivityContext context)
        {
            if (CanExecute() == false)
            {
                return;
            }

            InstanceManagerExtension extension = context.GetExtension<InstanceManagerExtension>();

            ExecuteBody(context, extension);
        }

        private Boolean CanExecute()
        {
            if (Body == null)
            {
                return false;
            }

            if (Body.Handler == null)
            {
                return false;
            }

            return true;
        }

        private InstanceHandler<TH> CreateHandler<TH>(
            ActivityContext context, 
            InstanceManagerExtension extension, 
            InArgument<String> resolutionName, 
            GenericArgumentCount handlerCount, 
            GenericArgumentCount argumentCount)
        {
            if (handlerCount > argumentCount)
            {
                return null;
            }

            String resolveName = resolutionName.Get(context);
            InstanceHandler<TH> handler = extension.CreateInstanceHandler<TH>(resolveName);
            List<Guid> handlerIdList = Handlers.Get(context);

            handlerIdList.Add(handler.InstanceHandlerId);

            return handler;
        }

        private void DestroyHandlers(ActivityContext context)
        {
            InstanceManagerExtension extension = context.GetExtension<InstanceManagerExtension>();
            List<Guid> handlers = Handlers.Get(context);

            handlers.ToList().ForEach(extension.DestroyHandler);
        }

        private void ExecuteBody(NativeActivityContext context, InstanceManagerExtension extension)
        {
            GenericArgumentCount argumentCount = ArgumentCount;
            List<Guid> handlerIdList = new List<Guid>((Int32)argumentCount);

            Handlers.Set(context, handlerIdList);

            InstanceHandler<T1> handler1 = CreateHandler<T1>(context, extension, ResolutionName1, GenericArgumentCount.One, argumentCount);
            InstanceHandler<T2> handler2 = CreateHandler<T2>(context, extension, ResolutionName2, GenericArgumentCount.Two, argumentCount);
            InstanceHandler<T3> handler3 = CreateHandler<T3>(context, extension, ResolutionName3, GenericArgumentCount.Three, argumentCount);
            InstanceHandler<T4> handler4 = CreateHandler<T4>(context, extension, ResolutionName4, GenericArgumentCount.Four, argumentCount);
            InstanceHandler<T5> handler5 = CreateHandler<T5>(context, extension, ResolutionName5, GenericArgumentCount.Five, argumentCount);
            InstanceHandler<T6> handler6 = CreateHandler<T6>(context, extension, ResolutionName6, GenericArgumentCount.Six, argumentCount);
            InstanceHandler<T7> handler7 = CreateHandler<T7>(context, extension, ResolutionName7, GenericArgumentCount.Seven, argumentCount);
            InstanceHandler<T8> handler8 = CreateHandler<T8>(context, extension, ResolutionName8, GenericArgumentCount.Eight, argumentCount);
            InstanceHandler<T9> handler9 = CreateHandler<T9>(context, extension, ResolutionName9, GenericArgumentCount.Nine, argumentCount);
            InstanceHandler<T10> handler10 = CreateHandler<T10>(context, extension, ResolutionName10, GenericArgumentCount.Ten, argumentCount);
            InstanceHandler<T11> handler11 = CreateHandler<T11>(context, extension, ResolutionName11, GenericArgumentCount.Eleven, argumentCount);
            InstanceHandler<T12> handler12 = CreateHandler<T12>(context, extension, ResolutionName12, GenericArgumentCount.Twelve, argumentCount);
            InstanceHandler<T13> handler13 = CreateHandler<T13>(context, extension, ResolutionName13, GenericArgumentCount.Thirteen, argumentCount);
            InstanceHandler<T14> handler14 = CreateHandler<T14>(context, extension, ResolutionName14, GenericArgumentCount.Fourteen, argumentCount);
            InstanceHandler<T15> handler15 = CreateHandler<T15>(context, extension, ResolutionName15, GenericArgumentCount.Fifteen, argumentCount);
            InstanceHandler<T16> handler16 = CreateHandler<T16>(context, extension, ResolutionName16, GenericArgumentCount.Sixteen, argumentCount);

            context.ScheduleAction(
                Body, 
                handler1, 
                handler2, 
                handler3, 
                handler4, 
                handler5, 
                handler6, 
                handler7, 
                handler8, 
                handler9, 
                handler10, 
                handler11, 
                handler12, 
                handler13, 
                handler14, 
                handler15, 
                handler16, 
                OnCompleted, 
                null);
        }

        private void OnCompleted(ActivityContext context, ActivityInstance completedInstance)
        {
            DestroyHandlers(context);
        }

        [Browsable(false)]
        public GenericArgumentCount ArgumentCount
        {
            get;
            set;
        }

        [Browsable(false)]
        public
            ActivityAction
                <InstanceHandler<T1>, InstanceHandler<T2>, InstanceHandler<T3>, InstanceHandler<T4>, InstanceHandler<T5>, InstanceHandler<T6>, 
                    InstanceHandler<T7>, InstanceHandler<T8>, InstanceHandler<T9>, InstanceHandler<T10>, InstanceHandler<T11>, InstanceHandler<T12>, 
                    InstanceHandler<T13>, InstanceHandler<T14>, InstanceHandler<T15>, InstanceHandler<T16>> Body
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName1
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName10
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName11
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName12
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName13
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName14
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName15
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName16
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName2
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName3
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName4
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName5
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName6
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName7
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName8
        {
            get;
            set;
        }

        public InArgument<String> ResolutionName9
        {
            get;
            set;
        }

        private Variable<List<Guid>> Handlers
        {
            get
            {
                return _handlers;
            }
        }
    }
}

The bulk of the code in this class is the definition of the resolution name properties. There is a resolution name property for each of the possible 16 instance resolutions. It defines the ArgumentCount property that determines how many instance resolutions are going to be processed by the activity. The class also defines a Body property holds a reference to the child activity that will be executed with the InstanceHandler references.

The class also maintains a List<Guid> variable that keep track of the all the InstanceHandlerId values created for the activity. This list of values is used to destroy resolved instances using the InstanceManagerExtension when the activity completes, is aborted or is cancelled. Persistence is managed by the InstanceManagerExtension as outlined in the previous post.

The InstanceResolver also implements the IActivityTemplateFactory interface. This interface defines the Create method that allows the activity to provide an activity definition when it is dragged from the toolbox to the designer. This is used to preconfigure an InstanceResolver instance with default values.

One of the issues with the support for generic activities that define more than one generic argument is that the developer gets a prompt for the generic argument types when the activity is dragged onto the design surface.

image

This is a usability issue for the InstanceResolver class as the developer using it will not often need all 16 arguments for their workflow. Unfortunately this implementation forces them to identify a type definition for all 16 arguments as this is what defines the InstanceResolve activity being used.

On a side note, there is a way around this for activity types that define one generic argument. Decorating the activity with the DefaultTypeArgumentAttribute allows you to specify the default type. ForEach<T> and ParallelForEach<T> use this to define the type of int. This is why the developer does not see the above prompt when dropping these activities onto the design surface. Unfortunately this attribute was only designed to support a single generic argument.

The workaround for this usability issue is to use some indirection. Another activity that implements IActivityTemplateFactory can be used to produce this result. This is where the non-generic InstanceResolver class comes into play.

namespace Neovolve.Toolkit.Workflow.Activities
{
    using System;
    using System.Activities;
    using System.Activities.Presentation;
    using System.Diagnostics;
    using System.Diagnostics.Contracts;
    using System.Drawing;
    using System.Windows;

    [ToolboxBitmap(typeof(InstanceResolver), "brick.png")]
    public sealed class InstanceResolver : NativeActivity, IActivityTemplateFactory
    {
        public static String GenerateDisplayName(Type activityType, GenericArgumentCount argumentCount)
        {
            Contract.Requires<ArgumentNullException>(activityType != null);

            Type[] genericArguments = activityType.GetGenericArguments();
            String displayName = "InstanceResolver<" + genericArguments[0].Name;
            Int32 maxArguments = (Int32)argumentCount;

            if (maxArguments == 1)
            {
                return displayName + ">";
            }

            for (Int32 index = 1; index < maxArguments; index++)
            {
                displayName += ", " + genericArguments[index].Name;
            }

            return displayName + ">";
        }

        [DebuggerNonUserCode]
        public Activity Create(DependencyObject target)
        {
            return
                new InstanceResolver
                    <Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object>();
        }

        protected override void Execute(NativeActivityContext context)
        {
            throw new NotSupportedException("This activity is intended to be used as a design time activity only. Use InstanceResolver instead.");
        }
    }
}

This activity is used to create a generic InstanceResolver instance by defining the type of Object for all 16 of the generic arguments. The user will not be prompted to define these types when dropping this activity on the designer. The result as far as the designer is concerned is that the workflow will contain the generic InstanceResolver rather than the non-generic InstanceResolver. The toolbox contains both of these activities so the developer still has a choice about how to get this activity onto the designer.

image

Why was this IActivityTemplateFactory.Create method not implemented in this way in the generic InstanceResolver?

The reason is that IActivityTemplateFactory and its Create method are defined against an instance of the activity rather than as a static method. The Create method on the generic InstanceResolver can only be invoked once all 16 generic arguments have been defined such that the activity instance can be created. Using a non-generic InstanceResolver activity gets around this issue as no generic types need defining before it is dropped onto a designer. The non-generic InstanceResolver is essentially a proxy into the generic InstanceResolver for the design-time experience.

This post has built upon the underlying framework for resolving dependencies in a workflow and has provided the implementation for a custom activity. The next post will look at the designer support for this activity.

Custom Windows Workflow activity for dependency resolution–Part 2

My previous post described the design goals for creating a custom WF4 activity that provides dependency resolution functionality. This post will look at the underlying support for making this happen.

The main issue with dependency resolution/injection in WF is supporting persistence. An exception will be thrown when a workflow is persisted when it holds onto a dependency that is not serializable. The previous post indicated that the solution to this issue is to have the workflow persist the resolution description and explicitly prevent serialization of the resolved instance itself.

The way this is done is via an InstanceHandler<T> class.

namespace Neovolve.Toolkit.Workflow
{
    using System;
    using Neovolve.Toolkit.Workflow.Activities;
    using Neovolve.Toolkit.Workflow.Extensions;

    [Serializable]
    public class InstanceHandler<T>
    {
        [NonSerialized]
        private Boolean _resolveAttemptMade;

        [NonSerialized]
        private T _resolvedInstance;

        internal InstanceHandler(String resolutionName)
        {
            InstanceHandlerId = Guid.NewGuid();
            ResolutionName = resolutionName;
        }

        public T Instance
        {
            get
            {
                if (_resolveAttemptMade)
                {
                    return _resolvedInstance;
                }

                _resolveAttemptMade = true;

                _resolvedInstance = InstanceManagerExtension.Resolve(this);

                return _resolvedInstance;
            }
        }

        public Guid InstanceHandlerId
        {
            get;
            private set;
        }

        public String ResolutionName
        {
            get;
            private set;
        }
    }
}

The InstanceHandler<T> class contains the description of the resolution. This identifies the type of resolution (being <T>) and the name for the resolution. This may by null for default resolutions in Unity. The class also creates a GUID value that identifies a particular instance of this class. This is needed so that instances can be resolved from a cache when the owning activity is either persisted or finalised (completed, aborted or cancelled).

The InstanceHandler also exposes a property for the instance being resolved. It is strongly typed to the generic type argument of T. The serialization of this instance is specifically denied via the NonSerialized attribute on the backing field. The other field used here is a flag that indicates whether the instance resolution has already occurred. This is used in order to support null resolution values.

This class can be serialized at any point and only the definition of how to resolve an instance will be stored. The instance is resolved when the Instance property is referenced rather than when the activity starts. This is done for two reasons.

  1. Lazy loading the instance – resolutions only occur when they are referenced
  2. Supporting resolutions after workflow has persisted and been resumed.

The first reason is merely a beneficial side effect. The second reason listed is a technical restriction that comes into play when a persisted workflow resumes and the Instance property of the handler is invoked. There is no reference to an activity context at this point and no prior code can execute in which the dependency can be resolved.

This class makes a static call out to an InstanceManagerExtension to resolve the instance. The InstanceManagerExtension class is used to abstract the resolution, management and clean up logic for instances requested by the handler class.

namespace Neovolve.Toolkit.Workflow.Extensions
{
    using System;
    using System.Activities.Persistence;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Threading;
    using System.Xml.Linq;
    using Microsoft.Practices.Unity;
    using Neovolve.Toolkit.Threading;
    using Neovolve.Toolkit.Unity;

    public class InstanceManagerExtension : PersistenceParticipant, IDisposable
    {
        private static readonly Dictionary<Guid, Object> _instanceCache = new Dictionary<Guid, Object>();

        private static readonly ReaderWriterLockSlim _instanceSyncLock = new ReaderWriterLockSlim();

        private readonly List<Guid> _handlerIdCache = new List<Guid>();

        private readonly ReaderWriterLockSlim _handlerSyncLock = new ReaderWriterLockSlim();

        private static IUnityContainer _container;

        public static T Resolve<T>(InstanceHandler<T> handler)
        {
            Contract.Requires<ArgumentNullException>(handler != null);

            T resolvedInstance = Container.Resolve<T>(handler.ResolutionName);

            using (new LockWriter(_instanceSyncLock))
            {
                if (_instanceCache.ContainsKey(handler.InstanceHandlerId))
                {
                    throw new InvalidOperationException("InstanceHandler cache is corrupted with a stale instance");
                }

                _instanceCache.Add(handler.InstanceHandlerId, resolvedInstance);

                return resolvedInstance;
            }
        }

        public void DestroyHandler(Guid instanceHandlerId)
        {
            Contract.Requires<ArgumentException>(instanceHandlerId.Equals(Guid.Empty) == false);

            DestroyHandlerByHandlerInstanceId(instanceHandlerId);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        internal InstanceHandler<T> CreateInstanceHandler<T>(String resolutionName)
        {
            Contract.Requires<ObjectDisposedException>(Disposed == false);

            InstanceHandler<T> handler = new InstanceHandler<T>(resolutionName);

            // Store this created handler so that it can be destroyed when the workflow is persisted
            using (new LockWriter(_handlerSyncLock))
            {
                _handlerIdCache.Add(handler.InstanceHandlerId);
            }

            return handler;
        }

        protected virtual void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                // Free managed resources
                if (Disposed == false)
                {
                    Disposed = true;

                    DestroyLocalHandles();

                    _handlerSyncLock.Dispose();
                }
            }

            // Free native resources if there are any.
        }

        protected override IDictionary<XName, Object> MapValues(
            IDictionary<XName, Object> readWriteValues, IDictionary<XName, Object> writeOnlyValues)
        {
            // This method is used to detect when the activity is being persisted
            DestroyLocalHandles();

            return base.MapValues(readWriteValues, writeOnlyValues);
        }

        private void DestroyHandlerByHandlerInstanceId(Guid instanceHandlerId)
        {
            using (new LockWriter(_handlerSyncLock))
            {
                _handlerIdCache.Remove(instanceHandlerId);
            }

            // Get this handler
            using (new LockReader(_instanceSyncLock))
            {
                if (_instanceCache.ContainsKey(instanceHandlerId) == false)
                {
                    return;
                }
            }

            Object instance = null;

            using (new LockWriter(_instanceSyncLock))
            {
                if (_instanceCache.ContainsKey(instanceHandlerId))
                {
                    instance = _instanceCache[instanceHandlerId];

                    _instanceCache.Remove(instanceHandlerId);
                }
            }

            if (instance != null)
            {
                Container.Teardown(instance);
            }
        }

        private void DestroyLocalHandles()
        {
            List<Guid> handleIdList;

            using (new LockReader(_handlerSyncLock))
            {
                handleIdList = new List<Guid>(_handlerIdCache);
            }

            handleIdList.ForEach(DestroyHandlerByHandlerInstanceId);
        }

        public static IUnityContainer Container
        {
            get
            {
                if (_container == null)
                {
                    _container = UnityContainerResolver.Resolve();
                }

                return _container;
            }

            set
            {
                _container = value;
            }
        }

        protected Boolean Disposed
        {
            get;
            set;
        }
    }
}

The InstanceManagerExtension creates InstanceHandler instances, resolve dependencies and tear down dependencies. The extension will resolve a Unity container from configuration if a container has not already been assigned prior to the first dependency being resolved. This allows for a custom container to be provided if that is required but also falls back on a default behaviour.

The extension will first be used by an activity to create an InstanceHandler<T>. This is done so that the handler GUID can be tracked by the extension. Each InstanceHandlerId created is cached in a list stored against the extension instance.

When an activity execution makes a call to the Instance property of the handler, the handler then comes back to the extension to resolve the dependency using the static method. The reason for the static method is that the handler does not have a reference to the activity context from which the extension instance is obtained. The resolved instance is then cached in a dictionary object. Because the resolution process is static, the cache of instances is also static.

The last function of the extension is the ability to tear down the instances it has resolved.

The extension has exposes a public DestroyHandler method that tears down the instance for a specified InstanceHandlerId. The tear down logic will remove the InstanceHandlerId from the instance handler Id cache and then obtain a reference to the resolved instance from the static dictionary cache. This instance is then removed from the cache and torn down using the container.

Tearing down a resolved instance occurs in any of the following circumstances:

  • The workflow completes
  • The workflow is cancelled
  • The workflow is aborted
  • The workflow is persisted
  • The extension is disposed

The custom activity will notify the extension when any of the first three events occur. The custom activity will request the extension to tear down all the InstanceHandlerId values for which it has a reference.

The extension supports persistence by inheriting from PersistenceParticipant and overriding the MapValues method. This method gets invoked as part of a persistence operation. This extension does not provide any custom persistence support, but uses this method as a notification that persistence is executing. At this point the extension instance looks at the instance cache of InstanceHandlerId values that it has created and tears down all the resolved instances.

Finally, the extension implements IDisposable. This will ensure that any resolved instances that are not torn down in any of the above events are correctly disposed when the extension is disposed by the workflow engine.

The InstanceHandler<T> and InstanceManagerExtension classes provide the base framework for resolving and tearing down instances in a workflow execution. The next post will look at a simple custom activity that will leverage these to provide a resolved instance to a workflow execution.

Custom Windows Workflow activity for dependency resolution–Part 1

The previous post talked about the issues with supporting DI in WF4. The nature of Windows Workflow means that there is no true support for DI. Using a custom extension and activity will allow for pulling in dependencies to a workflow while catering for the concerns outlined in the previous post. This series will refer to the dependency injection concept as dependency resolution because this technique is more aligned with the Service Locator pattern than DI.

This first part will go through the design goals of the custom activity.

The custom activity will have the following characteristics:

  • Operate as a scope type activity (like ForEach<T> or ParallelFor<T>)
  • It can hold a single child activity
  • Resolved instance is available to all child activities
  • Resolved instance is strongly typed
  • Support named resolutions
  • Support persistence
  • Dependency should only be resolved when they are referenced
  • Dependency should be torn down when the activity completes
  • Dependency should be torn down when the activity is persisted
  • Dependency must be resolved against when referenced after the activity is resumed from a bookmark
  • Provide adequate designer support for authoring the child activity

The biggest sticking point identified for dependency resolution in a workflow activity is how to deal with persistence. Using a DI container as the mechanism for creating the dependency instance means that there can be no guarantee that the instance is serializable. An exception will be thrown by the workflow engine if a workflow contains a reference to an instance that is not serializable when the workflow is persisted.

There are several scenarios that this implementation needs to cater for regarding persistence.

  1. A workflow execution where an instance is resolved and used without any persistence
    image

    This scenario is reasonably simple. The instance will be resolved when it is referenced. It will be torn down by the DI container (via the extension) when the activity completes.
  2. A workflow execution where instances are resolved and used before and after a bookmark
    image

    This scenario requires that the first resolved instance is torn down when the bookmark causes the workflow to persist. The second instance resolution needs to have enough information to create an instance after the workflow has been resumed from the bookmark. The activity then needs to ensure that the second instance is torn down when the activity completes.
  3. A workflow execution where an instance resolution scope surrounds a bookmark
    image

    As far as the implementation goes, this scenario is actually the same as the second. When the bookmark is resumed, there needs to be some information stored about the instance resolution so that the instance can be recreated again when it is referenced. This instance is then torn down when the activity completes.

The problems of persistence

The central issue with persistence of a dependency in WF is that the dependency instance may not be serializable. The support for persistence in this custom activity is a design issue more than an implementation issue. You may notice that the InvokeMethod activities in the above screenshots make a reference to an Instance property. This indicates how persistence is supported under the covers.

The answer to this problem is to persist the details of the resolution rather than the resolved instance itself. This means that we need to serialize the resolution type and the resolution name for each defined resolution in the workflow. Using a handler class will allow serialization of the resolution definition. It is this class that is provided to the child activity. This handler type specifically identifies that the actual resolved instance is not serializable. Using this handler type also provides the ability to resolve the instance only when it is requested.

The next post will go through the implementation of the handler and the extension classes.