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.
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
There are a few key components here
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:
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 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 :-)
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.
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.
As we might expect, there are 4 properties, and you will see all sorts of properties that describe the properties. A few interesting ones:
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.
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");
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.
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.
Wow, that’s a longer post than I intended. What have we covered:
What haven’t we covered yet
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):
2: ModelTreeManager mtm = new ModelTreeManager(ec);
3: mtm.Load(new Sequence());
4: mtm.Root.Properties["Activities"].Collection.Add(new WriteLine());