- UI Design Patterns
- The Model-View-ViewModel Pattern
- Binding the View Model to the View
- Lists and Data Elements
- Summary
Binding the View Model to the View
Binding the view model to the view is one of the key problems many MVVM frameworks try to solve. It can be done various ways, and there is no preferred method; otherwise, all experienced developers would be doing it the same way. The most popular method seems to be using a view model locator, whereas some people prefer to spin up controllers that perform the binding. My preferred method is to use the built-in design-time view model functionality provided by Silverlight using the design-time extensions. With Silverlight 5, it is also possible to use custom markup extensions.
In this section, you explore various methods to bind the view model to the view, and you can decide which method makes the most sense for your applications. The view model in each case processes a selection from a drop-down and render a shape.
View Model Locators
View model locators are simply classes that expose the view models as properties—typically as interfaces. The locator can determine the state of the application and return the appropriate view model based on whether it is called during design time or runtime. To see how a view model locator works, create a new Silverlight Application project called ViewModelLocators. Specify a grid with four equally sized quadrants (two rows and two columns). Provide an interface for the view model:
public interface IViewModelInterface { List<string> Shapes { get; } string SelectedShape { get; } }
Now implement a design-time view model:
public class DesignViewModel : IViewModelInterface { public List<string> Shapes { get { return new List<string> {"Circle", "Square", "Rectangle"}; } } public string SelectedShape { get { return "Circle"; } } }
Create the runtime view model. It will implement the property-change notification interface and provide a delegate for transitioning the visual state. The code for the view model is shown in Listing 7.1.
Listing 7.1. The Main View Model
public class ViewModel : IViewModelInterface, INotifyPropertyChanged { public ViewModel() { GoToVisualState = state => { }; } private readonly List<string> _shapes = new List<string> { "Circle", "Square", "Rectangle" }; public List<string> Shapes { get { return _shapes; } } public Action<string> GoToVisualState { get; set; } private string _selectedShape; public string SelectedShape { get { return _selectedShape; } set { _selectedShape = value; var handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs("SelectedShape")); } if (!string.IsNullOrEmpty(value)) { GoToVisualState(value); } } } public event PropertyChangedEventHandler PropertyChanged; }
Now you can create the ViewModelLocator class. The locator will simply determine whether it is being called during design time or runtime and return the appropriate view model.
public class ViewModelLocator { private IViewModelInterface _viewModel; public IViewModelInterface ViewModel { get { return _viewModel ?? (_viewModel = DesignerProperties.IsInDesignTool ? (IViewModelInterface) new DesignViewModel() : new ViewModel()); } } }
Now you can create a control to implement the behavior. The control will include the view model locator in its resources (the locator is typically declared at the App.xaml level to make it available to the entire application). Add a new control called LocatorControl.xaml and fill it with the following Xaml:
<UserControl.Resources> <ViewModelLocators:ViewModelLocator x:Key="Locator"/> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="White" DataContext="{Binding Source={StaticResource Locator}, Path=ViewModel}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <ComboBox ItemsSource="{Binding Shapes}" SelectedItem="{Binding SelectedShape, Mode=TwoWay}"/> </Grid>
Now define the shapes and place them in the second row of the LocatorControl:
<Ellipse x:Name="CircleShape" Width="50" Height="50" Fill="Green" Grid.Row="1"/> <Rectangle x:Name="SquareShape" Width="50" Height="50" Fill="Blue" Grid.Row="1"/> <Rectangle x:Name="RectangleShape" Width="100" Height="50" Fill="Red" Grid.Row="1"/>
Add the visual state groups. Part of the Xaml for the first state is shown in the following code. Repeat the ObjectAnimationUsingKeyFrames for the square and the rectangle, but set their visibility to the collapsed state. Next, copy and paste the visual state twice more for the square state and the rectangle state and then update which shapes are visible and which shapes are not.
<VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="ShapeGroups"> <VisualStateGroup.States> <VisualState x:Name="Circle"> <Storyboard> <ObjectAnimationUsingKeyFrames Storyboard.TargetName="CircleShape" Storyboard.TargetProperty="(UIElement.Visibility)"> <DiscreteObjectKeyFrame KeyTime="0:0:0"> <DiscreteObjectKeyFrame.Value> <Visibility>Visible</Visibility> </DiscreteObjectKeyFrame.Value> </DiscreteObjectKeyFrame> </ObjectAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup.States> </VisualStateGroup> </VisualStateManager.VisualStateGroups>
Set the locator control to navigate to a default visual state and set the delegate for transition states. This is done in the control code-behind:
public LocatorControl() { InitializeComponent(); if (DesignerProperties.IsInDesignTool) return; VisualStateManager.GoToState(this, "Circle", false); var locator = (ViewModelLocator) Resources["Locator"]; var vm = locator.ViewModel as ViewModel; vm.GoToVisualState = state => VisualStateManager.GoToState(this, state, true); }
The binding to the view model is the side effect of using the locator. You want to change the visual states from the view model, but the UserControl implements the functionality. Because of the context it is used in, it is not possible to get access to the host control and automatically bind the delegate. This must be done in the control itself by passing a delegate for the GoToState method to the view model.
Now you can declare an instance of the control in the main page in the upper-left quadrant and run the application. The shapes will swap out each time you select a new control. You will also see values in the combo box during design time because the view model locator returns the design-time view model for you.
Controllers
One of the less-common methods for binding the view model to the view is by using a controller. Contrary to popular belief, there is no law that MVVM cannot be mixed with other patterns. The controller pattern involves creating a class specifically tied to the view it is managing and then using the controller to spin up the view model and wire the logic.
First, create a new control called ControllerControl.xaml and copy the contents of the locator control. Remove the resources collection that references the view model locator, and remove the data context attribute on the grid that uses the locator resource. Next, create a new class called Controller and provide it with a constructor, like this:
public class Controller { private readonly IViewModelInterface _vm; public Controller(Control control) { if (DesignerProperties.IsInDesignTool) { _vm = new DesignViewModel(); } else { _vm = new ViewModel { GoToVisualState = state => VisualStateManager.GoToState( control, state, true) }; VisualStateManager.GoToState( control, "Circle", false); } control.DataContext = _vm; } }
Note that for testing, the element passed in would normally be an interface that includes the data context element. This would allow the controller to be tested without passing an actual control, as is done here to keep the example simple. Now that the controller is defined, it simply needs to be created in the code-behind of the control:
public partial class ControllerControl { public Controller Controller { get; set; } public ControllerControl() { InitializeComponent(); Controller = new Controller(this); } }
Create an instance of the control in the upper-right quadrant of the main page and compile the application. You’ll see design-time data in the main page, and when you run the application you’ll be able to swap the shapes in the control in the upper right.
Design-Time View Models
My favorite method to use because it works directly with the attributes supplied by the runtime is the use of design-time view models. In this approach, the design-time data is supplied using the design-time extensions. The runtime binding is done using an external mechanism. You’ll learn a more elegant way to perform the binding in Chapter 8, “The Managed Extensibility Framework (MEF).” For now, a simple class will perform the necessary wiring.
Create a new control called DesignControl and copy the contents of the controller control (basically everything from the main grid down). Add the following attribute to the LayoutRoot grid to specify the design-time view model:
d:DataContext="{d:DesignInstance Type=ViewModelLocators:DesignViewModel, IsDesignTimeCreatable=True}"
Listing 7.2 shows what the binder might look like—although there is only one entry in the dictionary, you can see how it would be easy to add additional entries mapping the control type to the view type. The dictionary could also reference instances of the view models to share the same one between views, and the wiring of the delegate can be made generic by implementing a common view model interface that specifies the delegate.
Listing 7.2. A View Model Binder
public class Binder { private readonly Dictionary<Type, Type> _bindings = new Dictionary<Type, Type> { { typeof(DesignControl), typeof(ViewModel) } }; public void Bind(Control control) { if (!_bindings.ContainsKey(control.GetType())) { return; } var vm = Activator .CreateInstance( _bindings[control.GetType()]); ((ViewModel) vm).GoToVisualState = state => VisualStateManager .GoToState(control, state, true); control.DataContext = vm; } }
The control can then call the binder and set the default state, like this:
public DesignControl() { InitializeComponent(); new Binder().Bind(this); VisualStateManager.GoToState(this, "Circle", false); }
Declare an instance of this control in the lower-left quadrant and run the application.
Custom Markup Extensions
Custom markup extensions can extend the Xaml model to include your own tags and properties. View model binding is a perfect example of how custom extensions can be used. First, create a control called MarkupControl and copy the content of the controller control (should be just the grid and so on, with no resources and no data context). In the code-behind, just set the default visual state:
public MarkupControl() { InitializeComponent(); VisualStateManager.GoToState(this, "Circle", false); }
Now create a custom markup extension. You can revisit Chapter 3, “Extensible Application Markup Language (Xaml),” to learn more about how to create the extensions. This extension grabs the object root to get a reference to the control that is hosting it. See Listing 7.3 for the full extension. It spins up the view model based on the type of the control, binds the visual state transitions, and returns the view model (in design time, it just returns a new instance of the design view model).
Listing 7.3. A Custom Markup Extension for View Model Binding
public class ViewModelBinderExtension : MarkupExtension { public override object ProvideValue( IServiceProvider serviceProvider) { var targetProvider = serviceProvider .GetService(typeof (IRootObjectProvider)) as IRootObjectProvider; if (targetProvider == null) return null; var targetControl = targetProvider.RootObject as UserControl; if (targetControl == null) return null; if (targetControl is MarkupControl) { if (DesignerProperties.IsInDesignTool) { return new DesignViewModel(); } var vm = new ViewModel { GoToVisualState = state => VisualStateManager.GoToState( targetControl, state, true) }; return vm; } return null; } }
With the extension created, you can add a namespace reference in the control:
xmlns:local="clr-namespace:ViewModelLocators"
Then set the data context of the grid using the extension. It will return the design-time view model in the designer and resolve the runtime view model when you run the application.
<Grid x:Name="LayoutRoot" Background="White"
DataContext="{local:ViewModelBinder}">
Add the control to the lower-right quadrant of the main page and run the application. You will now have four separate controls that perform the same thing using the MVVM pattern. The view model is the same in each case; the only difference is how it is bound to the control.
View-Model-First Approach Versus View-First Approach
All of the examples in this chapter have focused on what is referred to as a view-first approach. The view is instantiated and then the view model is found and bound to the view. I believe this makes sense because most users operate by moving through screens and most developers comprehend their applications in terms of screens. It’s important to note that this is not the only way to manage the application.
Some frameworks such as Caliburn.Micro (http://caliburnmicro.codeplex.com/) follow what is known as a view-model-first approach. In this approach, the application logic spins up view models and the view models determine what views are provided. In a sense the binding is more straightforward because you can use a convention-based approach to map the view to the view model. (Convention-based simply means the bindings can be made based on how you name your classes, so a view is always named SomethingView and a view model is always named SomethingViewModel. Therefore, the Something in common creates the binding.)
The disadvantage to this approach is that it is often not design-time friendly. It is difficult to design the application in Visual Studio or Blend because the designer doesn’t know the right way to spin up views based on the view models. The designers are inherently view-first because you navigate to a Xaml view in order to render the view in the designer.
Although I strongly prefer the view-first approach, the view-model-first frameworks are very popular and have been used in many applications. Proponents of this approach highly prefer it over the view-first approach. It’s not a question of one approach being correct but more a style preference. It makes sense to follow the model that not only is easiest for you to understand, but will also be easy for your team to follow and maintain.