Saturday, August 18, 2018

WPF: Ways to Set DataContext

Something that I've struggled when implementing Model-View-ViewModel design in WPF is how to specify DataContext so that I can bind control properties to view model properties. There are quite a few ways to do this, and each have trade-offs in terms of Blendability (design-time functionality in Blend or Visual Studio Designer) and maintainability.

Set in Code-Behind

When I first started doing MVVM in WPF, I used code behind to set the DataContext in all of my windows and pages:


public MyControl()
{
    InitializeComponent();
    DataContext = new MyViewModel();
}


Blendability with this approach is poor. The designer can't actually see that your DataContext is a MyViewModel instance. AFAIK, designers don't run that constructor at design time. (If they do, I'd really be surprised because I've never seen it happen.) So you don't get auto-completion, go-to-definition, or anything else in the designer.

But there is an advantage to the approach: you can design view models that take constructor parameters. This makes it easy to write view models that wrap around model objects as you can pass that model to the view model's constructor.

Set in XAML Using Locator Instance


After a few years, I started using MVVM Light instead of utilizing hand-rolled solutions for MVVM. Installing MVVM Light's full package (MvvmLight, not MvvmLightLibs) adds a few classes to your project. One of these is the ViewModelLocator class, which is responsible for instances of your view model classes. If you add an instance of this locator class to your application's resources in App.xaml, you can set your control's DataContext in XAML:


<UserControl
    x:Class="MyProject.MyControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d"
    DataContext="{Binding Source={StaticResource Locator}, Path=MyViewModel}">
</Code>


This approach offers far superior Blendability over the previous approach - you get auto-complete, extended error reporting, and design-time preview in Visual Studio Designer. This makes for a much faster feedback loop than running the program every time.

It's still possible to design your view models so that their constructors take parameters. However, this is effectively limited to dependency injection as the locator pattern makes it difficult to send a model object to the view model's constructor.

If you want, you can still load your view model with model-specific data. My typical methodology involves handling the control's Loaded event. The event handler can call a 'load' method on the view model, and you can get the view model instance either from the locator or by casting the value for the control's DataContext property to the correct type.

But if you use loading methods, expect to call them on a regular basis: the locator class can make the lifetime of your view models difficult to reason with. In other approaches, the life of the view model instance ends with the view that created it. But view models in the locator can live to the end of the application (if you keep a reference to each instance) or can change when you least expect it (if your locator's properties return a new instance every time).

You have to change the locator, the view XAML, and the view code-behind every time you want to use a different class for the view model. In my experience, it doesn't come up often, but it can still break unexpected parts of your application after making the change.

Set in XAML, Create New Instance in XAML


I recently learned about a third approach from a question on StackOverflow: Setting DataContext in XAML in WPF. The accepted answer from BradleyDotNET does something like this in the XAML:


<UserControl.DataContext>
    <vm:MyViewModel />
</UserControl.DataContext>


Like the previous method, you get auto-complete and errors during design time. And, if you assign a name to the view model instance, you can access it from code-behind without needing to cast anything. It just works, and it's an elegant solution as far as the code-behind goes.

There is a big downside: the view model class must have a default constructor. It's technically possible to get around it using a type converter, but you lose the ability to preview data, it leads to a lot of confusing code, and I wouldn't recommend it. Implementing a default constructor means having to inject dependencies through properties or put all of the DI code in the default constructor. This can lead to some messy code in your view model that can complicate your unit tests.

Set in XAML, Create New Instance in Code-Behind


While doing research for this article, I found that there's another way to assign a value to DataContext that involves writing some code-behind and XAML. But I don't recommend using it for reasons that will soon become apparent:

You can declare a property in the code-behind:


public MyViewModel ViewModel { get; } = new MyViewModel();


and use it in your XAML:


<Window x:Class="TestApp.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:TestApp"
    mc:Ignorable="d"
    Title="MainWindow"
    x:Name="Self"
    DataContext="{Binding ElementName=Self, Path=ViewModel}">


It seems like you would get design-time support with this approach, but this works almost as poorly at design-time as the code-behind approach. You can hover over property names and see their type, but you can't jump to their definition or preview the bindings in the designer. I understand why preview wouldn't work - the designer is not using an instance of the view model. But it seems weird that type definitions only partially work.

Another big downside is that I've never heard anyone talk about setting DataContext this way. It's a non-standard approach, and would probably receive some funny looks during a code review.

For those two reasons, I can't recommend actually using this hybrid approach. But it's kinda fun to think about.

Conclusion


That leaves me with three usable approaches for setting the DataContext for your windows, pages, and controls:

  • Set in Code-Behind
  • Set in XAML Using Locator Instance (the MVVM Light method)
  • Set in XAML, Create New Instance in XAML

They all have their benefits and problems, but if I had to choose one to use for the rest of my life, it would be MVVM Light's approach. With it, Blendability is excellent, code follows a commonly-used pattern, and you can still do dependency injection through view model constructors.

No comments:

Post a Comment