Freigeben über


Deep Dive into the WF4 Designer Data Model: ModelItem and ModelProperty

In this post I published an overview of the designer architecture.  I’ll copy the picture and the description of what I want to talk about today.

image

There are a few key components here

  • Source
    • In VS, this is xaml, but this represents the durable storage of the “thing” we are editing
  • Instance
    • This is the in memory representation of the item being edited.  In vs2010 for the WF designer, this is a hierarchy of System.Activities instances (an object tree)
  • Model Item Tree
    • This serves as an intermediary between the view and the instance, and is responsible for change notification and tracking things like undo state
  • Design View
    • This is the visual editing view that is surfaced to the user, the designers are written using WPF, which uses plain old data binding in order to wire up to the underlying model items (which represent the data being edited).
  • Metadata Store
    • This is a mapping between type and designers, an attribute table for lack of a better term.  This is how we know what designer to use for what type

Motivating the ModelItem tree

One observation that you could make when looking at the diagram above is “I should just be able to bind the view to the instance.”  This approach could work, but has a couple of implementation problems:

  • It is unrealistic to expect that instances of runtime types will all be built to fit perfectly into a design oriented world.  The most obvious problem is that our activity types aren’t going to inherit from DependencyObject, or implement INotifyPropertyChanged.  We can’t specify that all collections are ObservableCollection  [interesting trivia, ObservableCollection has moved from WindowsBase.dll into System.dll].  If we could, that would make life easy, but that’s not the case.  
    • Additionally, there are design time services that we need to think about supporting (such as Undo/Redo), and we need to make sure we can consistently handle this across many object types, including those that have not been written by us.
  • There may be a case for not actually operating on a live instance of the object graph.  Note, in VS2010, we do, but if we want to do work that would enable a design time XAML experience, we would need our instance to actually contain a lot of information about the source document. 
  • If we go directly from the view to the instance, we tightly couple the two together, and that makes doing more interesting things in the future tricky.  For instance, if we want to add refactoring support to update instances of objects, we need more than just the object graph to do that (the model item tree also keeps track of things like back pointers, so I know everybody that references the object). 

These reasons cause us to think about an abstraction we can use to intermediate the implementation details of the instance and the view with a common layer.  If you have programmed at the WPF designer extensibility level, you will likely be familiar with the idea (and some of the types) here.

The ModelItem tree

The way that I think about the ModelItem/ModelProperty tree is that it forms a very thing proxy layer on top of the shape of the instance being edited. 

Let’s start with a very simple type:

 public class Animal
{
    // simple property
    public string Name { get; set; }
    // complex property 
    public Location Residence { get; set; } 
    // list 
    public List<Animal> CloseRelatives { get; set; }
    // dictionary
    public Dictionary<string, object> Features { get; set; } 
}

public class Location
{
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public string State { get; set; } 
}

Ignore for a moment that I just gave an animal Features, I’m a PM, it’s how we think :-)

Now, let’s create some instances of that, and then actually create a ModelItem.

    1:  EditingContext ec = new EditingContext();
    2:  var companion1 = new Animal { Name = "Houdini the parakeet" };
    3:  var companion2 = new Animal { Name = "Groucho the fish" };
    4:  var animal = new Animal 
    5:                   {
    6:                       Name = "Sasha the pug",
    7:                       Residence = new Location 
    8:                       {
    9:                           StreetAddress = "123 Main Street",
   10:                           City = "AnyTown",
   11:                           State = "Washington"
   12:                       },
   13:                       Features = { 
   14:                          {"noise", "snort" },
   15:                          {"MeanTimeUntilNaps", TimeSpan.FromMinutes(15) }
   16:                       },
   17:                       CloseRelatives = { companion1, companion2 } 
   18:                   };
   19:  ModelTreeManager mtm = new ModelTreeManager(ec);  mtm.Load(animal);
   20:  ModelItem mi = mtm.Root;

One thing to note here is that I am using ModelTreeManager and EditingContext outside the context (no pun intended) of the designer (see lines 1, 19, and 20 in the above snippet).  This isn’t the usual way we interact with these, but it’s for this sample so that we can focus just on the data structure itself. [as an aside, my brother did have a parakeet named Houdini]. 

Let’s take a quick look at a visualization of what the data structure will look like.  Remember to think about the ModelItem tree as a thin proxy to the shape of the instance.

Rather than spend an hour in powerpoint,  I’ll just include a sketch :-)

IMAG0111

On the left, you see the object itself.  For that object, there will be one ModelItem which “points” to that object.  You can call ModelItem.GetCurrentValue() and that will return the actual object. If you look at the ModelItem type, you will see some interesting properties which describe the object.

  •  ItemType is the type of the object pointed to (in this case, Animal)

  • Parent is the item in the tree which “owns” this model item (in the case of the root, this is null)

  • Source is the property that provided the value (in the case of the root, this is null)

  • Sources is a collection of all the backpointers to all of the properties which hold this value

    • Note, the distinction between Source and Sources and Parent and Parents is a topic worthy of another post
  • View is the DependencyObject that is the visual (in teh case above, this is null as there is no view service hooked into the editing context)

  • Properties is the collection of properties of this object

Properties is the part where things get interesting.  There is a collection of ModelProperty objects which correspond to the shape of the underlying objects.  For the example above, let’s break in the debugger and see what there is to see.

image

As we might expect, there are 4 properties, and you will see all sorts of properties that describe the properties.  A few interesting ones:

  • ComputedValue  is a short circuit to the return the underlying object that is pointed to by the property.  This is equivalent to ModelProperty.Value.GetCurrentValue(), but has an interesting side effect that setting it is equivalent to SetValue().  
  • Name, not surprisingly, this is name of the property
  • PropertyType is the type of the property
  • Collection and Dictionary are interesting little shortcuts that we’ll learn about in a future blog post.
  • Value points to a model item that is in turn the pointer to the value
  • Parent points to the ModelItem which “owns” this property

As Value points to another model item, you can see how this begins to wrap the object, and how this can be used to program against the data model.  Let’s look at a little bit of code. 

 root.Properties["Residence"].
                Value.
                Properties["StreetAddress"].
                Value.GetCurrentValue()

You might say “hey, that’s a little ugly”  and I have two bits of good news for you.

  1. ModelItem has a custom type descriptor, which means that in WPF XAML we can bind in the way we expect (that is, I can bind to ModelItem.Location.StreetAddress, and the WPF binding mechanism will route that to mi.Properties[“Location”].Value.Properties[“StreetAddress”].  So, if you don’t use C# (and just use XAML), you don’t worry about this
  2. In RTM, we will likely add support for the dynamic keyword in C# that will let you have a dynamic object and then program against it just like you would from WPF XAML.  It’s pretty cool and I hope we get to it, if we do I will blog about it.

Here’s a set of tests which show the different things we’ve talked about:

 

    1:  ModelItem root = mtm.Root;
    2:  Assert.IsTrue(root.GetCurrentValue() == animal, "GetCurrentValue() returns same object");
    3:  Assert.IsTrue(root.ItemType == typeof(Animal),"ItemType describes the item");
    4:  Assert.IsTrue(root.Parent == null,"root parent is null");
    5:  Assert.IsTrue(root.Source == null, "root source is null");
    6:  Assert.IsTrue(((List<Animal>)root.Properties["CloseRelatives"].ComputedValue)[0] == companion1, 
    7:             "ComputedValue of prop == actual object");
    8:  Assert.IsFalse(((List<Animal>)root.Properties["CloseRelatives"].ComputedValue)[0] == companion2, 
    9:             "ComputedValue of prop == actual object");
   10:  Assert.AreEqual(root.Properties["Residence"].
   11:      Value.
   12:      Properties["StreetAddress"].
   13:      Value.GetCurrentValue(), "123 Main Street", "get actual value back out");
   14:  Assert.AreEqual(root, root.Properties["Residence"].Parent, "property points to owner");
   15:  ModelItem location = root.Properties["Residence"].Value;
   16:  Assert.AreEqual(root.Properties["Residence"], location.Source, "sources point to the right place");

Oh my, won’t this get explosively large?

Good question, and the truth is that yes, this could get large as you were to spelunk the object graph.  The good news is that we’re incredibly lazy about loading this, we will only flush out the properties collection on demand, and we won’t generate a ModelItem until it is requested.  When we combine this with the View virtualization work we have done, we will only ever load as much in the WF designer as you need.   This keeps the overhead minimal, and in does not represent a substantial memory overhead.

Why should I be careful about ModelItem.GetCurrentValue()

One might be tempted to just say “Hey, I’m in C#, I’ll just call GetCurrentValue() and party on that.  If you do that, you are entering dangerous waters where you can mess up the data model.  Since the underlying instance doesn’t likely support any change notification mechanism, the model item tree will get out of sync with the underlying instance description.  This will manifest itself in problems at the designer because our serialization is based off the instance, not the ModelItem tree (note, that’s a vs2010 implementation detail that could change in a subsequent release).  The net result though is that you will get your view out of sync with your instance and serialization and that’s generally considered problematic. 

Summary

Wow, that’s a longer post than I intended.  What have we covered:

  • The Modelitem tree and why we need it
  • The relationship between the underlying instance and ModelItem/ModelProperty
  • The shape and use of ModelItem / ModelProperty
  • Imperative programming against the tree

What haven’t we covered yet

  • ModelItemCollection and ModelItemDictionary
  • How to use Sources and Parents to understand how the object sits in the graph and manipulation we can do for fun and profit

I’ll get there.  In the meantime, if you have questions, let me know.

 

**** Minor update on 10/29 to fix a bug in the code ****

This is the way to use ModelTreeManager to generate ModelItems (Line 3 is the critical piece that was missing):

    1:  EditingContext ec = new EditingContext();
    2:  ModelTreeManager mtm = new ModelTreeManager(ec);
    3:  mtm.Load(new Sequence()); 
    4:  mtm.Root.Properties["Activities"].Collection.Add(new WriteLine());