Friday, July 02, 2010

ViewModel in .NET

[Changed very little from a sizeable post to PADNUG today:]

For observable objects like view models, I usually have a base class
that provides INotifyPropertyChanged and IDisposable. Change-tracked
properties are held in a PropertyBag, which is a souped-up Dictionary
that maps properties to backing values. It detects changes, so can invoke PropertyChanged intelligently.

Indeed, if you want to get elaborate, and are comfortable with generics, there's quite a bit that the base class can do for you. A nice pattern is

abstract class ViewModel<TViewModel> where TDerived : ViewModel<TViewModel>
{}

class ConcreteViewModel: ViewModel<ConcreteViewModel>
{}


This lets your base ViewModel class know the type of its subclass. Once it has that, it can do slick things with lambdas, like

protected TProperty Get<TProperty>(Expression<Func<TViewModel, TProperty>> propertyLambda)
{
return mPropertyBag.Get(propertyLambda);
}


and

// the return value is true iff the property changed
protected bool Set<TProperty>(Expression<Func<TViewModel, TProperty>> propertyLambda, TProperty value)
{
return mPropertyBag.Set(propertyLambda, value);
}


That's quite a mouthful, but when you use it, the generic parameters become implicit, so the syntax is just:

public IValueConverter EventDateConverter
{
get { return Get(x => x.EventDateConverter); }
private set { Set(x => x.EventDateConverter, value); }
}


It's not quite as simple as automatic properties, but it's the same number of lines; you don't have to declare a backing variable for each property. And, you get change tracking, lazy loading, and INotifyPropertyChanged. It's (reasonably) safe from problems if you rename your properties, because the lambda are compile-time checked. I say "reasonably" safe b/c you can still screw it up if you do something like:

public IValueConverter QuantityConverter
{
get { return Get(x => x.EventDateConverter); } // oops, now the lambda points to the wrong property
private set { Set(x => x.EventDateConverter, value); }
}


There's a dozen little tweaks you can do. For example, I've overloaded the Set() to accept a MethodBase, which allows the syntax

public IValueConverter QuantityConverter
{
get { return Get(x => x.EventDateConverter); }
private set { Set(MethodBase.GetCurrentMethod(), value); }
}


In other words, you never get copy and paste errors from the setter. 50% reduction in risk surface area.

Other things:
* Make PropertyBag smart enough to notice when it's got an IList<> or IEnumerable<>; in that case, the default values are empty collections, not null ones. For assemblies involve in UI, I've got an ObservablePropertyBag specialization that knows to initialize ObservableCollections, too.
* Give the base class two generic parameters: one for the concrete subclass, and one for an interface that the subclass implements. If you define the lambda expressions for Get() and Set() in terms of the interface, the lambdas in your properties won't compile until you put the properties on the interface. In other words, the compiler will force you to keep your interfaces up to date. It's not for everyone, but I figure if I'm going to work in a statically typed language (haven't moved to C#4 yet), I might as well make the compiler work for me.
* I don't show an example here, but since Set() returns a boolean indicating whether the value changed, you can call it as an argument to an if statement and do additional processing. Note that by the time Set() returns, the INotifyPropertyChanged event has been raised. If you want to do pre-processing before an event, you can register the pre-processor during construction. (That's kinda hacky; AFAIK, there's no contract that the framework will maintain the first-registered, first-notified order on event subscribers. But it's been true so far.)

Using generics this heavily isn't for everyone. My actual ViewModel base looks like

public abstract class ViewModelBase<TInstance, TViewModel, TView> : ViewModelBase, IViewModel<TViewModel, TView>
where TInstance : ViewModelBase<TInstance, TViewModel, TView>, TViewModel
where TViewModel : class, IViewModel<TViewModel, TView>
where TView : class, IView<TViewModel>


before you get to the opening bracket!

In sum, this can be a moderately deep topic if you want it to. I didn't even touch on keeping the PropertyBag threadsafe...

No comments: