Reacting to ViewState Changes … and finally using AttachedPropertiesService

(The series: This makes Part 7 of a series of posts on flowchart-like freeform layout activity designers, [Part 1Part 2 - Part 3 - Part 4Part 5Part 6 - Part 7 ])

Continuing the series about CanvasActivityDesigner and ICompositeView, I'll focus on an important idea – writing designers which can react to ViewState changes as naturally as they can react to Model changes.

There’s plenty of work to be done to get a really good change notification experience. Let's see why.

Motivation

Earlier in the series we wanted to make ‘edits’ to an Activity, but of data that isn't part of the activity itself. Like an activity's position in a Flowchart. We implemented position data using ViewState and by doing so got the advantage of the ViewState feature's built-in support for Undo/Redo. When we press Ctrl+Z the ViewState change is undone, and the data's previous value is restored. BUT in our designer, our graphics also need to display that change, which we do by updating WPF's visual tree to match the model's state after undo.

Thinking loosely along MVVM lines, the best way to to this looks like building a View which can automatically react to display the ViewModel’s changed state

Memory Jog: ViewState lives outside the Model Tree

The System.Activities.Presentation.Model classes do lots of nice stuff for us. They provide property change notifications and also collection change notifications, via the interfaces INotifyPropertyChanged and INotifyCollectionChanged. This plays very well with WPF bindings. (The workflow designer's AttachedPropertiesService can be used to add extra properties, and these attached properties also play very nicely with WPF bindings.)

The Model classes also automatically track all model changes we make on the undo/redo stack. BUT. The only properties on a ModelItem which support this behavior are the public, get-settable properties of the underlying CLR object. Attached properties do not get automatic model change tracking. Which is one reason attached properties might not work so well for extra X,Y coordinate information for activity positions within a Flowchart or our custom freeform layout designer (i.e. we could need to write a lot of undo/redo code - owch!).

So what if we instead use ViewStateService to add the ‘extra properties’ to our model item? The properties added are stored in a big dictionary (somewhere), and we have to get and set them through calls to ViewStateService.StoreViewStateWithUndo() and so on. Clearly this supports undo. But clearly also the View State is not a ‘real’ property. We can't get access to it through ModelItem.Properties[]. Nor do we receive any PropertyChanged(“ViewState”) notification from the model item itself. Nor do we get a nice WPF binding PropertyPath experience for view state.

The summary:

Core model tree: (ModelProperty etc.) Has automatic Undo/Redo support. Has good WPF binding experience. Only works for public properties of the wrapped CLR object.
View state: Has automatic Undo/Redo support. WPF Binding experience not so good. Creates new properties not found on the wrapped CLR object.
Attached properties: Lacks Undo/Redo support. WPF Binding experience is good, or potentially so. Creates new properties not found on the wrapped CLR object.

We want the best of all three worlds.

 

View State Storage and Changes

How is ViewStateService actually implementing property storage? Here’s a curiosity. The implementation of ViewStateService provided by the designer, WorkflowViewStateService, is built on System.Xaml.AttachablePropertiesService. I was surprised at this because I know there are other classes inside System.Activities.Presentation which implement the idea of AttachedProperties, though slightly different.

The backing behind System.Xaml.AttachablePropertiesService is in theory, some implementation of IAttachedPropertyStore. As far as I can see in practice this is always AttachablePropertiesService.DefaultAttachedPropertyStore, none of which gets us any change notification.

[Diversion: In contrast System.Activities.Presentation.AttachedProperty<T> provides change notification! Store that in your subconscious and continue…]

At this point some reader who actually looked at the ViewStateService will wonder why I am spending all this time looking at how everything is built from the bottom up? ViewStateService has change notification on it already right?

Yes – two events, ViewStateChanged and UndoableViewStateChanged. I am worried though about the usability of these events. If I create 100 designers I don’t want to register every designer to be triggered for global view state notification… It’s a solvable problem, but because I am lazy first I wanted to find out if the framework had already solved it for me.

Giving up on the framework solution wish, let’s have a bash at making some really usable change notifications.

 

Ideal View State Change Handling

In our ideal world, how on the designer side would we like to write the code to handle all of those changes in the activity’s position view state?

Most obvious idea: Register event callback to handle ViewStateChanged notification on every single child model item.

This idea seemed nice and simple. But it got very painful, very quickly. Quickly skim the points of pain:

  • Child model items are added and removed from our designer all the time! Every time a child is added or removed, we need to add or remove our property changed event callback accordingly. This turns into some very ugly code
  • Child views may be added and removed from our designer frequently! For instance if we recreate them every time the collection of child model items changes, then do we need to go and read the view state every time we create the view?!
  • Perf. Unless we a) remove all the property changed event callbacks correctly b) use some kind of WeakReference, we end up with dangling event handlers, bad, bad, bad.

After banging my head on that idea for a couple hours, writing and aborting writing of the efficient property change notification scheme, I realized writing all the imperative event handler code is a world of hurt, and there must be a better way!

