The
How to make a loading graphic in WPF XAML? question on
stackoverflow reminded me, that I wanted to write a short piece about the WPF infrastructure I’m currently writing. A first part was the
Doing GUI architecture the Right Way where I describe the basic advantages of the View Model. Today I’m going to show some details of how this enables very smooth removal of background tasks from the UI Thread. Beware that this is work in progress, so edges might be rough.
The basic structure is three-fold: the Model where the actual data resides, the View which interacts with the user and the View Model as a intermediate layer. I will start out with describing the View, which is really thin. Afterwards I’ll talk a bit why the Model alone is not enough. In the last part I’ll describe my approach to filling the gap.
The View
The easiest part is the View. In WPF, this is the easiest part:
<window ... DataContext="{StaticResource unitOfWork}">
<contentPresenter
Content="{Binding}"
ContentTemplateSelector="{StaticResource defaultTemplateSelector}" />
</window>
The
unitOfWork is the root object of the current View Model tree and the
defaultTemplateSelector uses meta data to map from a View Model class to the appropriate WPF template. In my case, this is coming from the bowels of the database but you can also just graft XAML (directly or by reference) onto the View Model.
Everything else in the View is done by binding to parts of the View Model and choosing the right template, either by way of the default template selector or overriding it when a special context arises (e.g. list item vs. detail view).
The Model
The most basic component is the Business Layer or the Model. This layer may talk to the database, call out to services, or do whatever else is needed to implement its interface.
There are many reasons why WPF cannot directly bind to this layer:
- Not built for binding. No
INotifyPropertyChanged or INotifyCollectionChanged interfaces.
- May block. Talking to the network or the hard disk may introduce delays which are unacceptable for the UI.
- Doesn’t know about multi-threading. Usually Models know only about Transactions, which are by themselves inherently single-threaded.
- No place for the View’s state. Where should the View store things like window sizes, selected items or other ephemeral information?
None of these points is relevant when doing batch processing or when doing an ASP.NET site, which is a good benchmark for me that the Model shouldn’t burden itself with trying to solve them.
The View Model
To work around the “shortcomings” of the Model, this additional layer provides mechanisms to decouple user interaction from delays in the Model and a place to add additional properties to store and share View state between multiple Views.
The basis of all presentable Models is this abstract base class:
public abstract class PresentableModel
: INotifyPropertyChanged
{
public ModelFactory Factory { get { return AppContext.Factory; } }
protected IContext DataContext { get; private set; }
protected IThreadManager UI { get { return AppContext.UiThread; } }
protected IThreadManager Async { get { return AppContext.AsyncThread; } }
public PresentableModel(IContext dataCtx) {
UI.Verify();
DataContext = dataCtx;
}
#region Public interface
private ModelState _State = ModelState.Loading;
public ModelState State {
get {
UI.Verify();
return _State;
}
protected set {
UI.Verify();
if (value != _State) {
_State = value;
OnPropertyChanged("State");
}
}
}
#endregion
#region INotifyPropertyChanged Members
private event PropertyChangedEventHandler _PropertyChangedEvent;
public event PropertyChangedEventHandler PropertyChanged {
add {
UI.Verify();
_PropertyChangedEvent += value;
}
remove {
UI.Verify();
_PropertyChangedEvent -= value;
}
}
protected virtual void OnPropertyChanged(string propertyName) {
UI.Verify();
if (_PropertyChangedEvent != null)
_PropertyChangedEvent(this, new PropertyChangedEventArgs(propertyName));
}
protected void AsyncOnPropertyChanged(string propertyName) {
Async.Verify();
UI.Queue(UI, () => OnPropertyChanged(propertyName));
}
#endregion
}
IThreadManager: a component that can Verify() whether execution is on the right thread and Queue() an action to the managed thread. Each model has two of them. The UI manager takes care of the UI thread and the Async manager handles asynchronous actions. The default implementations use the Dispatcher and the ThreadPool respectively.
State: a flag to indicate in which state the PresentableModel is. Possible states include Invalid, Loading, and Active. This also shows the default pattern for a Property on a PresentableModel: It can only be accessed from the UI thread (lines 19 and 23) and only notifies on real changes (line 24 and 26).
AsyncOnPropertyChanged(): the only method on PresentableModel that may be called from a background thread (line 54). By convention the method name starts with “Async”. The easiest of tasks, calling OnPropertyChanged() on the UI thread, is handled by using UI.Queue().
Actual Presentables
As an easy example of the flexibility of this model, I present you a
SearchActionModel which can react on updates to the search term and to asynchronously batch queries to the database whose results are displayed incrementally.
public class SearchActionModel: PresentableModel
{
private string _searchTerm = null;
public string SearchTerm {
get {
UI.Verify();
return _searchTerm;
}
set {
UI.Verify();
if (_searchTerm != value) {
_searchTerm = value;
OnNotifyPropertyChanged("SearchTerm");
Async.Queue(DataContext, AsyncUpdateResults);
}
}
private ObservableCollection<searchResult> _results = new ObservableCollection<searchResult>();
private ReadOnlyObservableCollection<searchResult> _resultsProxy = new ReadOnlyObservableCollection<searchResult>(_results);
public ReadOnlyObservableCollection<searchResult> Results {
get {
UI.Verify();
return _resultsProxy;
}
}
private void AsyncUpdateResults() {
Async.Verify();
UI.Queue(UI, () => {
_results.Clear();
State = ModelState.Loading;
string searchTerm = _searchTerm;
Async.Queue(DataContext, () => {
var results = DataContext.GetQuery<searchResult>().Where(r => r.Name.Contains(searchTerm));
foreach(var r in results) {
UI.Queue(UI, () => _results.Add(r));
}
UI.Queue(UI, () => State = ModelState.Active);
});
});
}
}
Note how the multiple
Queue() operations are nested to enforce an implicit and lock-less sequencing of the various actions.
The View is quite primitive:
<userControl x:Class="App.View.SearchActionView" ...>
<dockPanel>
<textBox
Text="{Binding SearchTerm}"
DockPanel.Dock="Top" />
<ctrls:LoadingDindicator DockPanel.Dock="Top" />
<listBox
ItemsSource="{Binding Results}" />
</stackPanel>
</userControl>
Note: the LoadingIndicator is a small widget binding to the State Property and displaying a little spinning wheel when the Model is not Active.
Of course even this trivial example is not without its share of problems:
- The
Results collection is cleared on every input (line 30). A more sophisticated implementation might want to filter the results locally before going to the database again. Linq should make it easy to encapsulate the filter logic.
- The loading code depends on a streaming Linq provider (line 35). If the underlying Linq provider blocks too long before returning the first result, a local workaround might be to implement client side paging and make multiple queries which only fetch a few elements each.
- All
Queue() magic aside, the code does play with fire and there are several possibilities to breach thread safety. For example, the search term has to be copied on the UI thread into a local variable (line 32) to avoid accessing the SearchTerm property from the background thread (line 34).
Conclusion and Future Work
This organisation of threading and binding is a powerful way to get highly flexible and easily bindable model classes for use with WPF. Still programming in a free threading application is playing with fire and even slight missteps may produce subtle and hard to find bugs.
Other things are more of a nuisance because I didn’t come around implementing them yet. First, the
State should support ref-counting semantics, because currently multiple parallel updates lead to the model being
Active as soon as the first update finishes. The other things are related to the
IThreadManager. It is currently impossible to abort actions once they’ve been queued and actions are mostly processed in FIFO which is annoying if somewhere is a longer running action, perhaps even consisting of multiple enqueued parts, which delay the reaction to the user’s last input.