Introduction
The core idea behind this code and article is to provide an uncomplicated
approach to reset a view-model without having to adjust DataContext
references in
views. It all started with an
MSDN forum thread where someone asked if there was an easy way to clear a
view model. He felt that clearing every property in the view model would be too
cumbersome and that it would be simpler if he could have a Cancel button
that would reset the DataContext
to a new view model instance. The
negative (so to speak) would be that he would have to have actual code in his
view (the handler for the Cancel button) that did this. This problem
intrigued me and I thought it would be an interesting exercise to design a set
of classes that would make this a seamless process.
I kept two fundamental requirements in mind when I designed this class :
- The developer should be able to reset the view model without any code in
his view.
- The class should be transparent, so the user should not have to change his
existing view model in any way to use this class.
This code will only work with WPF, and I have tested this with .NET versions
4.0 (full and client profile) as well as 3.5 (full and client profile). The demo
project is for Visual Studio 2010, but since it's .NET 3.5 compatible, you can
easily use this from Visual Studio 2008 too. Unfortunately the code will not
work on SilverLight as the classes it uses such as TypeDescriptionProvider
are not available in SilverLight (even in version 4.0).
And lest anyone writes this off as yet another over-designed MVVM class
targeting those who are anal retentive about avoiding code in their view, I
think this class has value outside of it's code-in-the-view avoidance
capabilities. *grin*
Why use this class at all?
Basarat Ali Syed wondered why I even needed to write this class. His exact
words were:
Why not just use a ViewModel locator (which you are using) combined
with a Messenger (also called mediator) to pass a message from the current
View to the ViewModel locator to setup new properties.
This was basically what I replied to him:
While people can do that if they want to, not everyone likes that approach
for these reasons:
- It's almost impossible to avoid code behind, since the view has to
trigger a need to update the view model.
- You may have to have a View reference in the View Model (which breaks basic
MVVM). Even if you use a Mediator, that's still just one level of indirection
back to the view.
- Setting up new properties is a hassle, specially for complex view model
classes
What my class achieves is this:
- You basically re-instantiate the view model. (this is analogous to
having the magical ability to use the same this-pointer in C++ to point to a
completely new instance, but at the same address)
- The view / original view model need to know nothing about what's going on
(meaning the class is easy to plug in and out)
- No code behind in the view
- No need to add mediator/in-between classes for communication
And finally, it's important to remember that this is not the sort of class
that you will just use everywhere and all the time. It has a specific design
purpose and it does that role well. Feel free to ask more questions on this
through the forum below.
Using the class with an existing MVVM application
Consider that you have an existing MVVM application where your
MainWindow
view is bound to a MainViewModel
instance.
<Window x:Class="ResettableViewModelDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ResettableViewModelDemo"
Title="Resettable View Model Demo Application" Height="350" Width="329" ResizeMode="NoResize"
DataContext="{Binding Source={x:Static local:ViewModels.MainViewModel}}" Background="#FF485093">
<Grid Margin="5" TextBlock.Foreground="White" TextBlock.FontSize="15">
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal">
<TextBlock Width="90" VerticalAlignment="Center">Item Code</TextBlock>
<TextBox Text="{Binding ItemCode}" Width="197" Margin="8" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Width="90" VerticalAlignment="Center">Description</TextBlock>
<TextBox Text="{Binding Description}" Width="196" Margin="8" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding AddCommand}" Width="90" Margin="5">Add</Button>
<Button Command="{Binding ExitCommand}" Width="90" Margin="5">Exit</Button>
</StackPanel>
<ListBox ItemsSource="{Binding Entries}" Height="180" Width="293" />
</StackPanel>
</Grid>
</Window>
Notice how the DataContext
is set to a static property in the
ViewModels
class.
class ViewModels
{
private static object mainViewModel = new MainViewModel();
public static object MainViewModel
{
get { return ViewModels.mainViewModel; }
}
}
MainViewModel
will be your existing view model implementation.
class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private int itemCode = -1;
public int ItemCode
{
get
{
return itemCode;
}
set
{
if (itemCode != value)
{
itemCode = value;
FirePropertyChanged("ItemCode");
}
}
}
private string description = "unknown";
public string Description
{
get
{
return description;
}
set
{
if (description != value)
{
description = value;
FirePropertyChanged("Description");
}
}
}
private ICommand exitCommand;
public ICommand ExitCommand
{
get
{
return exitCommand ?? (exitCommand = new DelegateCommand(() =>
{
Application.Current.MainWindow.Close();
}));
}
}
private ICommand addCommand;
public ICommand AddCommand
{
get
{
return addCommand ?? (addCommand = new DelegateCommand(() =>
{
entries.Add(new { ItemCode = ItemCode, Description = Description });
}));
}
}
private ObservableCollection<object> entries = new ObservableCollection<object>();
public ObservableCollection<object> Entries
{
get { return entries; }
}
}
Now, assume that you have a new requirement to add a Cancel button to
the view. Clicking the Cancel button must reset the two TextBox
es
to the default values, and should also clear the ListBox
. Without
the ResettableViewModel
class, you have two options - either reset
all the bound properties through a Cancel command method, or add
code-behind in your view to reset the DataContext
to a brand new
instance of the MainViewModel
class. The former approach may be
rather a lot of work when your view model is complex, and if you ever add/change
a bound property or collection, you need to remember to reset that too. The
latter approach is simple but a lot of MVVM aficionados would cringe at the
thought of doing that. Oh, isn't this a conundrum? Enter the
ResettableViewModel
class!
Once you add the required source files to your project (or alternatively, add
a reference to an assembly that contains these classes), you just need to do two
little things. The first task is to change the static property to return a
ResettableViewModel
instance. (Note: if you set your
DataContext
in some other way, you'd have to make appropriate changes to
do something similar)
class ViewModels
{
private static object mainViewModel =
new NSViewModelExtensions.ResettableViewModel(new MainViewModel());
public static object MainViewModel
{
get { return ViewModels.mainViewModel; }
}
}
Now add a Cancel button to your XAML and bind its Command
as follows.
<StackPanel Orientation="Horizontal">
<Button Command="{Binding ResetCommand}" Width="90" Margin="5">Cancel</Button>
<Button Command="{Binding AddCommand}" Width="90" Margin="5">Add</Button>
<Button Command="{Binding ExitCommand}" Width="90" Margin="5">Exit</Button>
</StackPanel>
Notice how the Cancel button's command is bound to a
ResetCommand
. This is provided by the ResettableViewModel
class, everything else will continue to come from your MainViewModel
class. That's all, you are done! If you run the app now, clicking Cancel
will reset your view model to its initial state. Now, that wasn't a lot of
work, was it?
Custom view model initialization
In some cases, your view model may not have a parameterless constructor, or
you may need to set some initial properties. For those scenarios, the class
provides a constructor that takes a Func<object>
delegate that will
be used to create the view model instance. Here's a simple example:
object mainViewModel = new NSViewModelExtensions.ResettableViewModel(
() => new MainViewModel());
Here's a more typical example:
object mainViewModel = new NSViewModelExtensions.ResettableViewModel(
() =>
{
return new MainViewModel()
{
ItemCode = 999,
Description = "Default Item"
};
});
One caveat
If your view model has a ResetCommand
property, it will will
overridden by the ResetCommand
property in the
ResettableViewModel
class. While it would probably be quite a rare
situation, if you do run into it, then the workaround is to rename your property
- sorry about that!
Implementation details
Put simply the ResettableViewModel
class acts as a proxy to the
actual view model instance. The only API newly added by the the
ResettableViewModel
class is the reset command, everything else comes
from the underlying view model. It does this by implementing a custom
TypeDescriptionProvider
which dynamically provides properties on the
ResettableViewModel
instance that are fetched from the underlying
view model object. I will quickly go through the various classes involved, and
although the code is written to be quite self explanatory, I will add my
comments where required. Here's what the ResettableViewModel
class
looks like.
[TypeDescriptionProvider(typeof(ResettableViewModelTypeDescriptionProvider))]
sealed class ResettableViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
private static string ErrorViewModelTypeHasToMatch =
"The type of the new View Model has to match that of the old View Model.";
private Func<object> creatorMethod;
public ResettableViewModel(object innerViewModel, Func<object> creatorMethod = null)
{
this.InnerViewModel = innerViewModel;
this.creatorMethod = creatorMethod;
}
public ResettableViewModel(Func<object> creatorMethod)
{
this.InnerViewModel = (this.creatorMethod = creatorMethod)();
}
public ResettableViewModel(Type innerViewModelType)
{
this.InnerViewModel = Activator.CreateInstance(innerViewModelType);
}
internal object InnerViewModel { get; private set; }
private ICommand resetCommand;
public ICommand ResetCommand
{
get
{
return resetCommand ?? (resetCommand = new InternalDelegateCommand(() =>
{
if (creatorMethod == null)
{
this.InnerViewModel = Activator.CreateInstance(
this.InnerViewModel.GetType());
}
else
{
var newViewModel = creatorMethod();
if (this.InnerViewModel.GetType() != newViewModel.GetType())
{
throw new InvalidOperationException(
ResettableViewModel.ErrorViewModelTypeHasToMatch);
}
this.InnerViewModel = newViewModel;
}
FirePropertyChanged(String.Empty);
}));
}
}
class InternalDelegateCommand : ICommand
{
private readonly Action executeMethod;
public InternalDelegateCommand(Action executeMethod)
{
this.executeMethod = executeMethod;
}
public void Execute(object parameter)
{
if (this.executeMethod != null)
{
this.executeMethod();
}
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
}
Notice how the class has a TypeDescriptionProvider
associated
with it of type ResettableViewModelTypeDescriptionProvider
(listed
below). The reason I have a private inner class that implements ICommand
is that I did not want to have any dependencies on any external command classes.
This way, whatever MVVM framework you are on, the ResettableViewModel
class would work smoothly since it has its own command class.
Pro-tip to update all bindings
Notice the call to FirePropertyChanged
passing an empty string,
well that will update every binding you have in your view.
The ResettableViewModelTypeDescriptionProvider
class is simple
and basically just provides a way for us to specify the custom type descriptor
for our class.
class ResettableViewModelTypeDescriptionProvider : TypeDescriptionProvider
{
private static TypeDescriptionProvider defaultTypeProvider =
TypeDescriptor.GetProvider(typeof(ResettableViewModel));
public ResettableViewModelTypeDescriptionProvider()
: base(defaultTypeProvider)
{
}
public override ICustomTypeDescriptor GetTypeDescriptor(
Type objectType, object instance)
{
ICustomTypeDescriptor defaultDescriptor =
base.GetTypeDescriptor(objectType, instance);
return instance == null ? defaultDescriptor :
new ResettableViewModelCustomTypeDescriptor(defaultDescriptor, instance);
}
}
ResettableViewModelCustomTypeDescriptor
is where we associate
the properties in the inner class with our proxy class (the word proxy
used very loosely there of course).
class ResettableViewModelCustomTypeDescriptor : CustomTypeDescriptor
{
public ResettableViewModelCustomTypeDescriptor(ICustomTypeDescriptor parent, object instance)
: base(parent)
{
customFields.AddRange(TypeDescriptor.GetProperties(
((ResettableViewModel)instance).InnerViewModel)
.Cast<PropertyDescriptor>()
.Select(pd => new ResettableViewModelCustomField(
pd.Name, pd.PropertyType))
.Select(cf => new ResettableViewModelCustomFieldPropertyDescriptor(
cf)).Cast<PropertyDescriptor>());
}
private List<PropertyDescriptor> customFields = new List<PropertyDescriptor>();
public override PropertyDescriptorCollection GetProperties()
{
return new PropertyDescriptorCollection(base.GetProperties()
.Cast<PropertyDescriptor>()
.Union(customFields)
.ToArray());
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return new PropertyDescriptorCollection(base.GetProperties(attributes)
.Cast<PropertyDescriptor>()
.Union(customFields)
.ToArray());
}
}
ResettableViewModelCustomField
is a simple class that represents
a property. It's used to store the property name and property type for all the
properties in the underlying view model instance.
class ResettableViewModelCustomField
{
public ResettableViewModelCustomField(String name, Type dataType)
{
Name = name;
DataType = dataType;
}
public String Name { get; private set; }
public Type DataType { get; private set; }
}
ResettableViewModelCustomFieldPropertyDescriptor
is where we
actually fetch the property values from the underlying instance and return it to
whoever asks for these properties on the outer class instance.
class ResettableViewModelCustomFieldPropertyDescriptor : PropertyDescriptor
{
public ResettableViewModelCustomField CustomField { get; private set; }
public ResettableViewModelCustomFieldPropertyDescriptor(
ResettableViewModelCustomField customField)
: base(customField.Name, new Attribute[0])
{
CustomField = customField;
}
public override bool CanResetValue(object component)
{
return true;
}
public override Type ComponentType
{
get
{
return typeof(ResettableViewModel);
}
}
public override object GetValue(object component)
{
return GetInnerPropertyInfo(component).GetValue(
((ResettableViewModel)component).InnerViewModel, new object[0]);
}
public override bool IsReadOnly
{
get
{
return false;
}
}
public override Type PropertyType
{
get
{
return CustomField.DataType;
}
}
public override void ResetValue(object component)
{
this.SetValue(component, Activator.CreateInstance(CustomField.DataType));
}
public override void SetValue(object component, object value)
{
GetInnerPropertyInfo(component).SetValue(
((ResettableViewModel)component).InnerViewModel, value, new object[0]);
}
public override bool ShouldSerializeValue(object component)
{
return false;
}
private PropertyInfo propertyInfo;
private PropertyInfo GetInnerPropertyInfo(object component)
{
return propertyInfo ?? (propertyInfo = ((ResettableViewModel)component)
.InnerViewModel.GetType().GetProperty(this.CustomField.Name));
}
}
The highlighted code in the above snippet is where we get and set properties
on the underlying view model instance.
Conclusion
That's it, I guess. As usual, please feel free to give me feedback and
criticism through the article forum. Your votes of 5 will be deeply appreciated
too, and I will have a shot of single malt scotch for every 5 I
get, and then maybe a few more!
History
- February 16, 2011 - Article first published.
- February 17, 2011 - Added text better explaining why this class
was designed.