A better idea: use WPF Binding to handle all the change notification regtistrations for us

    var view = Context.Services.GetService<ViewService>().GetView(modelItem);                   

    BindingOperations.SetBinding(view, Canvas.LeftProperty,

        new Binding {

            Source = modelItem,

            Path = new PropertyPath("ViewState+CanvasActivity+X")

        });

    BindingOperations.SetBinding(view, Canvas.TopProperty,

       new Binding {

           Source = modelItem,

           Path = new PropertyPath("ViewState+CanvasActivity+Y")

       });

 

WPF is doing weakly-reference event dispatching and avoiding any perf issues for us. Sweet. You’ll see why I’m using a slightly weird looking property path with plus signs soon.

 

The above code should work!

But how?

There is no such property to bind to, right?

Not a problem – with AttachedPropertiesService we can create one!

Attached Properties - System.Activities.Presentation style!

Class reference:

System.Activities.Presentation.Model.AttachedProperty
and,
System.Activities.Presentation.Model.AttachedPropertiesService

Executive summary: Add whatever-the-hell-you-want properties on your ModelItem. With custom getters and setters. And change notification.

And here’s what we can do with it. [Disclaimer: I tried this code out, it seemed to work, but no other quality control has been done to validate it.]

public class ViewStateAttachedProperty : AttachedProperty

{

    public override Type Type

    {

        get { return typeof(object); }

    }

    public override bool IsReadOnly

    {

        get { return false; }

    }

    public ViewStateAttachedProperty(string key, Type ownerType)

    {

        this.IsBrowsable = false;

        this.Name = "ViewState+" + key;

       this.Key = key;

    }

    string Key { get; set; }

    public override void ResetValue(ModelItem modelItem)

    {

        ViewStateService vss = modelItem.GetEditingContext().Services.GetService<ViewStateService>();

        vss.StoreViewStateWithUndo(modelItem, Key, null);

        base.NotifyPropertyChanged(modelItem);

    }

    public override object GetValue(ModelItem modelItem)

    {

        ViewStateService vss = modelItem.GetEditingContext().Services.GetService<ViewStateService>();

        object t = vss.RetrieveViewState(modelItem, Key);

        return t;

    }

    public override void SetValue(ModelItem modelItem, object value)

    {

        ViewStateService vss = modelItem.GetEditingContext().Services.GetService<ViewStateService>();

        vss.StoreViewStateWithUndo(modelItem, Key, value);

        base.NotifyPropertyChanged(modelItem);

    }

    private static Dictionary<string, ViewStateAttachedProperty> cache

        = new Dictionary<string, ViewStateAttachedProperty>();

    private class AddOnceService

    {

        public static ViewStateAttachedProperty Create(string key)

        {

            if (!cache.ContainsKey(key))

            {

                cache.Add(key, new ViewStateAttachedProperty(key, typeof(object)));

            }

            return cache[key];

        }

        public void AddOnce(EditingContext context, string key)

        {

            AttachedPropertiesService aps = context.Services.GetService<AttachedPropertiesService>();

            aps.AddProperty(Create(key));

        }

    }

    public static void Register(EditingContext context, string key)

    {

        if (!context.Services.Contains<AddOnceService>())

        {

            context.Services.Publish(new AddOnceService());

        }

        var addOnce = context.Services.GetService<AddOnceService>();

        addOnce.AddOnce(context, key);

        ViewStateService vss = context.Services.GetService<ViewStateService>();

        vss.UndoableViewStateChanged -= new ViewStateChangedEventHandler(vss_UndoableViewStateChanged);

        vss.UndoableViewStateChanged += new ViewStateChangedEventHandler(vss_UndoableViewStateChanged);

    }

    static void vss_UndoableViewStateChanged(object sender, ViewStateChangedEventArgs e)

    {

        ViewStateAttachedProperty prop;

        if (cache.TryGetValue(e.Key, out prop))

        {

            prop.NotifyPropertyChanged(e.ParentModelItem);

        }

    }

}

And… setting the property value

To use the (wonderful) class above we must Register() the ViewStateAttachedProperty, something like this:

    protected override void OnModelItemChanged(object newValue)

    {

        ModelItem canvasActivity = (ModelItem)newValue;

        ViewStateAttachedProperty.Register(

            canvasActivity.GetEditingContext(), "CanvasDesigner+X");

        ViewStateAttachedProperty.Register(

            canvasActivity.GetEditingContext(), "CanvasDesigner+Y");

        //...

 

and then we become able to set the property, more than likely inside of one of those headache-inducing ModelEditingScopes, like this:

 

 

    using (ModelEditingScope scope = canvasActivity.BeginEdit())

    {

        ModelItem droppedModelItem = canvasActivity.Properties["Children"].Collection.Add(droppedItem);

        ViewStateService vss = Context.Services.GetService<ViewStateService>();

        vss.StoreViewStateWithUndo(droppedModelItem, "CanvasDesigner+X", p.X);

        vss.StoreViewStateWithUndo(droppedModelItem, "CanvasDesigner+Y", p.Y);

        scope.Complete();

    }

 

The thing that makes me really happy? I’m not afraid of that ModelEditingScope any more.

CanvasViewStateAttachedPropertySample.zip