- Hooking up View and ViewModels in MVVM
- Communicating between Views and ViewModels in WPF
- Hierarchies and Navigation
- Validation in MVVM
- MVVM Toolkits / Frameworks
Hooking up View and ViewModels in MVVM
There are a couple ways to connect Views and ViewModels together in WPF:
- View-First: Where the view is constructed first, and the view model bound after-the-fact.
- ViewModel-First: Where the view model is constructed first, and the view is created after the fact.
View-First Construction
Method 1 - Statically Setting the DataContext
When the code-behind of a xaml view get constructed, it will call the InitializeComponent()
method, which will go ahead and scan the xaml constructing the view.
If you have a DataContext
set in the xaml, then a static instance of the view model will be created during this initialization step.
<UserControl>
<UserControl.DataContext>
<local:CustomerListViewModel /> <!-- Notice Here -->
</UserControl.DataContext>
<Grid>
<DataGrid ItemsSource="{Binding Customers}" />
</Grid>
</UserControl>
Method 2 - Imperatively Setting the DataContext
You can also set the DataContext
from the code behind, like so:
public partial class CustomerListView : UserControl
{
public CustomerListView()
{
this.DataContext = new CustomerListViewModel();
InitializeComponent();
}
}
You might want to do this if your view model has any constructor parameters, as xaml cannot handle the passing of arguments to the DataContext
.
Using the ViewModelLocator Pattern
ViewModelLocator is a meta-pattern for automatically locating and hooking up the right ViewModel.
Here’s an example of how you could implement a ViewModelLocator:
public static class ViewModelLocator
{
public static bool GetAutoWireViewModel(DependencyObject obj)
{
return (bool)obj.GetValue(AutoWireViewModelProperty);)
}
public static void SetAutoWireViewModel(DependencyObject obj, bool value)
{
obj.SetValue(AutoWireViewModelProperty, value);
}
public static readonly DependencyProperty AutoWireViewModelProperty =
DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool),
typeof(ViewModelLocator), new PropertyMetadata(false, AutoWireViewModelChanged));
private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject())) return;
var viewTypeName = d.GetType().FullName; // get the View's name
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
}
ViewModelLocator Process
- Determine which View type is being constructed
- Determine ViewModel type to create based on convention
- Construct ViewModel
- Set ViewModel as the View’s DataContext
Then for your Views, you will need to add the following:
<UserControl ...
xmlns:root="clr-namespace:YourNamespace"
root:ViewModelLocator.AutoWireViewModel="True">
Advantages include:
- Standard declaration in every view
- The View is no longer strongly coupled to a particular ViewModel type
ViewModel-First Construction
Before you start with ViewModel-First construction, it’s helpful to have some understanding of how data templates work.
Using Explicit and Implicit DataTemplates
Explicit DataTemplate Example
Below defines a DataTemplate
for displaying a Customer object.
This way, the ListBox knows how to properly display a Customer object.
<UserControl.Resources>
<DataTemplate x:Key="CustomerTemplate">
<StackPanel>
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text="{Binding LastName}" />
<TextBlock Text="{Binding Phone}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<!-- ... -->
<ListBox ItemsSource="{Binding Customers}" ItemTemplate="{StaticResource CustomerTemplate}" />
Implicit DataTemplate Example
<UserControl.Resources>
<DataTemplate DataType="{x:Type data:Customer}">
<StackPanel>
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text="{Binding LastName}" />
<TextBlock Text="{Binding Phone}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<!-- ... -->
<ListBox ItemsSource="{Binding Customers}" />
Using DataTemplates for ViewModel-First Construction
<Window.Resources>
<DataTemplate DataType="{x:Type Customers:CustomerListViewModel}">
<Customers:CustomerListView />
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CustomerViewModelFromMainWindowViewModel}" />
</Grid>
Communicating between Views and ViewModels in WPF
Using Commands for View to ViewModel Communication
The most common way to communicate between the View and ViewModel is by using an ICommand
in the ViewModel and binding to it in the View.
Below are some examples on how to bind for invocation of the command. See Relay Command for code-behind implementation.
<Button Content="Delete"
Command="{Binding DeleteCommand}"/>
<UserControl.InputBindings>
<KeyBinding Key="D" Modifiers="Control"
Command="{Binding DeleteCommand}" />
</UserControl.InputBindings>
Attached Properties/Behaviors - WIP
Intro to Attached Properties
- Attached properties are Dependency properties and a fundamental part of XAML
- Attached properties can be defined on any class, and can be applied to any object that derives from DependencyObject
- Attached properties form the basis for behaviors
- Can implement behaviors as attached properties, or using the XAML Behaviors for WPF open source library
- Using XAML Behaviors for WPF is the recommended approach
Intro to XAML Behaviors for WPF
- Behaviors still get hooked up to controls through attached properties
- Behaviors can expose their own dependency properties, so they can be data bound in XAML
- Behaviors can subscribe to events or property change notifications on the control they are attached to
- Behaviors can dispatch calls into the ViewModel, through Commands, setting data bound properties, or directly invoking a method on the ViewModel
- Behaviors can trigger on changes in the ViewModel to modify the control they are attached to, or other controls in the same visual tree.
Property Changed Notifications
- Need to trigger bindings to refresh as property values change
- Two options: dependency properties or INotifyPropertyChanged
- INotifyPropertyChanged is more appropriate for MVVM
Hierarchies and Navigation
Sometimes you will want a View to be composed of other Views (and their associated ViewModels).
Encapsulating INotifyPropertyChanged
You can have your ViewModels inherit from ViewModelBase to encapsulate INPC logic.
public class ViewModelBase : INotifyPropertyChanged
{
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null)
{
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
})
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
Navigating with View Switching
You can use a Command
with CommandParameter
to switch between views:
In the ViewModel:
public RelayCommand<string> NavCommand { get; private set; }
public ViewModelCtor()
{
NavCommand = new RelayCommand<string>(OnNav);
}
private void OnNav(string destination)
{
switch(destination)
{
case "orderPrep":
ChildViewModel = _orderPrepViewModel;
break;
// case ...:
}
}
In the XAML:
<Button Command="{Binding NavCommand}"
CommandParameter="orderPrep"/>
Validation in MVVM
- Data entry forms can still leverage WPF data binding validation features
- Validation logic belongs in the Model or ViewModel, not the View
- Can use any of:
- Exceptions
- IDataErrorInfo
- INotifyDataErrorInfo
- ValidationRules
- Favor INotifyDataErrorInfo
Adding Validation to an Input View
public class ValidatableViewModel : ViewModelBase, INotifyDataErrorInfo
{
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
public bool HasErrors
{
get { return _errors.Count > 0; }
}
protected override void SetProperty<T>(ref T member, T val, [CallerMemberName] string propertyName = null)
{
base.SetProperty<T>(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
private void ValidateProperty<T>(string propertyName, T value)
{
var results = new List<ValidationResult>();
ValidationContext context = new ValidationContext(this);
context.MemberName = propertyName;
Validator.TryValidateProperty(value, context, results);
if (results.Any())
{
_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
}
else
{
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
In XAML, this will check to see if the ViewModel implements INotifyDataErrorInfo
:
<TextBox Text="{Binding FirstName, ValidatesOnNotifyDataErrors=True}" />