wpf bootstrapping datatemplates–the chicken and the egg

I saw this post about MVVM binding issues go by on StackOverflow the other day and wanted to post a reply but never got around to it.

The basic issue the poster was dealing with was how to bootstrap a WPF application to load the first view and use a DataTemplate in a ResourceDictionary to define the View that would be displayed. Let’s build this from the “bottom” up as it were.

Define the View and ViewModel
What do you want to see and how should it look?

Let’s start with the ViewModel. The goal in the question was to show that two textboxes in the UI were bound to the same value and would show real-time the updates to the value they shared. So we have defined one public property: TestData that will be displayed/edited in our View.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace WpfTemplateBootstrap
{
/// <summary>
/// ViewModel implementing INotifyPropertyChanged so that View will be notified of changes
/// to property values
/// </summary>
class MainWindowViewModel: INotifyPropertyChanged
{
public MainWindowViewModel()
{
//set an initial value -- makes it easier to see if binding is correct on load
_testData = "hello";
}

private String _testData;
public String TestData
{
get { return _testData; }
set
{
_testData = value;
//must raise property changed with correct property name (case sensitive) for UI to stay in sync
OnPropertyChanged("TestData");

}
}

public void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}

}
public event PropertyChangedEventHandler PropertyChanged;
}
}

Things to note about supporting binding in the ViewModel:

  • It must implement INotifyPropertyChanged.
  • Values that will be bound to must be a public Properties.
  • Each property that is going to be bound in the UI must raise PropertyChanged to publish the fact that its value needs to be refreshed in the UI wherever it is bound.
  • The property name that is passed to the PropertyChangedEventHandler must match the property name exactly, it is case sensitive.
  • Adding default values at first will help show if the binding is working
  • If binding does not seem to be working always check the output window as it will show any binding errors that will help resolve the issue.

Now for the View. I think this is an exact copy of the sample code from the SO question, with one change: it is not a Window but a UserControl, since we want to load this as a View which will be hosted in a containing Window. The TextBox controls are both bound the same Property: TestData. Also, the UpdateSouceTrigger is set to “PropertyChanged” which updates the ViewModel on every key stroke, not just when you tab out, which is the default.

<UserControl x:Class="WpfTemplateBootstrap.MainWindowView"
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"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<TextBox Height="23" HorizontalAlignment="Left" Margin="61,14,0,0"
Name="textBox1" VerticalAlignment="Top" Width="120"
Text="{Binding Path=TestData, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
/>
<Label Content="Test:" Height="28" HorizontalAlignment="Left" Margin="12,12,0,0"
Name="label1" VerticalAlignment="Top" Width="43" />
<Label Content="Result:" Height="28" HorizontalAlignment="Left" Margin="10,46,0,0"
Name="label2" VerticalAlignment="Top" />

<TextBox Height="23" HorizontalAlignment="Left" Margin="61,48,0,0"
Name="textBox2" VerticalAlignment="Top" Width="120"

Text="{Binding Path=TestData, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
</Grid>

</UserControl>

Simple View/ViewModel loading

Now that we have defined what we want to see in the UI, we need to somehow get this loaded in our UI when the application starts up. The simplest path to this goal would be to just add an instance of our MainWindowView to our MainWindow.xaml.

<Window x:Class="WpfTemplateBootstrap.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTemplateBootstrap"
Title="MainWindow" Height="350" Width="525">
<local:MainWindowView />
</Window>

In order for this to work, somebody has to load up an instance of the MainWindowViewModel to be the data context for the View. Without this there is no object for the data binding engine to wire up to. In this simple model, the View code behind is “ViewModel aware” (gasp!) and loads up an instance of the correct ViewModel when it is initialized like this.

public partial class MainWindowView : UserControl
{
private MainWindowViewModel _vm;

public MainWindowView()
{
InitializeComponent();

_vm = new MainWindowViewModel();
this.DataContext = _vm;
}
}

One of the things about this question that caught my interest was that it seems the poster had tried this approach first, and had it working, but wanted to achieve a “cleaner” separation of logic from UI and so went to the next step of using DataTemplates to define the View so that the View could be completely agnostic as to the loading and type of the ViewModel. A noble goal to be sure, so let’s wire up that approach …

Template the ViewModel so it has a defined UI presence

With this approach we are going to turn things around. Instead of explicitly creating a View that we want to display in the UI and then trying to figure out how to get that View wired up to a ViewModel instance. We are going to add an instance of the ViewModel to the UI, and let the WPF framework figure out how to represent that object. So, let’s change our MainWindow. Using a ContentControl we can add our ViewModel to the UI, even though it is not a UI type itself.

<Window x:Class="WpfTemplateBootstrap.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTemplateBootstrap"
Title="MainWindow" Height="350" Width="525">
<ContentControl>
<ContentControl.Content>
<local:MainWindowViewModel />
</ContentControl.Content>
</ContentControl>
</Window>

If we run our application now, WPF will do its best and call ToString() on our object and show the results in the UI. Pretty cool, but not very useful.

ViewModelToString

So, next we define a DataTemplate. Think of a DataTemplate as a way of saying to WPF: “when you run into an instance of this Type that you need to display, this it what it looks like …” We could do this right in the MainWindow.xaml, but one goal of the question was to use a ResourceDictionary. This comes in handy when the Type will be displayed in different Windows in our application. So to set this up, add a ResourceDictionary to our project. Also reference that dictionary in the App.xaml file so that it is scoped, and available, to the entire application.

<!-- MainResourceDictionary.xaml file -->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTemplateBootstrap"
>

<DataTemplate DataType="{x:Type local:MainWindowViewModel}">
<local:MainWindowView />
</DataTemplate>

</ResourceDictionary>


<!-- App.xaml file -->
<Application x:Class="WpfTemplateBootstrap.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<!-- our application scope ResourceDictionary refs go here-->
<Application.Resources>
<ResourceDictionary Source="MainResourceDictionary.xaml" />
</Application.Resources>
</Application>

We can remove the code behind we added to the MainWindowView.xaml.vb now, since the View is no longer responsible for loading the ViewModel. We can run the application and see that our bootstrap and binding works.

bootstrapDone

Comments (1) -

coldSense
coldSense
1/8/2013 3:15:45 AM #

Very good explanation! Thanks! You answered to my question! In meantime I found another solution but yours is even better. I'll post mine to stackoverflow as soon as possible just to show to others a variant.

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About me

.NET developer in upstate NY, USA
Current focus technologies: WPF, WCF
Intrigued by: Functional programming ala F#, Code Analysis, Math
Hobbies: this blog, go figure

Month List