Undoable Layout for the Freeform Canvas Activity Designer (Part 6)


This is Part 6 of a series of posts on flowchart-like freeform layout activity designers, [Part 1Part 2Part 3Part 4Part 5 – Part 6  – Part 7]


I’ve started varying the titles. In Parts 1-4 we began developing a freeform layout designer called CanvasDesigner. In Part 5, we found that it was tricky getting positioning of the elements on the canvas implemented in a way that gels nicely with ModelEditingScope and Undo/Redo.


Best practice for using the ModelEditingScope

In our encounters with ModelEditingScope so far here are a few problems we had:



  1. The timing of the ModelCollection.NotifyCollectionChanged event depends on whether or not code runs inside a model editing scope.

  2. ModelEditingScopes do not necessarily complete successfully

And that’s not all. A third complication is that the effect of model editing scopes is non-local. From just looking at any given method definition it is impossible to know whether or not that method is being called within a ModelEditingScope in other code.


And what if it isn’t being called inside a model editing scope now in V1, but will be later when you write V1.1?!


Guidelines for minimizing the above headaches:


When writing code to perform model edits


1) Don’t depend on collection change-notification timing. Think of model collection change notification as an operation which happens at a completely unspecified time – maybe not at all, maybe even right now, synchronously. We just don’t know. It’s about as much fun as a race condition.


Reason: Imagine the code you are writing in function G gets called from function F. You write your code, and it works. Later, a bug is found in function F that requires introduction of a ModelEditingScope to fix. Now your function G is broken.


2) Avoid taking actions with side effects (which are not themselves automatically supported for undo) you would need to revert if things go bad. Consider saving actions you would need to revert if things go bad for execution in the change handler.


Reason: If the code we call threw an exception, it would be nice if we can catch the exception and continue. It’s fine as long as state is not corrupted.


3) Corollary of 2: If we can, the only side-effects that should happen within a ModelEditingScope are:



  • Model Edits

  • ViewState Edits (with Undo)

Reason: As discussed in another post, ModelEditingScope isn’t really extensible.


Guidelines at Work

OK, now that we have some guidelines, let’s see if we can apply them to the code at hand:


protected override void OnDrop(DragEventArgs e)


{


    ModelItem canvasActivity = this.ModelItem;


 


    //droppedItem – may be a ModelItem or a newly instantiated object (from toolbox)


    object droppedItem = DragDropHelper.GetDroppedObject(this, e, this.Context);


    Point p = e.GetPosition(this.ContentCanvas);


 


    using (ModelEditingScope scope = canvasActivity.BeginEdit())


    {


        ModelItem droppedModelItem =


          canvasActivity.Properties[“Children”].Collection.Add(droppedItem);


 


        Debug.WriteLine(droppedModelItem.View != null ? “hasView” : “nullView”);


        Debug.WriteLine(“X,Y {0},{1}”, p.X, p.Y);


 


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


        vss.StoreViewStateWithUndo(droppedModelItem, “CanvasActivity.X”, p.X);


        vss.StoreViewStateWithUndo(droppedModelItem, “CanvasActivity.Y”, p.Y);


        Canvas.SetLeft((UIElement)droppedModelItem.View, p.X);


        Canvas.SetTop((UIElement)droppedModelItem.View, p.Y);


 


        Debug.WriteLine(“Canvas Children Count: {0}”,


          canvasActivity.Properties[“Children”].Collection.Count);


        scope.Complete(); //***


        Debug.WriteLine(“Canvas Children Count: {0}”,


          canvasActivity.Properties[“Children”].Collection.Count);


    }


 


    e.Handled = true;


    DragDropHelper.SetDragDropCompletedEffects(e, DragDropEffects.Move);


    base.OnDrop(e);


}


 


I haven’t just gone crazy with the highlighter here. I wanted to pick out all the lines in danger of violating our guidelines because we are looking at state which depends on whether the model change has been committed or not.


 


This brings up an import special case of guideline #1: Don’t assume the model item you just created has a View!


 


You might guess that using the local variable droppedModelItem is dangerous. What do we know about this object within the model editing scope? Interestingly, it does definitely have a real value. But we don’t know whether it has a parent yet, whether that parent has any children, or in other words whether the ModelItem is joined to the model tree.


 


Part two of our implementation was the collection change notification handler code:


 


void Update(ModelItem canvasActivity)


{


    this.ContentCanvas.Children.Clear();


    foreach (ModelItem modelItem in canvasActivity.Properties[“Children”].Collection)


    {


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


        double x = (double)(vss.RetrieveViewState(modelItem, “CanvasActivity.X”) ?? 0.0);


        double y = (double)(vss.RetrieveViewState(modelItem, “CanvasActivity.Y”) ?? 0.0);


 


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


        Canvas.SetLeft((UIElement)view, x);


        Canvas.SetTop((UIElement)view, y);


        this.ContentCanvas.Children.Add((UIElement)view);


        DragDropHelper.SetCompositeView((WorkflowViewElement)view, this);


    }


}


 


