Freigeben über


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

Comments

  • Anonymous
    January 27, 2010
    Hi Tim, Thanks for the great blog post.   I think I understood most of the post (overall very informative), but I have few questions that I hope you could clarify.
  1. If System.Activities.Presentation.Attachedproperty<t> supports change notifications, why not use it rather than view state?  That is, why use view state at all?  Is it because view state supports undo/redo and has a 'built in' story around persistence? I had originally thought the attached properties / view state were an either/or option, but the post implemented an attached property by using the view state service.  I found this really interesting, and was wondering what value the view state service still gives us.
  2. Early in the post, you outlines the benefits of using WPF binding to solve the change notifications problem cleanly, rather than using imperative programming.  I'm not sure if/where he would apply this approach in his ongoing canvas activity sample. Thank you, Notre
  • Anonymous
    January 27, 2010
  1. You're on the right track here. It turns out that while AttachedProperty supports change notifications, it doesn't actually integrate with ModelEditingScope. Basically this is a 'best of both worlds' attempt where we get tight integration with Undo/Redo and PropertyChanged notifications. Since AttachedProperty and AttachedProperty<T> leave it entirely up to you to implement the data storage this lets you do pretty much anything - computing read-only property values on the fly is another option!
  2. It's used to configure the children views wherever they are created - i.e. the Update() method.
  • Anonymous
    January 27, 2010
    Thanks Tim, that answers my first question!   I'm still a little sketchy on the second point.  It would be helpful for me, any maybe other readers, if you could repost a complete example of the modified canvas activity?  Do you have the ability to add attachments to your blog posts?  That's what Kushal Shah has done on his blog, so maybe you're able to do the same as well. Notre

  • Anonymous
    January 27, 2010
    Attachment added!

  • Anonymous
    January 27, 2010
    The comment has been removed

  • Anonymous
    January 29, 2010
    It should get called at least twice for drag within the canvas activity, but I guess you are seeing it even more than twice? What's your current method to see how often it's getting called? Some debugging methods can cause reentrancy that wouldn't normally happen, as I found out today. Tim

  • Anonymous
    February 01, 2010
    I was originally just attaching a debugger to the Devenv.exe process, and putting a breakpoint in the Update method.   Based on your comments, I've revised the code such that I use System.Diagnostics.Trace.WriteLine to write a message whenever Update is called (at the beginning of the method).  I then use the DebugView utility to watch trace output.  When I move an activity within canvas activity, when I release the mouse button, I'm seeing four debug messsages written to the viewer. Thanks, Notre

  • Anonymous
    May 14, 2010
    Hi Tim, This seems to be exactly what I have been searching for. I am trying to use the canvasDesigner to create my own version of the If activity. Basically, I'd like to be able to drop my own activities into the Condition part of the If activity in the same way you can drop a custom activity into the Then or Else sections of the If. This way when using in a rehosted designer, the user won't have to enter VB code. Just drop an activity into it that will get evaluated to true or false. So I created another Designer called DecisionActivityDesigner in the same way as the canvasDesigner, and am trying to nest 3 instances of the CanvasDesigner in it. One instance is for the 'Condition' section of the custom IF activity, another for the 'Then' section, and the third for the 'Else' section. It seems to be ok when running through the rehosted designer until I try to drag an activity into any of the 3 CavasDesigners. I get the following error in the OnDragEnter event: Value cannot be null. Parameter name:Context Any ideas? Thanks

  • Anonymous
    June 01, 2010
    For other readers of Parry's comment, I've answered on the forums as well: but if you don't want arbitrary layout like we are doing here, you are probably much bettter off using WorkflowItemPresenter (or WorkflowItemsPresenter) - then things are suddenly really easy. Tim