Tuesday, 30 September 2014

Fun with ExpressionTextBox

After a short hiatus doing other things (a lot of back-end services work) I’m back in the fray with Workflow.

Yesterday, a colleague asked me to help out with an activity he was building that works like a combination of an If and a ForEach. The basic premise is that we have a List<Something>, and want to iterate through that list until we find a match with a Predicate<Something>, in which case we then schedule a child activity.

So, I came up with the following…

image

Here we have a collection of Noodle’s (as an argument called Stuff), and we’re going to iterate through all elements in the collection until we find the first that matches the predicate (x => x.Name == “Sausages”). When found, we then execute the Body activity, with item as the variable holding the selected Noodle instance.

The activity code is fairly simple…

    public class Find<T> : NativeActivity, IActivityTemplateFactory
{
public InArgument<IEnumerable<T>> Values { get; set; }

public ActivityAction<T> Body { get; set; }

public InArgument<Predicate<T>> Match { get; set; }

protected override void Execute(NativeActivityContext context)
{
var match = this.Match.Get(context);

// Iterate through the collection and if we find a match, we're sorted...
foreach(var o in Values.Get(context))
{
if (match(o))
{
context.ScheduleAction<T>(this.Body, o);
break;
}
}
}

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
...
}

public Activity Create(DependencyObject target)
{
...
}
}



The activity contains a collection of type T, a body that is passed the selected value from the collection, and a predicate that matches against the items in the collection. I’ve also overridden CacheMetadata to setup all of the arguments correctly, and in addition implemented IActivityTemplateFactory as there’s some setup that needs to be done when the activity is created in order to get it to work correctly. Full details are in the code download.


In the Execute method I iterate through the elements in the collection and execute the Match function against each. For the first that matches I then schedule the body activity, passing through the selected element. This then terminates the loop (that’s what we wanted, a more standard implementation might iterate though all matching elements in the collection.


The Designer


The important thing about the code is the designer (and that’s why I wrote the post in the first place). It’s fairly rudimentary, but there is something that might trip you up which is why I wrote this post.


The designer XAML contains a couple of ExpressionTextBox’s as shown in the image below…


image


The first is bound to the Values property on the activity, the second to the Match property. The important thing to note is that in order to get these to work properly you *must* use the ExpressionType property of the ExpressionTextBox…

    <sapv:ExpressionTextBox
Expression="{Binding ModelItem.Values, Mode=TwoWay,
Converter={StaticResource argToExpressionConverter}}"

OwnerActivity="{Binding ModelItem, Mode=OneWay}"
ExpressionType="{Binding ListType}"/>



Now, the type of items in the bound List is based on the type of element you choose to fill the list – in my case I have Noodles. So, the ExpressionType for the list would be IEnumerable<Noodle>, and the datatype for the predicate would be Predicate<Noodle>.


If you don’t use the ExpressionType property of the ExpressionTextBox then the text box will essentially be one-way, it will bind to values you set on the property grid, but won’t allow you to push values the other way (even if those values match 1:1 with what you type in the property grid). So, ExpressionType is mandatory, and has to be the right type.


In order to get the right type, I have created two properties on the Designer, and these call down to the underlying activity to get the actual type necessary. As an example here’s the code for the ListType…

    public Type ListType
{
get
{
if (null == _listType)
{
var findActivity = this.ModelItem.GetCurrentValue();

var valuesProp = findActivity.GetType().GetProperty("Values");

var args = valuesProp.PropertyType.GenericTypeArguments;

_listType = args[0];
}

return _listType;
}
}

private Type _listType;



This code gets the fund activity which is exposed through the ModelItem (which is a proxy object over the activity you are editing). I then lookup the Values property and return the first generic argument – in my example this will be IEnumerable<Noodle>. I have a similar property for the match predicate.


The Code


If you want to have look at the full code please click to download it…

1 comment: