Fixing: An Activity Designer for InvokeAction

Something that I had as a blog post was almost useful to someone. Almost, except for all the rough edges. So here is a second release of the post/code ‘An Activity Designer for InvokeAction<T>’. If you haven’t read it yet, this post will make more sense after having gone through the first one.

Two main Issues with the previously released zipped source code

1) It only works when you load XAML which already has a property reference - it doesn’t work when you drag+drop in a new InvokeAction<T> activity! (InvokeAction<T> activity is not in the VS toolbox by default, but you can add it.) This is a pretty serious limitation, for sure.

2) Usability: having to type the name in the text box is not as usable as being able to pick it from a list of properties the workflow already has

Also there’s a certain scenario Limitation which we don't address today: You can’t use our designer and InvokeAction<T> with an ‘inline’ handler Activity of the style used in ForEach (instead of doing a PropertyRef)

Here’s a screenshot of the first issue:

Could not generate view for InvokeAction`1.

One thing I like about this screenshot is the way that when your Activity Designer fails to instantiate it doesn’t crash VS, or even block you from editing your workflow - instead you just see the problem activity in the context of the workflow.

In this case the call stack is easily traceable from the call stack in the tooltip to the code:

        protected override void OnModelItemChanged(object newItem)

        {

            object invokeActionObj = ((ModelItem)newItem).GetCurrentValue();

            ActivityPropertyReference propRef = ActivityBuilder.GetPropertyReference(invokeActionObj);

 

            this.SourcePropertyName = propRef.SourceProperty;

            //set ETB expression type

            this.ArgumentETB.ExpressionType = invokeActionObj.GetType().GetGenericArguments()[0];

            base.OnModelItemChanged(newItem);

        }

 

Clearly something was null. It seems likely that propRef is null - because a newly created InvokeAction<T> object won’t have any attached properties yet. We could try to solve this by creating the missing propRef at this point. But in a new workflow there might not be any properties to reference yet! (One of the joys of design-time programming.)

 

So instead, let’s just do nothing when propRef is null.

            this.SourcePropertyName = (propRef != null) ? propRef.SourceProperty : null;

 

Next problem: if our workflow has a bunch of properties already, how can we enumerate them, and populate a combo box on the designer?

 

Idea 1: expose an ObservableCollection property on the designer

Here we create our ComboBox and set an ItemsSource to populate our combobox.

<ComboBox ItemsSource="{Binding Path=WorkflowProperties}" SelectedItem="{Binding Path=SourcePropertyName}" />

Notice that the binding Path doesn't have 'ModelItem' in it, we are just binding to a property on the designer object itself. The property is defined like:

        ObservableCollection<string> workflowProperties;

        public ObservableCollection<string> WorkflowProperties

        {

            get {

                if (workflowProperties == null)

                {

                    workflowProperties = new ObservableCollection<string>();

                }

                return workflowProperties;

            }

        }

 

OK, we have a collection, and one which provides ICollectionChanged notifications (for the WPF binding to update the listbox automatically) but I feel worried about this as a solution, because the collection doesn't update itself automatically. Maybe this wasn't such a great solution.

 

Idea 2: Expose the root model item as a property on the designer, and bind directly to the real ModelCollection

This was somewhat hacky thinking. With this idea we bind to the real model collection, and all the automatic updates should happen perfectly, right?

Unfortunately, I hit a bug in the Dev10 release of WF designer, which blew this plan out of the water. The bug itself:

Observed: ModelItemCollection throws ArgumentNullException when you
a) try to add 'null' to it.
b) try to find 'null' in it.

Expected:
a) ModelItemCollection allows you to add null
b) ModelItemCollection returns the index of null in the collection, or -1 if it is not in the collection.

It is actually b) which causes problems here. Every time you update the collection, ComboBox will try to update to find the new current index of the selected item in that collection. If nothing is selected yet, then it actually tries to find the index of 'null' in the collection. D'oh.

Anyway, for interest's sake, the code that doesn't quite work is:

      <ComboBox

            ItemsSource="{Binding Path=RootModelItem.Properties}"

            SelectedValuePath="Name"

            SelectedValue="{Binding Path=SourcePropertyName, Mode=TwoWay}"

        />

 

clip_image002

 

Here’s the ComboBox letting me select as the target of my property path any of the Properties on my Workflow. And a TextBox showing the name of the propertyName selected, which is what needs to be passed into ActivityBuilder.SetPropertyReference().

So far so close, but no cigar.

Well, I still think it's cuter to bind directly to the ModelCollection of properties, than to create a mirroring collection. But since ModelCollection will throw, it seems like our workaround is going to involve creating a new CollectionObject anyway.

Either we can
1) go back to idea 1, and add some change notification listeners, to keep our collection up to do date. We need to write code to synchronize collections based on collection changed notifications.
2) stick with idea 2, and add a converter, which returns a different collection. We need to write code to synchronize collections based on collection changed notifications.

So neither way is really easier than the other. Or fun. I guess the first one saves you writing a converter class. Here is the eventual workaround code I came up with, taking the Converter approach. So far I haven't found the scenario where I need to support any operations other than collection Add and Remove. 

namespace WorkflowConsoleApplication12

{

    public class ModelItemCollectionContentsView : ObservableCollection<object>

    {

        ModelItemCollection collection;

        public ModelItemCollectionContentsView(ModelItemCollection collection)

        {

            this.collection = collection;

            collection.CollectionChanged += new NotifyCollectionChangedEventHandler(collection_CollectionChanged);

            foreach (ModelItem mi in this.collection)

            {

                this.Add(mi.GetCurrentValue());

            }

        }

        void collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)

        {

            if (e.Action == NotifyCollectionChangedAction.Add)

            {

                int index = e.NewStartingIndex;

                foreach (ModelItem mi in e.NewItems.Cast<ModelItem>())

                {

                    this.Insert(index, mi.GetCurrentValue());

                }

                return;

            }

            else if (e.Action == NotifyCollectionChangedAction.Remove)

            {

                int index = e.OldStartingIndex;

                foreach (ModelItem mi in e.OldItems.Cast<ModelItem>())

                {

                    this.Remove(mi.GetCurrentValue());

                }

                return;

            }

            throw new NotImplementedException();

        }

    }

    public class ModelItemCollectionObservableCollectionConverter : IValueConverter

    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

        {

            if (value is ModelItemCollection)

            {

                return new ModelItemCollectionContentsView(value as ModelItemCollection);

            }

            throw new ArgumentException("value must be a ModelItemCollection");

        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

        {

            throw new NotImplementedException();

        }

    }

}

InvokeActionSample2.zip