Basics of Working with Custom Types in Workflow
If you have used a custom type for an activity / workflow property you have probably ran into a few issues. You might have had problems trying to get it serialized correctly or to be able to set the value in the property browser. With this post I will try and shed some light on some of the basics. I will be working with the following custom type:
[Serializable()]
public class MyType
{
string paramOne;
string paramTwo;
public MyType(string paramOne, string paramTwo)
{
ParamOne = paramOne;
ParamTwo = paramTwo;
}
public string ParamOne
{
get { return paramOne; }
set { paramOne = value; }
}
public string ParamTwo
{
get { return paramTwo; }
set { paramTwo = value; }
}
}
The default workflow serialization works pretty good for types that have a default constructor. However, if your type is like the one above, which doesn’t, you aren’t going to get what you expect is you use the activity in a code only workflow. If the property has a default value other than null in the designer.cs file you will see something like the following:
this.activity11.MyTypeProperty = ((MyType)(resources.GetObject("activity11.MyTypeProperty")));
And if you are using code separation it will look like the following:
<ns0:Activity1 x:Name="activity11">
<ns0:Activity1.MyTypeProperty>
<ns0:MyType ParamTwo="" ParamOne="" />
</ns0:Activity1.MyTypeProperty>
</ns0:Activity1>
The code separation version looks good, but when you do anything that causes deserialization, like building or closing & reopening the workflow, you will end up with the following error message:
CreateInstance failed for type 'ActivityLibrary1.MyType'. No parameterless constructor defined for this object.
First, I will start with the code only workflow. To get the proper serialization in the designer.cs file you need to have a CodeDomSerializer like the following:
public class MyTypeCodeDomSerializer : CodeDomSerializer
{
public override object Serialize(IDesignerSerializationManager manager, object value)
{
MyType customType = value as MyType;
return new CodeObjectCreateExpression(typeof(MyType), new CodeExpression[] { new CodePrimitiveExpression(customType.ParamOne), new CodePrimitiveExpression(customType.ParamTwo) });
}
}
To link the serializer to the type use the following DesignerSerializerAttribute on the type:
[DesignerSerializer(typeof(MyTypeCodeDomSerializer), typeof(CodeDomSerializer))]
Now, below is what you will get in the designer.cs file:
this.activity11.MyTypeProperty = new ActivityLibrary1.MyType("", "");
For the code separation workflow issue you need to create a WorkflowMarkupSerializer to deserialize the custom type but you will also need to do the serialization. Start by overriding the ToString method on your type. For this example I have the following:
public override string ToString()
{
return string.Format("ParamOne={0}, ParamTwo={1}", this.paramOne, this.paramTwo);
}
This will get your type serialized in a format that you know how to deal with and gives you the side benefit of having the property browser display the value of your type instead of its name. The workflow markup serializer would look like the following:
public class MyTypeSerializer : WorkflowMarkupSerializer
{
protected override bool CanSerializeToString(WorkflowMarkupSerializationManager serializationManager, object value)
{
return true;
}
protected override object DeserializeFromString(WorkflowMarkupSerializationManager serializationManager, Type propertyType, string value)
{
MyType myType = null;
if (!value.Contains("x:Null"))
{
string[] parameters = value.Split(new char[] { ',' }, 2);
if (parameters.Length == 2)
{
string paramOneMatch = "ParamOne=";
string paramTwoMatch = "ParamTwo=";
string paramOneVal = parameters[0].Trim();
string paramTwoVal = parameters[1].Trim();
if (paramOneVal.StartsWith(paramOneMatch, StringComparison.OrdinalIgnoreCase)
&& paramTwoVal.StartsWith(paramTwoMatch, StringComparison.OrdinalIgnoreCase))
{
paramOneVal = paramOneVal.Substring(paramOneMatch.Length);
paramTwoVal = paramTwoVal.Substring(paramTwoMatch.Length);
myType = new MyType(paramOneVal, paramTwoVal);
}
}
}
return myType;
}
}
You link it to the type with the following attribute:
[DesignerSerializer(typeof(MyTypeSerializer), typeof(WorkflowMarkupSerializer))]
Now you should be able to add your custom activity to either a code only or seperated workflow and you should be able build without getting any errors, assuming you don’t have some other validation issue.
You can now add an activity with a custom type to a workflow and build it, but to set the value you have to edit the code or xoml file by manually. The value displayed in the property browser isn’t editable so you can see what it is set to but not change it. To allow the user to edit the value you need to add a TypeConverter. I’ll start with making the string value of the type editable. For this we need to override two sets of methods, one for converting to and one for from. Because we aren’t going to supply a list of values we also need to say we don’t support standard values. What we end up with is the following:
public class MyTypeTypeConverter : TypeConverter
{
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(MyType))
{
return true;
}
return base.CanConvertTo(context, destinationType);
}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (context.PropertyDescriptor.PropertyType.IsAssignableFrom(typeof(MyType)) && destinationType == typeof(string))
{
if (value == null)
{
return string.Empty;
}
else
{
return (value as MyType).ToString();
}
}
return base.ConvertTo(context, culture, value, destinationType);
}
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (context.PropertyDescriptor.PropertyType == typeof(MyType))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
if (context.PropertyDescriptor.PropertyType == typeof(MyType))
{
return Helper.Deserialize(value as string);
}
return base.ConvertFrom(context, culture, value);
}
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
return false;
}
}
You link it to the type with the following attribute:
[TypeConverter(typeof(MyTypeTypeConverter))]
This is nice to be able to quickly edit here, but it isn’t the user experience that most people would expect. If you have more than two or three properties making the property browser wider just isn’t going to work. For this we need to create a separate line in the property browser for each property. You do this my adding the following pair of methods:
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
return true;
}
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
ArrayList properties = new ArrayList();
MyType customType = value as MyType;
if (customType != null && context != null)
{
PropertyDescriptorCollection props = TypeDescriptor.GetProperties(value, new Attribute[] { BrowsableAttribute.Yes });
properties.Add(props["ParamOne"]);
properties.Add(props["ParamTwo"]);
return new PropertyDescriptorCollection((PropertyDescriptor[])properties.ToArray(typeof(PropertyDescriptor)));
}
return base.GetProperties(context, value, attributes);
}
What we have in place now works great if you have a default value that is not null. However, if it is null your user would need to know the correct string format or they would still need to manually edit the code or xoml file. To fix this the final step is to create an editor. Start by creating add a Windows Form to you project that will allow users to set all the needed properties. When you are done with that it is time to create a UITypeEditor and override the EditValue method. This is where you will launch the form and return a new instance of your custom type.
There is a link below that contains this complete project with sample workflows showing the serialization. When you are working on developing this project you may find that the property browser and / or the editor is not behaving as you would expect. The easiest way to resolve this is to do a Build Clean, close VS and reopen the project. It can be a pain but the reason is VS is keeping a reference to an older version of the type.
Comments
Anonymous
July 17, 2006
PingBack from http://microsoft.wagalulu.com/2006/07/17/basics-of-working-with-custom-types-in-workflow/Anonymous
August 13, 2006
WF resourcesAnonymous
October 08, 2006
En estos dias, he ordenado algunos enlaces y recursos sobre el Windows Workflow Foundation, el motorAnonymous
January 05, 2007
I have followed your lead on using custom properties, and can set a value with the designer. But the value is not persisted - if I kick off the workflow containing my custom activity, the value set in the designer is missing. A value for a standard property type (string) is carried over. What might I be doing wrong?Anonymous
January 29, 2007
Without seeing your project there is no way for me to tell. My email address can be found through my profile at http://forums.microsoft.com/MSDN/User/Profile.aspx?UserID=43676&SiteID=1 if you want me to take a look at it.Anonymous
September 11, 2008
This is excellent stuff, however while trying to expand on this I get very stuck. I am trying to create a custom type UI form that will receive it data from a wcf service. However no matter what I try I cannot it to work. I continually get the same error, something along the lines of 'could not find endpoint and contract'. I have tried specifing the details in code, I have tested the service in other applications and it is fine. However calling it in a WF custom type UI is not working. Any ideas on how I could this to work?Anonymous
December 17, 2009
Is it also possible to do a similar trick when you want to have a property that is a non-serializable framework class? I'd like to have a custom activity with a MailAddress property. When I try this I get the error: CreateInstance failed for type 'System.Net.Mail.MailAddress'. No parameterless constructor defined for this object.