(This used to be an HTML form but CodeProject wouldn't render it.)
Suppose that the "serial number" box should be disabled if the user does not have a bike. Traditionally, we would write an event handler for when the value of the checkbox changes, and update the Enabled property when it does. Some code bases would also have code when the current person changes, and we must also handle the situation that the list of persons is empty.
GUI-specific data binding can solve this particular problem most of the time; typically you can bind the Enabled property on the serial number box to the value of the checkbox. However, it might not handle the case that no person is selected. And there are many other scenarios where data binding is insufficient:
- Suppose we would like the ListBox of persons to show a summary of each person (based on multiple properties of the person)--well, traditional data binding can't aggregate different properties of an object.
- Suppose we have an options dialog box that controls the appearance of the main window, we would like any changes to the options to immediately alter the main window's appearance--well, traditional data binding doesn't work across different windows.
- Suppose we are showing a shopping cart and we'd like to keep a label up-to-date that shows the total number of items and the total price--well, data binding can't aggregate properties of multiple objects.
- Suppose we'd like to display a certain panel only if the application is running in "administrator" mode, AND an item is selected, AND a check box is checked--well, traditional data binding can't derive a value from multiple unrelated sources of information.
- Suppose we'd like to filter a list based on a user's search string--in general, data binding doesn't help with filtering.
In these cases, if data binding helps at all, it requires you to jump through hoops, or add event handlers for edge cases, or at least keep dependencies in mind when you invoke your INotifyPropertyChanged events. For example, suppose you're using WPF and your ListBox is bound to a Person.ListLabel property that returns a summary string with the name and rank of a Person. If nothing else, you'll have to make sure you raise the PropertyChanged event for "ListLabel" when the name or rank is changed. That is, the code for the Name and Rank properties must explicitly state that ListLabel depends on them. (Now, you can avoid creating the ListLabel property by using a DataTemplate, but if you've programmed WPF then you have probably encountered the need to fire multiple PropertyChanged events for a single property change.)
Ideally, the actual code of a program should be closely related to the behavior rules it is supposed to follow. More generally, the solution space should resemble the problem space; your code should express your business rules. Usually, behavior rules can be described in a simple way:
- "The serial number field is enabled only if 'Has a bike' is checked and a person is selected".
- "The list box will show the name and rank of each person."
- "The font size of the main dialog is to match the font size selected on the options dialog."
- "In the shopping cart, the total price is shown under the list of items."
- "The list only shows items that contain the text typed in the 'Filter' box."
Introducing Update Controls
In a University undergrad project I experimented with starting to solve this problem, using a concept I called "propagating variables". I didn't entirely figure out how to make it work, though. So I was delighted to find that someone had solved this problem in .NET land, with a framework called Update Controls. Update Controls is a different way of programming, so it takes some practice, but I am delighted with how it streamlines my code.UpdateControls not only keeps your GUI up-to-date, but it also separates the GUI from the rest of the code better than any framework I have seen before. UpdateControls supports not only WPF but WinForms and Silverlight, too, and it encourages you to write your code in such a way that the view is replacable. I'm using UpdateControls for WinForms right now, because I find it easier, but if I ever want to upgrade to WPF, I will be able to do so with minimal changes to my Model and ViewModel code. Moreover, I could easily design a program that supports both a WinForms user interface and a Silverlight one at the same time: two views, one ViewModel. If somebody decides to create "UpdateControls for MonoTouch" someday, I could add an iPhone version, sharing the same viewmodel between Silverlight, WinForms and MonoTouch.
By strictly separating the ViewModel from the View, and placing all nontrivial logic in the ViewModel, you can effectively write unit tests for your GUI by writing tests for the ViewModel. Just make sure that the code in the View is so simple that no tests are needed, and that the output you want to test for is computed by the ViewModel instead of the View. You still have to manually test the view to make sure it looks right, but its logic and behavior can be unit-tested.
The Presentation and Navigation Models
Moreover, I am finding that the "Update Controls" way of doing things leads to clean, maintainable code. I especially like the Presentation Model concept. I think of the presentation model as a kind of "file system" of GUI state. That is, the presentation model is the root of a tree structure from which you can reach all of your application's important state information: the list of open documents, the currently selected item in each listbox, the options in your Options dialog, and so forth. The view (Forms and Controls) merely reflects the state of the presentation model.Conceptually the presentation model (PM) is broken down into two parts: the model, and the navigation model. I am not sure where the view-models fit into this pattern; heck, maybe this "file system" idea isn't exactly what Micheal Perry (author of Update Controls) was thinking of when he described the pattern.
But I don't think it's necessary to follow the pattern exactly as described in his blog. The key idea is that you can put all important state information in a big tree (analogous to a file system, or to the DOM in web programming), filled with the two basic Update Controls primitives, Independents and Dependents (known collectively as Precedents). Placing state in the UC-enabled presentation model makes your code very maintainable, because
- You no longer need to answer questions like "what events do I have to fire when I change this?" or "what properties change when this property changes?"
- If your state is in the presentation model, you no longer have to worry about what physical control(s) display a piece of state information--because controls don't depend on each other--and you can even move controls between different dialog boxes without breaking anything (other than the controls being moved).
- A tree structure is a good basis for a mental model of the application. In applications that hold state in many different ways, in separate "islands", it may be hard to keep track of everything. But the PM concept gives you a way to mentally visualize your program state and to communicate about state variables (because everything important has a named location in the tree.)
Have you ever fiddled with the console variables (CVars) in a video game such as Quake or Crysis? It's cool how you can change a variable and the game's output instantly matches the changes you make. Update Controls and the presentation model bring this kind of magic to your own code. You just define the state variables, stick them in a tree, and write simple snippets of code that describe how a GUI control depends on the tree.
The Presentation Model tree
The tree I'm describing isn't anything fancy. It's just an ordinary collection of classes, organized and nested in whatever way is most natural for the application. For instance, I'm writing an application that displays a collection of objects on a form. Well, actually there might be more than one collection of objects, with each collection shown on a different tab on the user interface; but I decided that my presentation model would only represent one data set (I create extra instances for multiple data sets). This data set has two fundamentally different kinds of objects, but for this discussion it doesn't matter what they are, so let's call them Apples and Bananas.The user can have either a "live view" which shows the current state of the objects, or a historical view, which shows the state of the objects at some point in the past. Plus, there is an options dialog, and the Presentation Model has a reference to the options. Note that there are no GUI objects in the presentation model; it provides access to the options, not to the dialog! It has four main properties (HModel, ApplesVM, BananasVM, Options) which, in turn, contain other properties, forming a tree:
PresentationModel (class)
.HModel (HistoryBrowserModel class: represents the model at one instant in time)
.Model (Model class: holds the core data model)
.Apples (list of FruitObject with state and history of each apple)
.Bananas (list of FruitObject with state and history of each banana)
(properties of FruitObject are not shown because they are not relevant
to this blog entry. In reality, my app is not fruit-related.)
.StartTime (DateTime: minimum value of the time slider in the GUI)
.EndTime (DateTime: maximum value of the time slider in the GUI;
derived automatically from time stamps on the FruitObjects)
.ViewTime (DateTime: current time being viewed in the GUI)
.LiveMode (bool: if true, ViewTime will be locked to EndTime)
.Apples (a projection of Model.Apples at the current ViewTime)
.Bananas (a projection of Model.Bananas at the current ViewTime)
.ApplesVM (FruitListViewModel class)
.CompleteList (list of FruitViewModels derived from HModel.Apples)
.Filter (user-defined filter that reduces the set of objects to display)
.FilteredList (list of FruitViewModels limited by the Filter)
.SelectedItem (currently selected FruitViewModel, or if multiple apples are
selected, a fake ViewModel that provides a combined view)
.BananasVM (another instance of FruitListViewModel)
.CompleteList
.Filter
.FilteredList
.SelectedItem
.Options (shared between all instances of PresentationModel)
.ServerAddress
... (more options)
What I'm most proud of is the time slider. Basically, the time slider is bound to the value of _pm.HModel.ViewTime (where _pm is the presentation model). When the user moves the slider, an event written by me updates _pm.HModel.ViewTime. UpdateControls figures out the rest:
- It knows that _pm.HModel.Apples and _pm.HModel.Bananas depend on ViewTime and automatically updates them.
- It also knows that _pm.ApplesVM.CompleteList and _pm.BananasVM.CompleteList depend on _pm.HModel.Apples and _pm.HModel.Bananas so it updates the CompleteLists too.
- In turn, the FilteredList properties depend on the CompleteList properties so they are updated.
- Finally, the Update Controls in the tab (ListBoxes, etc.) know that they depend on FilteredList, so they update themselves too. Like magic!
All I had to do was correctly associate my data with Update Controls primitives such as Independent, Dependent, Independent<T>, Dependent<T>, DependentList<T> and IndependentList<T>; and to implement GetHashCode and Equals on wrapper objects such as ViewModels (it's important to tell UpdateControls that two wrappers are equal if the wrapped objects are equal.) UpdateControls automatically detects dependencies between properties that use these primitives, and updates all Dependents on-demand (i.e. when their values are requested).
Has a bike?
So let's say you've designed a presentation model "PModel" for your application and it represents the form shown at the beginning of this blog entry. You're using Windows Forms. So, first, you need to give the form a reference to the presentation model:PModel _pm;
public Form1(PModel pm)
{
InitializeComponent();
_pm = pm;
}
// Somewhere else
Application.Run(new Form1(new PModel()));
Then, to ensure that the "serial number" checkbox (an instance of UpdateCheckBox) is enabled at the right time, use the WinForms designer to create an event handler for the "GetEnabled" event, and put the following code in it:
return _pm.SelectedPerson != null && _pm.SelectedPerson.HasBike;
Similar code snippets must be written for the control's Text property and for properties of all the other controls on the dialog. I'm not going to explain right now how to actually use Independents and Dependents to create the presentation model; I would refer you to the Update Controls blog for that information. Right now I just want to point out the two key advantages of this approach over conventional event-driven code:
- The code closely follows the requirement: "The serial number field is enabled only if 'Has a bike' is checked and a person is selected". If the requirements change, it's easy to update the code to match them.
- If you used Update Controls correctly (assuming no bugs in UC itself), there is no way the program can malfunction with respect to this requirement. It will always behave as that one line of code describes. The UC Way automatically leads to fewer bugs--although if you're new to UC, bugs that do occur tend to be serious head-scratchers.
I should point out one disadvantage to UpdateControls: it will permeate your codebase, changing the way you write both your ViewModels and your Models, so you'll be making a major commitment. And while UpdateControls integrates with WPF very closely, the WinForms version requires a special "Update" version of every control that you want to stay up-to-date automatically, and only a few properties currently support automatic updates. If you need to use 3rd-party Model classes in an UpdateControls app, you'll have to write decorator classes to augment the model classes with "Independent sentries". Similarly, to use 3rd-party controls in a WinForms app, you'll need to create a derived class or a special container that has private "Dependent sentries" and updates them during Application.Idle events. For example, I had to write an UpdateTrackBar class to represent my time slider, because UpdateControls didn't come with one.
Another issue, currently, is that the way UpdateControls handles lists is not scalable. Normally lists have a single "independent sentry" that controls updates to the lists. Adding, removing, or modifying a single item will cause each dependency to scan the entire list. If you are filtering a million-item list down to 100 items that you show on-screen, that million items will be scanned and filtered every time any item is added, removed, or changed, even if that item is not one of the 100 items displayed.
I'm trying to think of a way to make it more scalable, but I have no solutions to offer yet. There is one mitigating factor: UpdateControls updates dependencies in a kind of "lazy" fashion. If you change several items at once (e.g. in a single method or for-loop) in the million-item list, each of its dependencies is typically updated only once (in WinForms, dependencies will be updated in the next Application.Idle event; in WPF they will be updated in a posted message. Early updates occur on-demand, however, e.g. if your code explicitly accesses the filtered list.)
Still, I am quite certain that using a framework like UpdateControls will become an industry standard practice someday.
To get started with UpdateControls yourself, read its blog, check out examples such as Noteworthy, and write a small app to try it out. Noteworthy is odd--a "blog editor" that doesn't have a body text field?--but it seems to be the best example available at the moment. If you have any questions about UpdateControls, it's probably best to ask them on the discussions list.
4 comments:
Regarding update controls and data binding, what you're after is a dataflow-oriented or reactive programming model. In functional languages like Haskell and OCaml, this is known as functional reactive programming (FRP). .NET's reactive extensions are a good foundation for this.
If you look in my Sasa library, you'll see Sasa.Reactive.Property<T>. This is a mutable reactive value that conforms to the IObservable interface, and you can use this to implement the reactive UI properties you're after.
Your bike example would then reduce to something like:
BikeSerialNo.Enabled = BikeList.SelectedItem.Where(x => x != null);
Where "Enabled" would be of type Property<bool>, and SelectedItem would be of type Property<ListItem>.
This is a model similar to what you'll find in Bling. I've experimented with a few platform and location independent UI frameworks based on these abstractions, and I think I've settled on a good model that I'll probably release in the coming year.
If you keep an eye on my blog, you'll hear about it!
Ahh it would be so nice if blogger let you modify comments. Or if it wasn't hideously buggy. Anyway...
Are you saying you're developing a UI framework, or that you're developing extensions to an existing framework? If the latter, are you targeting WPF? (which I hate, but may be forced to use more in the future.) Are you aware of any 3rd-party .NET UI frameworks (i.e. UI libraries, not WPF extensions) that are really well designed?
And what would trigger the update in your framework? Would we still have to implement INotifyPropertyChanged twice, in the Model and ViewModel?
Update Controls is super cool because it detects dependencies automatically, but has its own drawbacks. Maybe someday, through metaprogramming or something, it'll be possible to avoid both the tedium of INotifyPropertyChanged and the run-time overhead and global state of Update Controls.
Re:INotifyPropertyChanged, see Sasa.Reactive.NamedProperty<T> which inherits from Sasa.Reactive.Property<T>. It implements both INotifyPropertyChanged and INotifyPropertyChanging for you, after you provide the name to use.
As for what triggers the update, the framework handles updates for you. The UI is a UI abstraction layer of sorts, that is location and platform agnostic. I have a WPF and HTTP bindings in mind; the latter demonstrates how the same UI code works with remote UIs as well.
Bling is really the best public example of the reactive UI paradigm at the moment, so you should check it out.
Post a Comment