Introduction
Assuming that you have a decent understanding of C#, getting started
in WPF isn't too difficult. I started looking at WPF a while ago, and
didn't find many helpful MVVM tutorials. Hopefully this article
addresses that.
As with learning any new technology, you get the benefit of
hindsight. From my perspective, almost every tutorial on WPF I've come
across is inadequate for one of several reasons:
- The example is all in XAML.
- The example glosses over the key facts that would actually make your life easier.
- The example tries to show off WPF/XAML's capabilities with lots of pointless effects that aren't helping you.
- The example uses classes that have properties that appear far too
similar to framework keywords and classes, and are therefore difficult
to identify in the (XAML) code as being user defined (the
ListBox GroupStyle
's Name
attribute is a complete headache for novices).
So to address this, I've written this based on what I would have
liked to have found as the #1 hit on Google after typing 'WPF Tutorial'.
This article may not be 100% correct, or even do things 'the one true
way', but it will illustrate the main points that I wish I had found in
one place 6 months ago.
I will quickly introduce some topics, then show an example that
explains or demonstrates each point. Accordingly, I haven't really
attempted to make the GUIs pretty, that's not the point of this article
(see the bullet points above).
As this tutorial is quite long, I've elided quite a lot of
code for brevity, so please download the attached zip file, and look at
the examples (.NET 4.0/VS2010). Each example builds on the previous one.
The Basics
- The most important thing about WPF is data binding. In short, you
have some data, typically in a collection of some sort, and you want to
display it to the user. You can 'bind' your XAML to the data.
- WPF has two parts, the XAML which describes your GUI layout and effects, and the code-behind that is tied to the XAML.
- The neatest and probably most reusable way to organise your code is
to use the 'MVVM' pattern: Model, View, ViewModel. This has the aim of
ensuring that your View contains minimal (or no) code, and should be
XAML-only.
The Key Points You Need to Know
- The collection you should use to hold your data is the
ObservableCollection<>
. Not a list
, not a dictionary
, but an ObservableCollection
. The word 'Observable
'
is the clue here: the WPF window needs to be able to 'observe' your
data collection. This collection class implements certain interfaces
that WPF uses.
- Every WPF control (including 'Window's) has a '
DataContext
' and Collection
controls have an 'ItemsSource
' attribute to bind to.
- The interface '
INotifyPropertyChanged
' will be used extensively to communicate any changes in the data between the GUI and your code.
Example 1: Doing It (mostly) Wrong
The best way to start is an example. We will start with a
Song
class, rather than the usual
Person
class. We can arrange songs into Albums, or one large collection, or by Artist. A simple
Song
class would be as follows:
public class Song
{
#region Members
string _artistName;
string _songTitle;
#endregion
#region Properties
public string ArtistName
{
get { return _artistName; }
set { _artistName = value; }
}
public string SongTitle
{
get { return _songTitle; }
set { _songTitle = value; }
}
#endregion
}
In WPF terminology, this is our 'Model'. The GUI is our 'View'. The
magic that data binds them together is our 'ViewModel', which is really
just an adapter that turns our Model into something that the WPF
framework can use. So just to reiterate, this is our 'Model'.
Since we've created a
Song
as a reference type, copies are cheap and light on memory. We can create our
SongViewModel
quite easily. What we need to consider first is, what are we going to (potentially) display? Suppose we just care about the
song
's artist name, not the
song
title, then the
SongViewModel
could be defined as follows:
public class SongViewModel
{
Song _song;
public Song Song
{
get
{
return _song;
}
set
{
_song = value;
}
}
public string ArtistName
{
get { return Song.ArtistName; }
set { Song.ArtistName = value; }
}
}
Except that this isn't quite correct. Since we're exposing a property in our
ViewModel
, we would obviously want a change to the
song
's artist name made in the code to be automatically shown in the GUI, and vice versa:
SongViewModel song = ...;
song.ArtistName = "Elvis";
Notice that in all the examples here, we create our view model *declaratively*, i.e., we do this in the XAML:
<Window x:Class="Example1.MainWindow"
xmlns:local="clr-namespace:Example1">
<Window.DataContext>
-->
<local:SongViewModel />
</Window.DataContext>
This is equivalent to doing this in your code-behind
MainWindow.cs:
public partial class MainWindow : Window
{
SongViewModel _viewModel = new SongViewModel();
public MainWindow()
{
InitializeComponent();
base.DataContext = _viewModel;
}
}
And removing your
DataContext
element in the XAML:
<Window x:Class="Example1.MainWindow"
xmlns:local="clr-namespace:Example1">
-->
This is our view:
Clicking the button does not update anything, because we have not completely implemented data binding.
Data Binding
Remember I said at the start that I would choose a property that stands out. In this example, we want to display the
ArtistName
. I chose this name because it is
NOT the same as any WPF attribute. There are a countless number of examples on the web that choose a
Person
class and then a
Name
attribute (the
Name
attribute
exists on multiple .NET WPF classes). Perhaps the authors of the
articles just don't realise that this is particularly confusing for
beginners (who are, curiously enough, the target audience of these
articles).
There are dozens of other articles about data binding out there, so I
won't cover it here. I hope the example is so trivial that you can see
what is going on.
To bind to the
ArtistName
property on our
SongViewModel
, we simply do this in the
MainWindow.xaml:
<Label Content="{Binding ArtistName}" />
The '
Binding
' keyword binds the content of the control, in this case a
Label
, to the property '
ArtistName
' of the object returned by
DataContext
. As you saw above, we set our
DataContext
to an instance of
SongViewModel
, therefore we are effectively displaying
_songViewModel.ArtistName
in the
Label
.
Once again:
clicking the button does not update anything,
because we have not completely implemented data binding. The GUI is not
receiving any notifications that the property has changed.
Example 2: INotifyPropertyChanged
This is where we have to implement the cunningly named interface:
INotifyPropertyChanged
.
As it says, any class that implements this interface, notifies any
listeners when a property has changed. So we need to modify our
SongViewModel
class a little bit more:
public class SongViewModel : INotifyPropertyChanged
{
#region Construction
public SongViewModel()
{
_song = new Song { ArtistName = "Unknown", SongTitle = "Unknown" };
}
#endregion
#region Members
Song _song;
#endregion
#region Properties
public Song Song
{
get
{
return _song;
}
set
{
_song = value;
}
}
public string ArtistName
{
get { return Song.ArtistName; }
set
{
if (Song.ArtistName != value)
{
Song.ArtistName = value;
RaisePropertyChanged("ArtistName");
}
}
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
#region Methods
private void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
There are several things now happening here. Firstly, we check to see
if we are going to really change the property: this improves
performance slightly for more complex objects. Secondly, if the value
has changed, we raise the
PropertyChanged
event to any listeners.
So now we have a
Model
, and a
ViewModel
. We just need to define our
View
. This is just our
MainWindow
:
<Window x:Class="Example2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Example2"
Title="Example 2" SizeToContent="WidthAndHeight" ResizeMode="NoResize"
Height="350" Width="525">
<Window.DataContext>
-->
<local:SongViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="Example 2 - this works!" />
<Label Grid.Column="0" Grid.Row="1" Content="Artist: " />
<Label Grid.Column="1" Grid.Row="1" Content="{Binding ArtistName}" />
<Button Grid.Column="1" Grid.Row="2" Name="ButtonUpdateArtist"
Content="Update Artist Name" Click="ButtonUpdateArtist_Click" />
</Grid>
</Window>
To test the databinding, we can take the traditional approach and create a button and wire to its
OnClick
event, so the XAML above has a button, and
Click
event, giving the code behind:
public partial class MainWindow : Window
{
#region Members
SongViewModel _viewModel;
int _count = 0;
#endregion
public MainWindow()
{
InitializeComponent();
_viewModel = (SongViewModel)base.DataContext;
}
private void ButtonUpdateArtist_Click(object sender, RoutedEventArgs e)
{
++_count;
_viewModel.ArtistName = string.Format("Elvis ({0})", _count);
}
}
This is ok, but it is not how we should use WPF: firstly, we have
added our 'update artist' logic into our code-behind. It does not belong
there. The
Window
class is concerned with windowing. The
second problem is, suppose we want to move logic in the *button* click
event to a different control, for example, making it a menu entry. It
means we will be cut'n'pasting, and editing in multiple places.
Here is our improved view, where clicking now works:
Example 3: Commands
Binding to GUI events is problematic. WPF offers you a better way. This is
ICommand
. Many controls have a
Command
attribute. These obey binding in the same way as
Content
and
ItemsSource
, except you need to bind it to a *property* that returns an
ICommand
. For the trivial example that we are looking at here, we just implement a trivial class called '
RelayCommand
' that implements
ICommand
.
ICommand
requires the user to define two methods:
bool CanExecute
, and
void Execute
. The
CanExecute
method
really just says to the user, can I execute this command? This is
useful for controlling the context in which you can perform GUI actions.
In our example, we don't care, so we return
true
, meaning that the framework can always call our '
Execute
'
method. It could be that you have a situation where you have a command
bound to button, and it can only execute if you have selected an item in
a list. You would implement that logic in the '
CanExecute
' method.
Since we want to reuse the
ICommand
code, we use the
RelayCommand
class that contains all the repeatable code we do not want to keep writing.
To show how easy it is to reuse the
ICommand
, we bind
the Update Artist command to both a button and a menu item. Notice that
we no longer bind to Button specific Click event, or Menu specific Click
event.
Example 4: Frameworks
By now, if you have read closely, you'll probably notice that a lot
of this is just repetitive code: raising INPC, or creating commands.
This is mostly boilerplate, and for INPC, we can move it to base class
that we call '
ObservableObject
'. For the
RelayCommand
class,
we just move that into our .NET class library. This is how all of the
MVVM frameworks you find on the web begin (Prism, Caliburn, etc.).
As far as the
ObservableObject
and
RelayCommand
classes
are concerned, they are rather basic and are the inevitable result of
refactoring. Unsurprisingly, these classes are practically the same as
those by
Josh Smith.
So we move these classes into a small class library that we can reuse in future.
The view looks much the same as before:
Example 5: Collections of Songs, Doing It Wrong
As I said before, in order to display collections of items in your
View
(i.e. the XAML), you need to use an
ObservableCollection
. In this example, we create an
AlbumViewModel
,
which nicely collects our songs together in something that people
understand. We also introduce a simple song database, purely so we can
quickly produce some song information for this example.
Your first attempt might be as follows:
class AlbumViewModel
{
#region Members
ObservableCollection<Song> _songs = new ObservableCollection<Song>();
#endregion
}
You might think: "I have a different view model this time, I want to display the songs as an
AlbumViewModel
, not a
SongViewModel
".
We also create some more
ICommands
and attach them to some buttons:
public ICommand AddAlbumArtist {}
public ICommand UpdateAlbumArtists {}
In this example, clicking 'Add Artist' works fine. But clicking
'Update Artist Names', fails. If you read the yellow highlighted note on
this
page on MSDN, it explains why:
To fully support transferring data values from binding
source objects to binding targets, each object in your collection that
supports bindable properties must implement an appropriate property
changed notification mechanism such as the INotifyPropertyChanged
interface.
Our view looks like this:
Example 6: Collections of Songs, the Right Way
In this final example, we fix the
AlbumViewModel
to have an
ObservableCollection
of
SongViewModels
that we created earlier:
class AlbumViewModel
{
#region Members
ObservableCollection<SongViewModel> _songs = new ObservableCollection<SongViewModel>();
#endregion
}
Now all our buttons that are bound to commands operate on our collection.
Our code-behind in MainWindow.cs is still completely empty.
Our view looks like this:
Conclusion
Instantiating Your ViewModel
One last point that is worth mentioning is that when you declare your
ViewModel
declaratively in the XAML, you cannot pass it any parameters: in other words, your
ViewModel
must have an implicit, or explicit default constructor. How you add state to your
ViewModel
is up to you. You may find it easier to declare the
ViewModel
in the
MainWindow.cs code-behind, where you can pass in construction parameters.
Other Frameworks
There are lots of other MVVM Frameworks of wildly different complexity and functionality, targeting WPF, WP7, Silverlight, and any combination of the three.
Finally...
Hopefully these six examples show you how easy it is to write a WPF
application using MVVM. I've tried to cover all of the points that I
think are important and often discussed in multiple articles.
If you find this article helpful, please feel to vote it up.
If you find faults in this article, or I've said anything wrong, or
you have some other issue with it, please leave a comment below
explaining why, and how you would fix it.