With a basic understanding of the workflow landscape, we can now examine how ISVs can provide additional value for their products by extending them through the creation and support of modular components. The ultimate goal of this article is to give you an understanding of the extension possibilities that either Visual Studio or SharePoint Designer provide.
The extension options available to us, as we described earlier, are the following:
Although SharePoint Designer supports other constructs (for example, Steps, Lookups, and Variables), they are not available to us to extend or customize. While it might appear that this limits your options, we will show that you will have more than enough opportunity for extension. It might also appear problematic to have three distinct options available to you. But you’ll see that the options are extremely similar and in fact build upon each other.
Let's start by creating a custom activity for use with Visual Studio; the SharePoint Designer action we build later will be a relatively simple extension of this activity. The activity we build will allow workflows to update a proprietary software application via a Web service with status information based upon a document review process that varies from one installation of the product to another.
Introducing the Scenario: Contoso Software
Our scenario involves Contoso Software, a small ISV that produces CaseTrak. CaseTrack is case management software designed specifically for local-government mental health offices to manage information about patients diagnosed with mental or behavioral disabilities. We assume that over the last several years, a near-epidemic level increase in the number of autism diagnoses in young children has put a significant strain on the mental health offices, and has resulted in a decrease in their ability to manage cases effectively.
In this scenario, CaseTrak streamlines much of the case management process. As part of their go-to-market strategy, Contoso has done research and determined that a significant number of mental health offices use Windows SharePoint Services 3.0 to manage documents and provide a central location for employees to access information. Contoso has set a strategic direction for their product to integrate with Windows SharePoint Services. Initial efforts have produced Web Parts and a rich Web service API that can provide read/write interaction between CaseTrak and Windows SharePoint Services.
For their next major product release, Contoso is interested in facilitating integration between CaseTrak and Windows SharePoint Services to support the case-review process. The case-review process as it is exposed through the Web Parts currently is entirely manual. Case managers need to remember to initiate the process and track it throughout its lifecycle. This often causes error and missed deadlines.
Contoso has several requests from customers to make this easier. Their initial design called for a single monolithic workflow controlled entirely by CaseTrak. Every client would follow the same core process. Contoso designed several "extensibility points" into the workflow to allow customers some ability to customize the workflow to their specific needs.
Upon review with several key clients, Contoso realizes that this approach will not work for several reasons:
Different regulations apply to the case-review process and even within those laws, there is a lot of opportunity for offices to handle things slightly differently.
Clients using Windows SharePoint Services are accustomed to its flexibility and interface. Forcing them into a different tool just for the case management process is counterproductive.
IT departments at client locations are also resistant to adding another workflow engine to their environment that they must support
Because of these concerns, Contoso’s product architecture group has proposed delivering a modular set of components that either the client or Contoso’s own professional services division can use to build Windows SharePoint Services workflows to the exact specifications of each client and while still taking full advantage of the power of CaseTrak.
The following sections explore how Contoso does that.
Note: |
|---|
| Another benefit that Contoso will realize from producing custom activities is the ability to have all of their CaseTrak activities adopt a different look and some additional functionality from the rest of the default activities. Their product marketing department sees a high value in extending their brand into this new area. |
Digging into the Scenario
In this scenario, we will be the developer for Contoso Software who is tasked with creating the workflow activity. Before we can really get started writing code, we need to make sure our environment is set up properly. There are numerous resources available on the Web that provide detailed information about setting up a proper SharePoint development environment, so only a simple list of the items you’ll need to have installed follows:
-
Windows Server 2003 or Windows Server 2008
-
Microsoft .NET Framework 3.0
-
Microsoft .NET Framework 2.0
-
Visual Studio 2008
-
Windows SharePoint Server 3.0 or Microsoft Office SharePoint Server 2007
Notice there is no actual requirement for working on a SharePoint platform for this part of our activity because all we are doing here is building a component that operates under WF. As a SharePoint workflow is built upon WF, this means that the component we build will also work in SharePoint Server or Windows SharePoint Server. Item five is on the list just for testing our activity later on.
A WF activity consists of between one and seven individual classes, as follows:
-
ActivityCodeGenerator: Allows you to insert code into the activity during compilation.
-
ActivityDefinition: The primary class of the activity. It controls what the activity actually does and is the only required class. If the other classes are not included in your activity assembly, default values are used.
-
ActivityDesigner: Controls the structural appearance of the activity at design time in Visual Studio for non-theme elements.
-
ActivitySerializer: Provides functionality for custom serialization.
-
ActivityTheme: Controls color and other theme elements of the activity’s appearance in Visual Studio.
-
ActivityToolboxItem: Used in Visual Studio to respond to events triggered when the activity is added to a workflow and also to control the appearance of the activity in the Visual Studio Toolbox.
-
ActivityValidator: Verifies activity properties at both run time and design time.
Because this activity will be part of our packaged product, which clients will see and interact with, we will use most of the classes (specifically, ActivityDefinition, ActivityDesigner, ActivityTheme, ActivityToolboxItem, and ActivityValidator). Remember, though, that the only required class is the default ActivityDefinition class.
Code for the ActivityDefinition Class
The first piece of code to write for our activity is the one required class: ActivityDefinition.
To create the ActivityDefinition class
-
Start Visual Studio, and then create a Workflow Activity Library project (appropriately found under Workflow in the list of project types).
-
Name the project CaseTrak.Activities.
-
After the project opens, rename the default Activity1.cs something more meaningful (for example, StatusUpdater.cs). Visual Studio prompts you to rename all references; click OK.
We will still need to manually rename one entry in the Activity Designer generated code region within the StatusUpdater.Designer.cs file.
-
Before we write any code, in Project Properties set your project to be strong named.
-
Follow your company’s code-signing policies for configuring the proper key.
Note: |
|---|
| We recommend that your company adopt a code-signing policy if one is not already in place. |
-
Unless you will be using some of the capabilities specific to the .NET Framework 3.5, you should also change the Target Framework to 3.0.
Now you can start adding code.
Code for the Execute Method
The first line of code we are going to add is an override of the Execute method.
protected override ActivityExecutionStatus
Execute(ActivityExecutionStatus context)
This method is required because it is called by the WF host (SharePoint Server or Windows SharePoint Services) when it is time to run the activity.
As you can see in the previous method signature, the Execute method returns a value from the ActivityExecutionStatus enumeration, as follows:
-
Canceling. Activity was ended prematurely
-
Closed. Activity has finished its tasks
-
Compensating. Rolling back changes
-
Executing. Activity is still running
-
Faulting. An error has occurred
-
Initialized. Internal use only; you won't use it in your code
In most cases, the method returns a value of ActivityExecutionStatus.Closed.
The function of this activity is to update CaseTrak with the status of a case review process. CaseTrak supports five statuses for case reviews:
-
Open
-
Closed
-
Pending
-
Deferred
-
In Process
As part of the first wave of Windows SharePoint Services integration, your Web service API exposes a CTSetCaseReviewStatus method, which your activity will call. As you have likely guessed, this method handles the process of changing the status of a Case record. It takes the ID string for the Case record and an integer Status value and returns a Boolean indicator as to whether it succeeded.
public bool CTSetCaseReviewStatus(string sCaseID, int iStatus)
The details of that Web service are not pertinent to this article. In your application’s API, you will have any number of Web services available to perform the required functionality. For the sake of completeness, the Web service project is included with the source code that accompanies this article (see Additional Resources).
The core code for the Execute method for this activity is relatively simple.
try
{
ct.CTSetCaseReviewStatus(CaseID, Convert.ToInt32(CTStatus));
}
catch (Exception ex)
{
//See full code listing (http://code.msdn.microsoft.com/modularworkflow) for an error handling event.
}
return ActivityExecutionStatus.Closed;
For now, we leave the catch block empty. The full code listing adds some error handling in the form of an event handler, but we can hold off on that for now. These couple of lines accomplish the work of this activity: Call the Web service (ct is the Web service proxy object, which is declared elsewhere), cast the integer value we received for the iStatus parameter to the appropriate CTStatus enum value, and tell the workflow host (Windows SharePoint Services) that we’re finished.
Code for the Properties
Looking at the previous Web service method call, you can see a reference to two other variables: CaseID and Status. These are simply properties that are a part of our Activity class.
private string _caseID;
[DesignerSerializationVisibilityAttribute
(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
[DescriptionAttribute("Unique identifier of the current Case")]
[CategoryAttribute("CaseTrak Configuration")]
public string CaseID
{
get { return _caseID; }
set { _caseID = value; }
}
private CTStatusType _iStatus;
[DesignerSerializationVisibilityAttribute
(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
[DescriptionAttribute("Status designation of the current Case")]
[CategoryAttribute("CaseTrak Configuration")]
public CTStatusType CTStatus
{
get { return _iStatus; }
set { _iStatus = value; }
}
Notice that the Status property is of type CTStatusType. This is an enumeration of the valid statuses listed earlier.
public enum CTStatusType
{
Open=1,
Closed=2,
Pending=3,
Deferred=4,
InProcess=5
}
A benefit of using enumerations for values is that the activity will automatically render this property as a drop-down list of choices in the Visual Studio Properties window (Figure 5).
Figure 5. Custom Properties added to the activity as seen by developers in Visual Studio
At this point, we have a functionally complete activity that could be used to build a Visual Studio workflow. It exposes two properties that allow the workflow developer to specify the current Case and also the status to set the Case to. Exactly how those values are arrived at is highly dependent upon the client for which this workflow is being built, and presents a good example of why we are exposing the functionality of our application as modular components.
Remember Visual Studio developers will likely be a more demanding audience. Presumably, they are familiar with other activities for building workflows and will expect that our activity provides at least that much functionality. So let’s circle back and enhance our activity.
Event Handlers
Most of the built-in activities support at least one customizable event, simply named MethodInvoking. As the name implies, this event is raised when the activity is invoked, before it actually does its work. Experienced workflow developers will expect that our activity provides the same functionality.
Event handlers are added as dependency properties.
public static DependencyProperty MethodInvokingEvent =
DependencyProperty.Register("MethodInvoking", typeof(EventHandler),
typeof(StatusUpdater));
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[Description("The MethodInvoking event is raised before CaseTrak is updated with the new status.")]
[Browsable(true)]
public event EventHandler MethodInvoking
{
add { base.AddHandler(StatusUpdater.MethodInvokingEvent, value); }
remove { base.RemoveHandler(StatusUpdater.MethodInvokingEvent, value); }
}
With the MethodInvoking dependency property declared and registered, the only thing that remains to do is to raise the method. We do this by adding the following line to the Execute method as the first line, right above the try.
base.RaiseEvent(StatusUpdater.MethodInvokingEvent,this,EventArgs.Empty);
If you look at Figure 5, you’ll see the MethodInvoking property exposed in the Visual Studio Properties window. Whatever method name the developer sets as this property value is now executed before the activity does any other work. The other property shown in Figure 5, CaseTrakError, is another event handler that is included in the full code listing that accompanies this article. It performs a function similar to MethodInvoking except that it allows the workflow developer to specify an event to be called in case an error is returned from CaseTrak.
We now have a functionally complete activity that provides some functionality that workflow developers expect. At this point, we could package and deploy our activity. Instead, we’re going to take a few extra steps to really make our activity shine.
Code for the ActivityValidator
Like most applications, CaseTrak doesn’t respond well to being given information that is not valid or that is incomplete. In fact, it won't work if you attempt to set the status of a case without specifying both the CaseID and the new Status. For this reason, it is important that our activity have functionality that ensures that the workflow developer using it provides a value for the required CaseID and Status properties.
We do this by using the ActivityValidator class.
There is one core method we’ll override in this class, which is appropriately named Validate.
public override ValidationErrorCollection Validate(ValidationManager manager,object obj)
{
ValidationErrorCollection activityErrors = base.Validate(manager, obj);
StatusUpdater ctsu = obj as StatusUpdater;
if ((null != ctsu) && (null != ctsu.Parent))
{
if (ctsu.CaseID == null)
{
activityErrors.Add(ValidationError.GetNotSetValidationError("CaseID"));
}
}
}
The code for this method is still pretty straightforward. It checks the various properties that must have values to see if they contain them. If they do not, it adds a new validation error to the ValidationErrorCollection object for each property that fails validation and returns that collection. The end result of this process is to display a notification to the workflow developer that indicates things are not quite right, as shown in Figure 6.
Figure 6. Validation errors shown to Visual Studio workflow developers
With the ActivityValidator class in place, all we need to do is register it with our ActivityDefinition. We do this by decorating the ActivityDefinition class with an attribute that references our validator class.
[ActivityValidator(typeof(StatusUpdaterActivityValidator))]
Finally, we're ready to enhance the appearance of our activity and make it stand out. We tackle this by using two classes: ActivityDesigner and ActivityDesignerTheme.
Code for the ActivityDesigner and ActivityDesignerTheme
If you're observant, you have already noticed the look of our activity in Figure 6. We show it again in Figure 7, this time with a view of what the activity would like without these customizations. You can decide which one you like better.
Figure 7. Two views of the custom activity: one customized (bottom), one not (top)
Notice the following in the customziations we've made:
-
The customized design is wider to provide room for the CaseTrak logo on the left, and ensures that the name of the activity doesn't wrap onto multiple lines in the workflow designer the way the default activities does. A minor point, perhaps, but it does give our activity a more professional look.
-
The addition of the CaseTrak logo (Figure 7a) helps to tie all of the CaseTrak activities together.
-
The red and black theme of the CaseTrak logo is carried into the activity background.
-
Although less noticeable, we have also changed the placement of the text and image within the activity.
These elements all combine to produce an activity that is more visually appealing than the default activities. Add to this the fact that all CaseTrak activities can easily be made to use this look, and you have something that your product marketing department will take notice of.
Accomplishing this appearance is made quite simple by using two distinct classes. Let's look at the simpler of the two first: ActivityDesignerTheme. This class is primarily responsible for the fonts and colors used to draw the activity. All of the work in this class takes place in the constructor.
public CaseTrakActivityDesignerTheme(WorkflowTheme theme): base(theme)
{
BackColorStart = Color.Red;
BackColorEnd = Color.Black;
BackgroundStyle = System.Drawing.Drawing2D.LinearGradientMode.ForwardDiagonal;
ForeColor = Color.White;
}
Easily enough, all we do here is set our foreground and background colors and the background style.
We achieve the rest of our enhanced appearance by using the ActivityDesigner class. This class is responsible for the mechanics of the Activity Design: the size and placement of the rectangles that bound the activity and the text and image contained within the activity.
The first method we look at is OnLayoutSize, which sets the overall dimensions of the activity.
protected override Size OnLayoutSize(ActivityDesignerLayoutEventArgs e)
{
base.OnLayoutSize(e);
return new Size(150, 45);
}
As you can see, it simply returns a new Size construct with the dimensions we want; in this case, 150 pixels wide and 45 pixels high (the default activities render at 91 pixels by 43 pixels.)
After the overall size of the activity is set, the rest of the display pieces are controlled by two distinct rectangles within the activity frame: ImageRectangle and TextRectangle.
protected override Rectangle ImageRectangle
{
get
{
Rectangle rectActivity = this.Bounds;
Size size = new Size(20, 20);
Rectangle rectImage = new Rectangle();
rectImage.X = rectActivity.Left + 5;
rectImage.Y = rectActivity.Top + ((rectActivity.Height - size.Height) / 2);
rectImage.Width = size.Width;
rectImage.Height = size.Height;
return rectImage;
}
}
protected override Rectangle TextRectangle
{
get
{
Rectangle rectActivity = this.Bounds;
Size size = new Size(130, 40);
Rectangle rectText = new Rectangle();
rectText.X = this.ImageRectangle.Right + 5;
rectText.Y = rectActivity.Top + 2;
rectText.Size = size;
return rectText;
}
}
Each of these methods merely performs some simple calculations to determine the placement of the Image and Text within the activity.
The final piece of this class is responsible for adding the CaseTrak logo into the activity. We take care of this in the Initialize method of the ActivityDesigner class.
protected override void Initialize(Activity activity)
{
base.Initialize(activity);
Bitmap img = CaseTrak.Activities.Properties.Resources.CaseTrakIcon16;
this.Image = img;
//Adding our custom Verb.
verbAbout = new ActivityDesignerVerb(this, DesignerVerbGroup.Edit,
"About", new EventHandler(ShowAboutHandler));
Verbs.Add(verbAbout);
}
The JPEG used for the logo is loaded into the Activity assembly as an embedded resource.
You likely noticed one additional thing in the Initialize method: the ActivityDesignerVerb. Figure 8 shows the end result of these two lines of code.
Figure 8. Adding options to the Activities context menu
The code for the ShowAboutHandler event handler is simple.
private void ShowAboutHandler(object sender, EventArgs e)
{
MessageBox.Show("CaseTrack(tm) Status Updater Workflow Activity.\r\n\r\n (c) 2007, Contoso Software, Inc.\r\n", "About",MessageBoxButtons.OK, MessageBoxIcon.Information);
}
This is a nice way to add some help text or, as we do here, a copyright message to a custom activity. There are countless other uses for activity verbs, ranging from launching custom wizards to configure activities to interacting directly with your core application to extract test data—whatever your business needs dictate.
With that, all of the code is in place. All we need to do is tie the pieces together. Again, we do that by decorating our classes with attributes.
Add the following attribute to the top of the ActivityDesigner class to tell it which Theme class to use.
[ActivityDesignerTheme(typeof(CaseTrakActivityDesignerTheme))]
Now, add the following attribute to the ActivityDefinition, either before or after the Validator attribute we added previously.
[Designer(typeof(StatusUpdaterDesigner))]
That takes care of jazzing up the look of our activity on the workflow designer palette. We now have just one more place where we can add some pizzazz: the Visual Studio toolbox.
Code for the ToolboxItem Class
As the name implies, the ToolboxItem class is responsible for the presentation and actions of our activity on the Visual Studio Toolbox. Like the DesignerTheme class, the action here takes place in the constructor.
private StatusUpdaterToolboxItem(SerializationInfo info, StreamingContext context)
{
this.Deserialize(info, context);
this.Description = "Update Case Status in CaseTrak";
this.Company = "Contoso Software, Inc.";
this.DisplayName = "CaseTrakStatus Updater";
this.Bitmap = new Ã
Bitmap(CaseTrak.Activities.Properties.Resources.CaseTrakIcon16);
}
We need to tie our ToolBoxItem back to our activity and as shown previously, we do this with an attribute added above the ActivityDefinition class declaration.
[ToolboxItem(typeof(StatusUpdaterToolboxItem))]
As you might have guessed, the result of this customization is to add some information about our activity when it is displayed in the Visual Studio Toolbox, as shown in Figure 9.
Figure 9. Adding copyright and other information to the activity for display in the Toolbox
Note: |
|---|
| For this class, the image file loaded into the Toolbox must be 16 pixels by 16 pixels and 256 colors. |