Introduction
Working with WPF for a while now, I've become a big fan of MVVM, the Model-View-ViewModel design pattern, which means the data is kept in the Model, the GUI is managed by the View and the ViewModel functions as a mediator between the Model and the View. The View uses the ViewModel as its DataContext. I've made it a mission for myself to build views which use no code in the XAML .cs files. This technique simplifies any changes which needs to be done by the View. However, the new task I was facing was a bit different. I've been told that the feature I was about to develop is planned to have UI which might be web based. My ViewModel couldn't have carried any objects which were Window specific, such as ICommand
. Also the new UI was to be developed by a third party outside my company. That third party may want to use stubs to test their development. These requirements demanded new planning on my behalf.
Background
The first thing I did was making interfaces of all my future to be View models. The View was to know the interfaces without knowing the exact implementation. This makes the usage of stubs much easier. You can find the interfaces in the "Interfaces" project. The second thing was creating a class called InterfacesFactory
which is the only class from the view model which the view is aware of. This class creates the Required View models. Dependency injection can come in handy as well.
public IEditableCityViewModel GetEditableCityViewModelForAdd(Guid countryId)
{
return new EditableCityViewModel(new City(),countryId);
}
public IEditableCityViewModel GetEditableCityViewModelForEdit(Guid cityId)
{
City city = DataManager.Instance.GetCity(cityId);
return new EditableCityViewModel(city);
}
Having the view working with interfaces and not concrete classes posed a problem when using DataTemplate
, which needs concrete types. I solved this problem by building my own DataTemplateSelector
:
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate dTemplate = null;
if (item != null)
{
Type[] types = item.GetType().GetInterfaces();
if (types.Contains(typeof(IReadOnlyContinentViewModel)))
{
dTemplate = ContinentDataTemplate;
}
else if (types.Contains(typeof(IReadonlyCountryViewModel)))
{
dTemplate = CountryDataTemplate;
}
else if (types.Contains(typeof(IReadOnlyCityViewModel)))
{
dTemplate = CityDataTemplate;
}
}
return dTemplate;
}
<local:ReadOnlyObjectsDataTemplateSelector
x:Key="selector"
ContinentDataTemplate="{StaticResource readOnlyContinentDataItem}"
CountryDataTemplate="{StaticResource readOnlyCountryDataItem}"
CityDataTemplate="{StaticResource readOnlyCityDataTemplate}"
/>
Instead of ObservableCollection
, I used IList
. Instead on INotifyPropertyChanged
, I used events which could be registered. On this scenario, the View registers itself to events and is responsible to update itself. It is important to remember that the data can only be updated from the Main
thread.
private void GetCurrentTime(object state)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke
(new SimpleOperationDelegate(this.CurrentTimeUpdated));
}
private void CurrentTimeUpdated()
{
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(txtCurrentTime, TextBlock.TextProperty);
bindingExpression.UpdateTarget();
}
Instead of ICommand, I gave API calls on the View model. These APIs are called by asynchronous calls, due to the face that they might take time to commit.
private void onSave(object sender, ExecutedRoutedEventArgs args)
{
this.Cursor = Cursors.Wait;
SimpleOperationDelegate delg = new SimpleOperationDelegate(this.SaveOperation);
delg.BeginInvoke(this.OperationCompleted, delg);
}
private void SaveOperation()
{
m_DataContext.Save();
}
private void OperationCompleted(IAsyncResult result)
{
System.Windows.Application.Current.Dispatcher.BeginInvoke
(new AsyncCallback(this.EndOperation), result);
}
private void EndOperation(IAsyncResult result)
{
this.Cursor = Cursors.Arrow;
SimpleOperationDelegate delg = result.AsyncState as SimpleOperationDelegate;
delg.EndInvoke(result);
this.DialogResult = true;
this.Close();
}
Using the Code
The example solution is a TreeView
presentation of Continents, countries and cities. There is an ability to add, edit or remove each one of them. The Model and the View are kept in the Lib project. The View is kept in the UILIB project. There is the Interfaces project, which holds all the interfaces the View works with and a TestsLib project which I used for testing my code. The data is kept in an XML file within the Lib project. The solution can easily be run after compiling it.
Points of Interest
My intention was that the View model would not be changed when the time comes. I hope it would be so.
History
- 22nd March, 2010: Initial version