Workflow Foundation 4 allows you to add your own custom activities in code. You have a choice of CodeActivity, AsyncCodeActivity and NativeActivity. CodeActivity is ideal if you want to add a simple activity that doesn’t block. AsyncCodeActivity is great if you need an activity that needs to do some lengthy processing and you can support the IAsyncResult pattern. But if you require some lengthy business process that needs to wait for some external input you need a NativeActivity and a Bookmark.
Most samples you can find use the main method of the workflow host to send the required input for the bookmark, but this puts the burden on the host; it is way better to use an activity with an extension. The activity can then delegate the long work to the extension. In this post I will build a simple activity that uses an extension like this.
Start by building a GetNameActivity that derives from NativeActivity:
1: public class GetNameActivity : NativeActivity
2: {
3: public const string GetNameBookmark = "GetName";
4:
5: protected override bool CanInduceIdle
6: {
7: get
8: {
9: return true;
10: }
11: }
12:
13: public OutArgument<string> Name { get; set; }
14:
15: protected override void Execute(NativeActivityContext context)
16: {
17: Bookmark bookmark =
18: context.CreateBookmark(GetNameActivity.GetNameBookmark, BookmarkCompleted);
19: // Done
20: }
21:
22: protected void BookmarkCompleted(NativeActivityContext context, Bookmark bookmark, object value)
23: {
24: context.SetValue(Name, (string)value);
25: }
26: }
This activity works the way you find in a lot of samples. Also note that a NativeActivity that used bookmarks needs to override the CanInduceIdle property to return true. The idea is that in the host you wait for the workflow to go idle, then code it so you ask the user for the name, and then you resume the bookmark. Let’s NOT do this, but instead use a workflow extension.
Add another class called GetNameExtension, implementing the IWorkflowInstanceExtension interface:
1: public class GetNameExtension : IWorkflowInstanceExtension
2: {
3: private WorkflowInstance Instance { get; set; }
4:
5: public void GetName(Bookmark bookmark)
6: {
7: ThreadPool.QueueUserWorkItem(
8: (ignore) =>
9: {
10: Console.Write("Your name please: ");
11: string name = Console.ReadLine();
12: Instance.BeginResumeBookmark(bookmark, name,
13: (ticket) => { Instance.EndResumeBookmark(ticket); }, null);
14: }
15: );
16: }
17:
18: public IEnumerable<object> GetAdditionalExtensions()
19: {
20: yield break;
21: }
22:
23: public void SetInstance(WorkflowInstance instance)
24: {
25: Instance = instance;
26: }
27: }
This class has a simple GetName method, taking the bookmark as an argument. It then starts some background processing to perform a lengthy operation, in this case getting some input from the console. Once it has the input it resumes the bookmark. The GetAdditionalExtensions is implement to return an empty collection (using yield break) and the SetInstance is implemented to store the current workflow instance (although not really required for this example).
Now let’s use the extension. The execute method should now be implemented to retrieve this extension, and then call its GetName method with the bookmark:
1: protected override void Execute(NativeActivityContext context)
2: {
3: GetNameExtension ext = context.GetExtension<GetNameExtension>();
4: if (ext == null)
5: throw new InvalidOperationException("This activity requires a GetNameExtension");
6:
7: Bookmark bookmark =
8: context.CreateBookmark(GetNameActivity.GetNameBookmark, BookmarkCompleted);
9: ext.GetName(bookmark);
10:
11: // Done
12: }
So how do we get this extension installed? There are two ways; first we can install the extension in the host code:
1: WorkflowApplication wf = new WorkflowApplication(new Workflow1());
2: wf.Extensions.Add(new GetNameExtension());
3: wf.Run();
This is almost as bad as having the host resume the bookmark; so what is better? Let’s make the activity create the extension itself; we can do this in the CacheMetadata method:
1: protected override void CacheMetadata(NativeActivityMetadata metadata)
2: {
3: base.CacheMetadata(metadata);
4: metadata.AddDefaultExtensionProvider( () => new GetNameExtension() );
5: }
This way, when the activity gets created, it can install the extension itself.
Using an extension this way is ideal if you have a number of activities that need to talk to some backend system like a database. The extension can then be used to model the connection, while the activity itself models the command. All activities in the workflow instance then share the command…
Don’t worry, the workflow runtime will ensure only one instance of this extension is created for the workflow.