Growing an MVVM Framework in 2003, part III—Properties Redux
This post is from a series on my experiences starting to grow an MVVM Framework in .NET 1.1.
* Part I—Event Handlers * Part II – Properties * Part III – Properties Redux * Part IV—Unit Tests * Part V—Reflections and RegretsFull source code can be found in my Google Code repository.
A Change of Plans
Last time I showed how I managed the binding of ViewModel properties to the properties on the View's controls. I promised to talk this time about how the use of the mini-framework affected the testability of the code. I changed my mind—I want to return to the whole properties discussion.
Festering Dissatisfaction
The method I had for binding ViewModel properties to the View worked, but it left a bad taste in my mouth. A few things bothered me about the implementation. Recall that to add a bound property the ViewModel had to have code something like this:
private Property bookListItems;
public string BookListItems
{
get { return bookListItems.AsList(); }
set { bookListItems.Value = value; }
}
I have a couple of problems with this.
- it's pretty chatty
- the client programmer has to know when to use
.AsList()
or not, since strings and bools don't require it - the viewbinding code had to look for the private field, and that just felt gross
Poor man's generics
When I first wrote the code, I was bothered a little by the weaknesses in the property bindings. It wasn't until I wrote about the code here that the suck really started to get to me. And worse, I was unhappy with what I'd wrote. One phrase from the post kept coming back to me:
At this point, I was really missing generics.
What did I mean by that? Why did I miss generics? I hadn't explained that well, even to myself. So I thought about it. What would I do with the generics if I had them? And I thought for a bit longer. Then I had it. I'd make a Property
class to proxy the view's properties—that would tighten up the code and relieve programmers of the burden of knowing when to use .AsList
.
Well, I don't have generics, but I do have Manual Type Creation. That's somewhat less convenient, but it's not like I'm going to need dozens of different property types—3 will do for a start. So I decided to see what I could do with a little Property type hierarchy.
public abstract class Property
{
protected PropertyStorageStrategy storage;
protected Property(PropertyStorageStrategy storage)
{
this.storage = storage;
}
}
public class ListProperty: Property
{
public ListProperty(PropertyStorageStrategy storage): base(storage)
{}
public IList Value
{
get { return (IList) storage.Get(); }
set { storage.Set(value); }
}
}
public class StringProperty: Property
{
// pretty much what you'd expect
}
public class BoolProperty: Property
{
// pretty much what you expected above, only more Bool-y
}
There's not a terrible amount here, just a family of properties. Each concrete class is responsible for providing a Value
property that will return (or accept) a typed value. The real work is done by the storage
member—it keeps track of the untyped value that the concrete class will take or dole out. As the name PropertyStorageStrategy
suggests, a Property can vary the source and sink for its value via the Strategy design pattern.
I was holding it for a friend
Let's look at the storage strategy that defers to a property on another object.
public interface PropertyStorageStrategy
{
object Get();
void Set(object value);
}
public class BoundPropertyStrategy: PropertyStorageStrategy
{
private object obj;
private PropertyInfo propertyInfo;
public BoundPropertyStrategy(object obj, PropertyInfo property)
{
this.obj = obj;
this.propertyInfo = property;
}
public void Set(object value)
{
propertyInfo.SetValue(obj, value, null);
}
public object Get()
{
return propertyInfo.GetValue(obj, null);
}
}
Unsurprisingly, this looks a lot like the BoundProperty
class from last time. After all, the core functionality is pretty much the same. So, inject a BoundProperty into one of ListProperty, StringProperty, or BoolProperty, and we get a strongly-typed proxy for the underlying object.
Tying it together
Of course the new classes required a change to the ViewModel/Model binding code. Locating the ViewModel fields to bind is pretty much the same as it was, except only public fields that derive from Property are considered. The BindFieldToControl becomes the slightly-better named BindPropertyToControl
:
private bool BindPropertyToControl(Control control, FieldInfo field)
{
string controlPropertyName = ControlAttributeName(control, field.Name);
if ( controlPropertyName == null )
{
return false;
}
PropertyInfo controlProperty = control.GetType().GetProperty(controlPropertyName, myBindingFlags);
if ( controlProperty == null )
{
return false;
}
BoundPropertyStrategy strategy = new BoundPropertyStrategy(control, controlProperty);
ConstructorInfo constructor = field.FieldType.GetConstructor(new Type[] {typeof (PropertyStorageStrategy)});
object propertyField = constructor.Invoke(new object[] {strategy});
field.SetValue(this, propertyField);
return true;
}
The first part of the method just makes sure that the control we've found has a name that matches the first part of the property. Then we look for a property on the control that completes the name. Once those hurdles are past, the magic happens:
- create a new BoundProperty to proxy the control's property value
- take the property field type and find the constructor that takes a PropertyStorageStrategy
- make a new property object, passing in our BoundProperty
- set the property object onto the ViewModel
How's it work?
Overall, I think okay. Here's a sample of the ViewModel code.
public StringProperty TitleText;
public BoolProperty FindEnabled;
public ListProperty BookListItems;
public void TitleTextChanged(object sender, EventArgs e)
{
string newText = TitleText.Value;
FindEnabled.Value = (newText != null & newText.Length > 0);
}
public void FindClick(object sender, EventArgs e)
{
ICollection books = bookDepository.Find(TitleText.Value);
IList bookListItems = BookListItems.Value;
bookListItems.Clear();
foreach ( string book in books )
{
bookListItems.Add(book);
}
}
The client developer has to remember to use the funny property types,
but this isn't that much harder than, say Func
. At least the names
make some sense. The .Value
could get a little old, but I prefer
having it on both the get and the set even over just on the set. I
like having the strong-typing built in to the type, rather than
forcing the client developer to do the conversion in a property.
On the downside, additional property types will have to be added to the framework by hand, but that shouldn't come up too often. Also, the storage strategy for the properties is maybe a little complicated, but at least clients of the framework never have to deal with it directly. The observant among you will probably criticize the strategy because so far there's no use for it. Bear with me. Next time I'll show you how the strategy adds value.