Here the blue code is not so obvious in its violation of the guidelines, but it becomes a violation nonetheless: we don’t know if Update() is called before or after StoreViewStateWithUndo(). Obviously a problem, but how to solve it?


 


The specific approach:


1) It’s just a bug. We should work around the issue by somehow making sure the view state exists before Update() happens. (Specific Solution: Set the view state on the ModelItem before adding the model item to the collection.)


 


The general approach:


2) It’s a problem because we don’t have change notification that works off of the view state. If we had that change notification the order of changes wouldn’t matter, either way we can respond to each change by updating the appropriate aspect of the view. (General Solution: do some work to get change notifications and handle them.)


 


A Solution


 


Only the specific approach will be short enough to fit into this blog post, so here it is. It uses something I think is a cute trick: Creating a ModelItem for an object which hasn’t yet been added to the model tree. Later when the wrapped object is added to the model tree, the item you just created is the one added.


 


protected override void OnDrop(DragEventArgs e)


{


    ModelItem canvasActivity = this.ModelItem;


 


    object droppedItem = DragDropHelper.GetDroppedObject(this, e, this.Context);


    Point p = e.GetPosition(this.ContentCanvas);


 


    ModelItem droppedModelItem =


        droppedItem as ModelItem ??


        Context.Services.GetService<ModelTreeManager>().CreateModelItem(null, droppedItem);


 


    using (ModelEditingScope scope = canvasActivity.BeginEdit())


    {


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


        vss.StoreViewStateWithUndo(droppedModelItem, “CanvasActivity.X”, p.X);


        vss.StoreViewStateWithUndo(droppedModelItem, “CanvasActivity.Y”, p.Y);


 


        ModelItem sameGuy = canvasActivity.Properties[“Children”].Collection.Add(droppedItem);


        Debug.Assert(Object.ReferenceEquals(droppedModelItem, sameGuy));


 


        scope.Complete();


    }


 


    e.Handled = true;


    DragDropHelper.SetDragDropCompletedEffects(e, DragDropEffects.Move);


    base.OnDrop(e);


}


 


One thing about the function we’re calling here CreateModelItem() : always pass null as the first parameter.


 


Note 1: I say ‘always’. Really I mean just go with this advice until someone gives you specific advice to the contrary.


 


Note 2: Creating the model item doesn’t need to be inside of the editing scope. Nor does the creation get reverted by an aborted editing scope. The reason? It’s not actually a model change. Only the following are changes:




  • a change in model property values,


  • a change in the shape of the model tree. Creating something that isn’t connected to anything else in the model tree doesn’t change the shape of the model tree.

Note 3: I don’t expect the Debug.Assert to ever fail. But the semantics do have a small chance of change in future…

Comments (5)

  1. Daniel Fung says:

    I got

    Unable to cast object of type "System.Activities.Statment.If" to type "System.Activities.Presentation.Model.ModelItem"

    wheni tried to drag n drop an If activity to the canvas

    help

  2. Daniel Fung says:

    Ok i figured it out

    ModelItem droppedModelItem =   droppedItem as ModelItem ??
           Context.Services.GetService<ModelTreeManager>().CreateModelItem(null, droppedItem);

    in VB.NET is

    dim droppedModelItem as ModelItem
    if typeOf(droppedItem) is ModelItem then
      droppedModelItem = droppedItem
    else
     droppedModelItem = Me.Context.Services.GetService(Of ModelTreeManager).CreateModelItem(Nothing, droppedItem)
    endif

  3. Daniel Fung says:

    Ok, so i can have two activities on the canvas, how do i connect then,

    i’ve seen examples online for the freeformactivitydesigner and code for GetConnections and Glyphs etc

    i cannot seem to find the same classes in the .net 4.0 classes,

  4. tilovell09 says:

    Hi Daniel,

    Unlike WF 3.x, there is no special API for supporting connectors included the WF 4.0 .net framework classes, and especially nothing analogous to ActivityDesigner.GetConnections().

    WF4.0 doesn’t have any general concept of connections – it is up to the activity designer author to introduce such a concept if desired, or use whatever graphical techniques they like to show the control flow.

    Tim

  5. ike C says:

    Great post,I have a suggestion though  could you post also the complete -working- sample. I cant make it to work and I’m sure I got lost on some of the back and forth , this topic is super coolimportant…  please post the full source code.

    Thanks

Skip to main content