Introduction To WPF With MVVM
Introduction To WPF With MVVM
Canon
Canon - Any comprehensive list of books within a field.
- dictionary.com
To demonstrate WPF with MVVM I am going to incrementally develop a small application
which allows the user to search an archive of books. The application is called Canon and
the source code [SourceCode] is available for download from my website. I developed
Canon using Visual Studio 2010 and .Net 4, but WPF applications can also be created
with Visual Studio 2008 and .Net 3.5. I have assumed that the reader is following along.
Fire up Visual Studio and create a new WPF Application called Canon. Build and run the
application to make sure everything works correctly, and you should see a very simple
window like the one shown in figure 1:
Finally go into App.xaml and modify the StartupUri attribute of the Application
element so that it reads:
<Application x:Class="Canon.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="View/MainWindow.xaml">
If you made all of these modifications correctly you should get the same window again
when you build and run the application. Now is a good time to add the project to source
control as we will only be adding to the structure from now on, rather than changing it.
App.xml defines the WPF application with the Application element. The first attribute,
x:Class specifies the namespace and name of the corresponding class, which is defined
in App.xml.cs. The next two attributes bring in the necessary namespaces for XAML and
the StartupUri attribute specifies the path to the main window's XAML file. The main
window is the first window displayed by the application on start-up. The
Application.Resource elements are for declaring resources for use within the
application. WPF In Action contains an explanation and several examples of how and
when they can be useful. We'll have a look at resources later when we want to load
images into the Canon application.
namespace Canon
{
public partial class App : Application
{
}
}
App.xaml.cs defines the C# part of the application class. As you can see the class name
and namespace correspond to the name and namespace defined in the x:Class attribute
of the Application element. The App class is partial. Part of it is defined in XAML,
including the inheritance from the Application class, and part in C#. The App class
does not have any fields or values as it is currently completely defined in XAML. We'll want
to change this shortly when we inject a view model.
Now that we understand how a WPF application is defined let's take a look at how a
window is defined by examining MainWindow.xaml and MainWindow.xaml.cs.
3
In the Window element the x:Class attribute specifies the name and namespace of the
corresponding C# class and the next two elements bring in the XAML namespaces. The
Title attribute specifies the title that is displayed in the window and the Height and
Width attributes specify the height and width of the window. The Grid element declares
the type of layout that the window will use to display its controls. I'll explain more about
layouts when we create the UI controls later.
namespace Canon.View
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
MainWindow.xaml.cs defines the code behind the window. The class name and
namespace correspond to the name and namespace defined in the x:Class attribute of
the Window. The MainWindow class is partial as part of it is defined in XAML - including
inheritance from the Window class - and part in C#. Inheriting from Window in the source
file is therefore redundant and can be removed. The MainWindow class's only member is
a constructor which calls InitializeComponents. InitializeComponents behaves
in exactly the same way as it does in a Windows Forms application and initialises the
components defined in MainWindow.xaml.
Injecting a ViewModel
Before we can inject a view model into a view we need an instance of a view to inject it
into. To get the instance of the main window you can remove the StartupUri attribute:
<Application x:Class="Canon.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
...
</Application>
add a constructor to the App class and instantiate an instance of the view there instead. To
actually display the main window you need to call Show on it.
4
of course), you will see exactly the same window. All we've done is move the creation of
the first window from XAML to C#. Now we have an instance of a window to inject a view
model into.
A view model need be nothing more complex than a normal class. It does not require any
special base class, interfaces or members. It's just about the data. Create a project level
folder called ViewModel and create the following class in it (don't forget to add it source
control):
namespace Canon.ViewModel
{
public class MainWindowViewModel
{
}
}
Every WPF view has a DataContext property of type object. This property is null
unless a view model is injected into the view. When a view model is injected WPF sees
that DataContext is no longer null and uses it. We'll cover an example of simple binding
shortly. The DataContext property is also available within the view. This means the view
knows about the view model it has, but the view model continues to know nothing about
the view that's using it. You can Inject the view model into the view by creating an instance
of it and setting the DataContext property on the view:
public partial class App
{
public App()
{
new MainWindow
{
DataContext = new MainWindowViewModel()
}.Show();
}
}
If you run the application again there will be no difference. Something in the view must be
bound to a property in the model to see a difference in the UI.
Once the binding is in place the main window will display the string returned by the
AppTitle property. To bind the window title to the property we have to modify the Title
attribute of the Window element in MainWindow.xml from:
<Window x:Class="Canon.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
to:
<Window x:Class="Canon.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding AppTitle}" Height="350" Width="525">
The curly braces tell WPF that we do not want to display the literal value. The key word
Binding tells WPF we want to bind to a property in the view's view model and AppTitle
is the name of that property. Remember that the x:Class attribute specifies a C# class
and WPF knows it can bind to that class's DataContext property. If you run the
application again now, you will see that the main window's title displays Canon instead of
MainWindow.
This simple Book class contains a unique nullable id for each book, its title, author,
publisher and ISBN. If a Book instance is created with a null id it means that it is a new
book. If the id has a value it means that the book has been saved before.
namespace Canon.Model
{
public interface IBookRepository
{
Book Search(string searchTest);
Book Save(Book book);
}
...
This of course will prevent the project from building. To get it building again we need an
implementing instance of IBookRepository to pass to MainWindowViewModel's
constructor. For this I knocked up a memory based mock implementation:
7
book.Title.ToLower().Contains(searchText) ||
book.Author.ToLower().Contains(searchText) ||
book.Publisher.ToLower().Contains(searchText) ||
book.ISBN.ToLower().Contains(searchText);
Sidebar: SimpleBookRepository
The SimpleBookRepository mock object is fairly straight forward. It persists a list of
books in the books list:
Note that if the SearchFields method returns true the Search method knows it's
found a matching book, stops iterating through the books and returns the current book. Of
course there might be multiple matches, but the Search method only returns the first
match.
The Save method can both update existing books and save new ones. New books are
identified as having a null Id. If the book being saved has an id and is already in the
book list, it is removed. This may seem a little odd. However, if the existing book is just
added to book list it will be in there twice. Also remember that books are compared for
equality by their ids. By the time the bottom of the Save method is reached the book has
an id and does not exist in the book list, so it can be added without fear of duplication.
A SimpleBookRepository instance can be injected into the main window view mode as
follows:
public partial class App
{
public App()
{
new MainWindow
{
DataContext = new MainWindowViewModel( new SimpleBookRepository() )
}.Show();
}
}
and the project will build again. However there's still no change in the main window when
the application is run.
The window starts with a height of 200 and a width of 450 and can be expanded, however
it cannot be contracted below 200 high and 450 wide. You're probably thinking that I could
have just specified the minimums and you're right, I could have. However, the window
would have started off somewhat bigger to begin with. Play around with different sizes until
you get a feel for it.
WPF uses layouts for arranging controls on a UI. WPF layouts are a little bit like Java
layouts. The window in figure 2 consists of a DockPanel layout, a Grid layout and a
StackPanel layout. A DockPanel consists of five sections, top, bottom, left, right and
centre. A section is only visible if a component is put into it. For example you could have a
window with a tool bar across the top, a status bar at the bottom, an explorer view to the
left, a help view to the right and a text editor in the middle. The Canon UI has a toolbar at
the top that holds a StackPanel (another type of layout we'll look at in a minute) which in
turn holds the search box and search button. The remaining space in the DockPanel, the
centre section which gets the components added last to the DockPanel, holds a Grid
layout with the rest of the UI components. To create a DockPanel simply declare it as a
child element of the Window element.
<Window x:Class="Canon.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding AppTitle}"
MinHeight="200"
Height="200"
MinWidth="450"
Width="450">
<DockPanel>
</DockPanel>
</Window>
Next we want to add a tool bar and tell the DockPanel that we want to display it at the
top:
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
</ToolBar>
</ToolBarTray>
</DockPanel>
10
If you run the application again now you will see that the toolbar takes over the whole
client area of the window. We only want it to be a thin strip across the top and we want the
rest of the area to be a Grid layout. All we have to do is add a Grid to the DockPanel:
<DockPanel>
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
</ToolBar>
</ToolBarTray>
<Grid>
</Grid>
</DockPanel>
I'll discuss the Grid layout in more detail once we've completed the toolbar, but I wanted
you to be able to run the application and see the toolbar across the top of the window and
the empty Grid in the remaining client area. You'll notice that the dock position is not
specified for the Grid. You can only specify a dock position of Top, Bottom, Left or
Right. Any panel or control that does not have a dock position specified is placed in the
centre section of the DockPanel. Child ordering effects positioning because the
DockPanel iterates through its child elements in order, setting the position of each
element depending on remaining space.
The toolbar consists of a text box that is used to enter a title, author, publisher or ISBN
number to search for and a button to initiate the search. These can be placed directly into
the ToolBar, but using StackPanel creates a better looking layout:
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<StackPanel Orientation="Horizontal">
<TextBox Margin="5,5,5,5" Width="150"/>
<Button Margin="5,5,5,5" IsDefault="True">Search</Button>
</StackPanel>
</ToolBar>
</ToolBarTray>
A StackPanel stacks its children horizontally or vertically. This is ideal for us as we want
to group the text box and button together in the tool bar. We want them horizontally, so we
set the Orientation attribute to Horizontal. The Vertical orientation could also be
used, but that would look rather odd. To add a TextBox and a Button to the
StackPanel, just declare them as children. Both controls have a Margin attribute which
puts a border around the outside of each control. Each comma delimited number specifies
the spacing around the top, left, bottom and right of the control in that order. The text box's
Width attribute speaks for itself. Without it the text box would be very narrow and would
grow as content was typed into it. Setting the width gives it a sensible starting width and
2 See chapter 2, Working with layouts
11
Rows and columns can be defined just by placing the appropriate empty element (e.g.
<RowDefinition/>) in the appropriate section. This would give the rows and columns a
default height and width and is almost certainly not what you want. Setting the
RowDefinition Height attribute to auto will adjust the height of the row to match the
controls contained in each cell. This is ideal as all the rows in the Canon UI contain a label
and a text box and are therefore all the same height.
The first column contains the labels for all of a book's fields and the second column holds
the text boxes for the values of the fields. The cells in the first column should all be the
same width as the longest label. To achieve this we set the Grid's IsSharedSizeScope
attribute to true (the default is false) and set the first ColumnDefinition's
SharedSizeGroup attribute. The name given to it is unimportant. If we had more than
one column that we wanted to be the same width, we'd specify the same name in all of
those columns. Without using shared size scoping we'd have to set a specific width for the
column. We want the second column to take up the remainder of the UI's width, so we set
its Width attribute value to an asterisk to tell it to stretch out as far as it can. All that's left
is to put the controls into the cells:
<Grid IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition SharedSizeGroup="A"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
12
As you can see, each Label and TextBox has a Grid.Column and Grid.Row attribute
that specifies its position in the Grid. As with a Button, the text for the Labels is specified
between the opening and closing Label elements. Each of the text boxes also has a
Margin set so that there is a reasonable gap between each of them.
Commands
The search text box and search button are closely related (but not coupled!). A user won't
see the result of their search until they have typed something into the text box and clicked
the button. The button shouldn't really be enabled unless there is content in the text box.
To achieve this, we need to bind the text box to a property in the view model and bind the
button to a command. I'll explain a bit more about WPF commands in a moment. To bind
the search text box to a property in the view model, we first need the property:
public class MainWindowViewModel
{
public string SearchText { get; set; }
...
}
As with the AppTitle binding, TextBox binding is a simple case of using curly braces,
the Binding keyword and the name of the property to bind too. The one difference is that
the SearchText property has both a getter and setter. This means that as well as the
value of SearchText being displayed in the search TextBox, any change to the search
TextBox by the user is also written to the SearchText property. This is two way binding
and is worked out by WPF automatically.
WPF has a version of the Command Pattern [CommandPattern]. WPF In Action describes
WPF's implementation of the command pattern in detail and WPF Apps With The ModelView-ViewModel Design Pattern describes an ICommand based implementation that can
be bound when using MVVM 3. What we're interested in is binding a button to a command
so that we can perform an action when that button is pressed and telling that button
whether it should be enabled or not. This is the WPF Apps With The Model-ViewViewModel Design Pattern implementation:
public class RelayCommand : ICommand
{
private readonly Action<object> execute;
private readonly Predicate<object> canExecute;
public RelayCommand(Action<object> execute)
13
: this(execute, null)
Showing how it is used should provide enough explanation of it for our purposes. If you
want to understand it in more detail see WPF Apps With The Model-View-ViewModel
Design Pattern. Some people recommend lazy loading RelayCommand objects:
private RelayCommand _saveCommand;
public ICommand SaveCommand
{
get
{
if (_saveCommand == null)
{
_saveCommand = new RelayCommand(...);
}
return _saveCommand;
}
}
but I really don't see the need. It's a lot of extra code, including a null check and the
property is accessed as soon as the window is displayed and bound anyway. So I just do
this:
public class MainWindowViewModel
{
...
public string SearchText { get; set; }
public ICommand RunSearch{ get; private set; }
public MainWindowViewModel(IBookRepository repo)
{
...
RunSearch = new RelayCommand(o => Search(), o => canSearch() );
14
The getter of the RunSearch property is public so that it can be bound to, but the setter is
private so that it can only be set internally. The RelayCommand object itself is created in
the view model constructor:
RunSearch = new RelayCommand( o => Search(), o => canSearch() );
The first parameter is an Action delegate, which encapsulates a method that has a single
parameter and does not return a value. A lambda expression is used to specify the method
to call when the command is executed. As it's a delegate you could do all sorts of in-line
command implementations, but I find it clearer to delegate to another method. The second
parameter is a Predicate delegate, which represents a method that defines a set of
criteria and determines whether the specified object meets those criteria. A lambda
expression is used to specify a method that determines whether the command should be
enabled. (The o parameter is ignored as it is not needed in this scenario). To determine if
the command should be enabled, we look to see if SearchText is not null or is empty:
private bool canSearch()
{
return !string.IsNullOrEmpty(SearchText);
}
The next stage is to bind the command to the button. This is achieved by by adding a
Command attribute to the search Button element:
<Button Margin="5,5,5,5" IsDefault="True" Command="{Binding RunSearch}">Search</Button>
If you run the application now you will see that the search button is disabled and does not
enable until you enter something into the search text box and it loses focus. This is
because, as with an edit box in a browser, the event which indicates that the contents have
changed is not fired until the text box loses focus. To have the event fired every time the
contents of the text box have changed, we need to modify its binding:
<TextBox Margin="5,5,5,5"
Width="150"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"/>
If you run the application again you will see that the button immediately enables or
15
public
public
public
public
...
string
string
string
string
...
<Label Grid.Column="0" Grid.Row="0">Title:</Label>
<TextBox Grid.Column="1" Grid.Row="0"
Margin="5,5,5,5" Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Column="0" Grid.Row="1">Author:</Label>
<TextBox Grid.Column="1" Grid.Row="1"
Margin="5,5,5,5" Text="{Binding Author, UpdateSourceTrigger=PropertyChanged}}"/>
<Label Grid.Column="0" Grid.Row="2">Publisher:</Label>
<TextBox Grid.Column="1" Grid.Row="2"
Margin="5,5,5,5" Text="{Binding Publisher, UpdateSourceTrigger=PropertyChanged}}"/>
<Label Grid.Column="0" Grid.Row="3">ISBN:</Label>
<TextBox Grid.Column="1" Grid.Row="3"
Margin="5,5,5,5" Text="{Binding ISBN, UpdateSourceTrigger=PropertyChanged}}"/>
16
The Title, Author, Publisher and ISBN text boxes use two way binding just like the search
text box. If a book is found it is used to set the view model's properties:
private void Search()
{
Book book = repo.Search(SearchText);
if (book != null)
{
Title = book.Title;
Author = book.Author;
Publisher = book.Publisher;
ISBN = book.ISBN;
}
}
The above Search method uses the current value of the SearchText property to call
Search on the repository. Remember that the view model's private Search method is
called by the RunSearch command and the command can only be executed if the
SearchText property is not null or empty. So by the time the Search method is called,
SearchText is guaranteed to be valid. If a book is found a valid Book object is returned,
otherwise null is returned. If a valid Book object is returned, its properties are used to set
the view model's properties. However, if you run the application now you will be
disappointed. Even if you enter a matching search criteria and click the search button, you
will not see the Title, Author, Publisher or ISBN text boxes populated. This is because we
haven't told WPF that the properties have changed. WPF will automatically register itself
with a ProperyChanged event if one is provided:
public abstract class PropertyChangeEventBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
This implementation from WPF Apps With The Model-View-ViewModel Design Pattern is
so useful that it's worth putting it in an abstract base class and then inheriting from it in the
view model. This makes the OnPropertyChanged method available to the view model
and when called fires an event with a PropertyChangedEventArgs object containing
the name of the property that has changed. WPF picks this up and uses the appropriate
binding to update the UI. You can see this if you modify the view model Search method as
follows:
private void Search()
{
Book book = repo.Search(SearchText);
if (book != null)
{
Title = book.Title;
Author = book.Author;
Publisher = book.Publisher;
17
Now if you run the application, enter a matching search criteria and click the search button,
you will see that book details are displayed!
Images
Currently the Canon application uses the standard icon in its title bar. It doesn't really
make Canon stand out from any other Windows application. If you look on your (Windows
7 at least) task bar you'll see that all the open applications have an icon. If they all had the
standard icon it would be difficult to tell them apart.
I use free icon libraries, like Silk Icon Set [SilkIcons], available on the internet for icons. I
usual put images into an Images folder at the project level, so create one for the Canon
project. Paste a suitable image (e.g. a 16x16 PNG) for the Canon icon into it and add the
image to the project in the usual way. Make sure its Build Action property is set to
Resource. Adding the image as an icon to the main window is done by setting the Icon
attribute in the Window element:
<Window x:Class="Canon.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="{Binding AppTitle}"
MinHeight="230"
Height="230"
MinWidth="450"
Width="450"
FocusManager.FocusedElement="{Binding ElementName=searchBox}"
Icon="/Canon;component/images/lightbulb.png">
The format of the Icon attribute value is Microsoft Pack URI [PackURI] and consists of the
following tokens:
/Canon The name of the resource file, including its path, relative to the root of the
referenced assembly's project folder.
18
Menus are declared in their parent component with the Menu element. In the case of a
DockPanel they also inherit the DockPanel.Dock attribute which is set to Top to put it in
the same place as the tool bar tray. The order of child elements is important. If you put the
menu below the tool bar tray the menu will appear below the tool bar tray. Add a drop
down menu by adding a MenuItem with the Header attribute set:
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
</MenuItem>
</Menu>
The underscore in front of the F in File specifies that F is the short cut key for the File
menu. To add an item to the drop down menu, add a child MenuItem element:
<Menu DockPanel.Dock="Top">
<MenuItem Header="_File">
<MenuItem Header="_Save" Command="{Binding RunSave}"/>
</MenuItem>
</Menu>
The Header attribute specifies the name of the item and the command binding is the
same as a button command binding. We also need to add the command to the view
model:
public class MainWindowViewModel : PropertyChangeEventBase
{
19
The canSave method just returns true for the time being. We'll put it to better use later.
Menu items can also have images and the same image can be used for a tool bar button
too. You could repeat the location of the image for both the menu item and the tool bar
button, but a better solution is to add a resource:
<DockPanel>
<DockPanel.Resources>
<BitmapImage x:Key="SaveImage" UriSource="/Canon;component/images/disk.png" />
</DockPanel.Resources>
</DockPanel>
This resource is added to the dock panel. Resources can be added to most components
and are in scope within that component and all of its children. Before you add the
DockPanel.Resources element, make sure you add a suitable image, called something
like disk.png, to the images folder the name must match the name specified in
UriSource. You can add all sorts of resources including the BitmapImage shown
above. The x:Key attribute specifies the name that the resource will be referred to by
when it's used by other components. The UriSource attribute is the path to the resource.
It also uses Pack URI.
The MenuItem.Icon and Image child elements are required to add an image to a menu
item:
<MenuItem Header="_Save" Command="{Binding RunSave}">
<MenuItem.Icon>
<Image Source="{StaticResource SaveImage}"/>
</MenuItem.Icon>
</MenuItem>
The image to use is specified by the Source attribute of the Image element which maps
to the x:Key attribute of the resources. The image is bound to the resource, so uses curly
braces. The resource is static as it is known at compile time, so uses the
StaticResource keyword followed by the name of the resource. If you run the
application now you will see the image next to the new menu item. The same image can
be used as a tool bar icon. Add a new tool bar under the existing one. Add a button with a
Command binding to the tool bar and an Image element that binds to the save image.
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<StackPanel Orientation="Horizontal">
...
</StackPanel>
</ToolBar>
<ToolBar>
20
Can you spot the flaw? The Id is not set, which means every time you save a new book
instance will be created, even if it has exactly the same field values as an existing one. To
get around this, we need to keep a reference to the loaded book:
public class MainWindowViewModel : PropertyChangeEventBase
{
...
private Book currentBook;
...
public MainWindowViewModel(IBookRepository repo)
{
...
currentBook = new Book();
...
}
private void Search()
{
var book = repo.Search(SearchText);
if (book != null)
{
currentBook = book;
Title = book.Title;
Author = book.Author;
Publisher = book.Publisher;
ISBN = book.ISBN;
21
OnPropertyChanged("Title");
OnPropertyChanged("Author");
OnPropertyChanged("Publisher");
OnPropertyChanged("ISBN");
}
...
private void Save()
{
currentBook = repo.Save(new Book
{
Id = currentBook.Id,
Title = Title,
Author = Author,
Publisher = Publisher,
ISBN = ISBN
});
}
}
22
and a new Command like RunSave and RunSearch. The difference with RunNew is that it
does not need a canNew method as it is always permitted to create a new book:
RunNew = new RelayCommand(o => New());
You could create a canNew method hard coded to return true for consistency if you
wanted too. The implementation of New looks like this:
private void New()
{
Update(new Book());
}
private void Update(Book book)
{
currentBook = book;
Title = book.Title;
Author = book.Author;
Publisher = book.Publisher;
ISBN = book.ISBN;
OnPropertyChanged("Title");
OnPropertyChanged("Author");
OnPropertyChanged("Publisher");
OnPropertyChanged("ISBN");
}
The Update method is duplication of the code in the Search method, so the Search
method can be refactored to remove the duplication:
private void Search()
{
var book = repo.Search(SearchText);
if (book != null)
{
Update(book);
}
}
If you run the application now you can create, save and search for new books.
System Commands
WPF supports a range of system commands for operations including cutting, copying and
pasting. This means you can add standard functionality without having to implement the
details. For example you can add an edit menu:
<MenuItem Header="_Edit">
<MenuItem Header="Cut" Command="ApplicationCommands.Cut" />
<MenuItem Header="Copy" Command="ApplicationCommands.Copy" />
<MenuItem Header="Paste" Command="ApplicationCommands.Paste" />
</MenuItem>
23
it is not enabled and does not close the application. What is missing is a command binding
and handler methods:
<Window>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Close" Executed="CloseCommandHandler"/>
</Window.CommandBindings>
...
</Window>
The CommandBinding element uses its Command and Executed attributes to map the
Close system command to the CloseCommandHandler handler, which is defined in the
MainMindow class:
private void CloseCommandHandler(object sender, ExecutedRoutedEventArgs e)
{
Close();
}
Clearly CloseCommandHandler just calls the Close method on the window to close it
and consequently the application.
Detecting Changes
Do you remember that earlier on we implemented a not particularly helpful binding for the
Canon window title? Do you also remember the canSave method that always returns
true? It would be far more useful for the user to only be able to save when there were
changes to be saved and for the window title to indicate when there are changes to be
saved:
public string AppTitle
{
get
{
return string.Format("Canon{0}", IsDirty ? " *" : "");
}
}
...
private bool canSave()
{
return IsDirty;
}
24
The IsDirty property compares the current book fields against the equivalent UI fields to
determine if there are any changes. Unfortunately this leads to some more verbose
changes to the UI field properties to get the title and save command to update in real time:
private string title = string.Empty;
public string Title
{
get
{
return title;
}
set
{
title = value;
OnPropertyChanged("Title");
OnChange();
}
}
...
private void OnChange()
{
OnPropertyChanged("AppTitle");
}
I have only shown the changes for the Title property, but the Author, Publisher and
ISBN properties must be changed in the same way. Instead of using the default property
implementation we have to implement our own set method so that when the property is
updated we can tell WPF to also update the window title. This means we also need to
separately store the property value, which is initialised to an empty string to match the
default Book instance, and implement a get method too. One advantage is that we can
also move the WPF notification that the property has changed to the property itself so that
we don't need to remember to call OnPropertyChanged anywhere else in the code
where we assign the property. So the Update method is reduced to:
private void Update(Book book)
{
currentBook = book;
Title = book.Title;
Author = book.Author;
Publisher = book.Publisher;
ISBN = book.ISBN;
}
25
Finally
This is where this article leaves the Canon application. There is more to do, but that falls
outside the scope of an introductory article. Here I introduced you to simple WPF UI
development and the Model-View-ViewModel pattern including simple binding and
commands. Then I demonstrated how to make WPF GUIs more aesthetically pleasing
with the use of images and more user friendly with the use of menus and toolbars and
showed how to implement those menus and toolbars with custom and system commands.
In future articles I will cover unit testing and patterns for maintaining the separation
between the view model and the view when you want to display message boxes and child
windows or use custom controls.
References
[WPFInAction] WPF In Action with Visual Studio 2008 by Arlen Feldman and Maxx
Daymon. Manning. ISBN: 978-1933988221
[Presentation Model] Presentation Model by Martin Fowler:
http://martinfowler.com/eaaDev/PresentationModel.html
[MVVM] WPF Apps With The Model-View-ViewModel Design Pattern by Josh Smith.
MSDN Magazine: http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
[SourceCode] Canon 0.0.1 Source Code: http://paulgrenyer.net/dnld/Canon-0.0.1.zip
[CommandPattern] Design patterns : elements of reusable object-oriented software by
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Addison Wesley. ISBN: 9780201633610
[SilkIcons] Silk Icon Set from Mark James: http://www.famfamfam.com/lab/icons/silk/
[PackURI] Pack URIs in WPF: http://msdn.microsoft.com/en-us/library/aa970069.aspx
26