Monday, June 16, 2008

Domain Specific Language (DSL) with Workflow, Part 2

In the previous article I presented some basic ideas for creating DSL activities in Workflow. In this part I want to deep into the challenges I faced during the implementation and share the solutions. As usual, let's start from the requirements:

  1. DSL is a set of services, each one implemented by a separate function.
  2. For each such function provide a specialized Activity.
  3. All the return/out/ref parameters should work as expected.
  4. Developing of those Activities should be simple and provide Activity validation.

The first requirement is easy - I can have a separate method for each DSL service in my ExternalDataExchange interface:

// The ExternalDataExchange attribute is a required attribute 
// indicating that the local service participates in data exchange with a workflow
[ExternalDataExchange]
public interface ITaskService
{
int CreateTask(string taskId, string assignee, ref string text);

//...
}


To meet the next requirements I wanted to provide a base class, that by simple derivation from it and decorating my properties with some attribute, the desired DSL activity will be created:



[ToolboxItemAttribute(typeof(ActivityToolboxItem))]
public class CreateTask : DslActivity<CreateTask>
{
// Properties on the task
public static readonly DependencyProperty ReturnValueProperty = DependencyProperty.Register("ReturnValue", typeof(System.Int32), typeof(CreateTask));
public static readonly DependencyProperty AssigneeProperty = DependencyProperty.Register("Assignee", typeof(System.String), typeof(CreateTask));
public static readonly DependencyProperty TaskIdProperty = DependencyProperty.Register("TaskId", typeof(System.String), typeof(CreateTask));
public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(System.String), typeof(CreateTask));

public CreateTask()
// Map the activity to the service method
: base(typeof(ITaskService), "CreateTask")
{
}

// Property, mapped to the return value of the method
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
// "Well Known" name for the return value
[DslParameter(Name = "(ReturnValue)")]
[Category("ReturnValue")]
public int ReturnValue
{
get
{
return ((int)(base.GetValue(ReturnValueProperty)));
}
set
{
base.SetValue(ReturnValueProperty, value);
}
}

// Property, mapped to the 'assignee' parameter of the method
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
[BrowsableAttribute(true)]
[DslParameter(Name = "assignee")]
[Category("Parameters")]
public string Assignee
{
get
{
return ((string)(base.GetValue(AssigneeProperty)));
}
set
{
base.SetValue(AssigneeProperty, value);
}
}

// Other properties - mappings to the rest method parameters


In this way development of DSL activities is simple and straightforward. Now let's look into the base class to see how it's achieved. All the magic is done inside the constructor:



// Initializes parameters by reflecting the typeof(T) members
// and looking for DslParameterAttribute attribute.
private static readonly IList<ParameterInfo> _parameters = GetParameters(typeof(T));

public DslActivity(Type interfaceType, string methodName)
{
// Set up the interface and method
InterfaceType = interfaceType;
MethodName = methodName;

for (int i = 0; i < _parameters.Count; i++)
{
ParameterInfo pi = _parameters[i];
// This is the main 'trick' - create a binding to 'this' Activity
// with path set to the relevant property name
ActivityBind bind = new ActivityBind("/Self", pi.Path);
WorkflowParameterBinding binding = new WorkflowParameterBinding(pi.ParameterName);
binding.SetBinding(WorkflowParameterBinding.ValueProperty, bind);
ParameterBindings.Add(binding);
}
}


That's all, hope you enjoyed! Download here the sample code for this article.

1 comment:

Noor Wood said...

the code review methodology presenting with the coding is very well versed and i like the way things written in concise way. Good one.