Source for this entry is available at http://www.cavinconsulting.com/Code/Scribble2.zip.
OK. Up to this point we've managed to get an application running and it actually acts a little like the old MFC Scribble tutorial. Let's continue on this path adding some functionality to the application. The original tutorial starts out by designing a document class named CScribbleDoc. The goal of this class is to store a collection of stroke objects in a serializable manner (can load from and save to disk). This was there to introduce the user to the MFC serialization framework and also because MFC didn't have anything like the new Ink Canvas control.
Without sparking a religious debate about whether the data should be stored in a UI object (the InkCanvas) or in a more tier-friendly business object (CScribbleDoc), I'm going to defer adding the complexity of a document object. I plan on adding some discussion on the different forms of document serialization in future installments. Feel free to agree or disagree, this is just the path I plan on taking.
Today, we add the ability to load and save your scribbled masterpiece. The first step is to add menu items for New, Open, Save, and Save As. To do this we could open window1.xaml in the designer select the File menu item and edit the Items collection in the properties window, but the version of the designer I have (codename "Cider," November CTP) fails with all sorts of cryptic messages. So, we resort to editing the xaml by hand. We turn this:
<MenuItem Header="_File">
<MenuItem Header="E_xit"
InputGestureText="Alt+F4"
Click="OnFileExit"
ToolTip="Click here to exit"
Name="FileExit"/>
</MenuItem>
</MenuItem>
into this:
<MenuItem Header="_File">
<MenuItem Header="_New"
Name="fileNew" />
<MenuItem Header="_Open"
Name="fileOpen" />
<MenuItem Header="_Close"
Name="fileClose"/>
<MenuItem Header="_Save"
Name="fileSave" />
<MenuItem Header="Save _As..."
Name="fileSaveAs"/>
<Separator />
<MenuItem Header="E_xit"
InputGestureText="Alt+F4"
Click="OnFileExit"
ToolTip="Click here to exit"
Name="fileExit"/>
</MenuItem>
Compiling and running at this point generates a nice looking menu complete with some of the keyboard shortcuts we all know and love (I love them, anyway -- once you take the time to learn them, you'll love them, too). I say some of the keyboard shortcuts because I can use Alt+f,s to save (that's what I have used ever since the old DOS text editor), but I can't use Ctrl+s. So far as I can tell, there is no way to attach a shortcut combination directly to a menu item. We will ignore this problem for now...
Of course, what are menu items without something happening when the user clicks on them? We can implement these commands using the same event handler mechanism we used for the Exit menu item. The resulting xaml looks something like this:
<MenuItem Header="_File">
<MenuItem Header="_New"
Name="fileNew"
Click="OnFileNew"
ToolTip="Creates a new Scribble document" />
<MenuItem Header="_Open"
Name="fileOpen"
Click="OnFileOpen"
ToolTip="Opens an existing Scribble document" />
<MenuItem Header="_Close"
Name="fileClose"
Click="OnFileClose"
ToolTip="Closes the current Scribble document" />
<MenuItem Header="_Save"
Name="fileSave"
Click="OnFileSave"
ToolTip="Saves the current Scribble document" />
<MenuItem Header="Save _As..."
Name="fileSaveAs"
Click="OnFileSaveAs"
ToolTip="Saves the current Scribble document under a different name"/>
<Separator />
<MenuItem Header="E_xit"
InputGestureText="Alt-F4"
Click="OnFileExit"
ToolTip="Click here to exit"
Name="fileExit"/>
</MenuItem>
and the resulting code behind will have this:
public void OnFileNew(object sender, RoutedEventArgs e)
{
// TODO:
}
public void OnFileOpen(object sender, RoutedEventArgs e)
{
// TODO:
}
public void OnFileClose(object sender, RoutedEventArgs e)
{
// TODO:
}
public void OnFileSave(object sender, RoutedEventArgs e)
{
// TODO:
}
public void OnFileSaveAs(object sender, RoutedEventArgs e)
{
// TODO:
}
public void OnFileExit(object sender, RoutedEventArgs e)
{
this.Close();
}
Of course, windows application is complete without a toolbar (except Office 2007 -- it's too good for a toolbar). Most applications have toolbar buttons for common operations. In our list of operations above, we will have a toolbar button for New, Open, and Save. In order to add a toolbar to our UI, we will need another row in our master layout grid. The Height property of the new row will be set to "Auto" and we will put a ToolbarTray control inside the new row. A ToolbarTray is a WPF control that allows one or more toolbars to be contained inside of it and allows the standard drag and drop actions we have all come to expect from windows applications. The interesting part of window1 xaml looks like this:
<ToolBarTray Grid.Row="1">
<ToolBar>
<Button ToolTip="Creates a new Scribble document"
Click="OnFileNew"
Name="fileNewButton">
<Image Source="Resources/new.png" />
</Button>
<Button ToolTip="Opens an existing Scribble document"
Click="OnFileOpen"
Name="openFileButton">
<Image Source="Resources/open.png" />
</Button>
<Button ToolTip="Saves the current Scribble document"
Click="OnFileSave"
Name="saveFileButton">
<Image Source="Resources/save.png" />
</Button>
</ToolBar>
</ToolBarTray>
The first thing to note with this code is that the tooltips and event handlers are duplicated between the menu item and the corresponding toolbar button. The fact that they use the same event handler makes a lot of sense. After all, how many times do you want to write the save logic. The duplication of the tooltip, on the other hand, indicates the likelihood for spelling mistakes and information inconsistencies to creep into your delivered product. Additionally, life becomes very difficult if you decide to localize your application to elbonian.
The second thing to note is that the paths to the icon buttons are paths relative to the installation directory of the application. This means, in order to deploy the application, you have to distribute the icons also. In order for life to work well in the development environment, you'll have to add a build event to copy the resources to the bin directory. Since this is an unacceptable situation, we will fix this before addressing the duplication between the menus and toolbars.
To permanently attach images into our application, I am going to use the Resources tab under our project properties. Right-click on the project, select Properties and click on the Resources tab. Click Add Resource and choose Add Existing File. Search for a bitmap you would like to use for the images on the button (I find the VS2005 image library extremely useful for this), and click Open. This will copy the images to a Resources directory within your project. It will also auto-generate a class allowing you to access these images from code. It does not, however, create any way for you to access these images from the designer. You can, however, mark the newly imported images as resources for the application by changing the Build Action to Resource. I'm not sure if this embeds two copies of the same bitmap in the final assembly or not, but it appears to work.
Now, we address the code duplication between the menu items and the toolbar buttons. It turns out that the clever designers of WPF already took this into account and solved the problem by using a Command Pattern. Without going too deeply into what the Command Pattern is, let me say that it simplifies the plumbing in between what gets done (like loading a file) and all of the various ways a user is able to trigger that thing happening (like File->Open or clicking on the toolbar). Now, for the Patterns and Practices purists out there, this explanation is completely inadequate and quite possible slightly untrue. To them, never mind. To the rest of us, let us see how this applies.
Instead of hooking up event handlers directly from a button or a menu item to a specific event handler in our application, the kind folks over at WPF have created a series of commands we can use to perform the same task. "What's the difference," you might ask. The difference is that the command handles a whole bunch of the common properties necessary to be useful from anywhere (button, menu, mouse gesture, key gesture, etc.). Here's how it works. Instead of defining a button like this:
<Button ToolTip="Creates a new Scribble document"
Click="OnFileNew"
Name="fileNewButton">
<Image Source="Resources/new.png" />
</Button>
We can define it like this:
<Button Command="ApplicationCommands.New">
<Image Source="Resources/new.png" />
</Button>
Who cares? What does this buy us? Well, we can also define the File->New menu item like this:
<MenuItem Command="ApplicationCommands.New" />
What does that get us? For starters, there is no mention of the literal string "New" in there anywhere. That's right, the ApplicationCommand.New class takes care of describing itself to the file menu item and the toolbar button. Moreover, the command class is multilingual and automatically translates itself depending on the locality of the machine it is running on. Also, the command responds to standard input gestures (that's what they like to call keyboard shortcuts now). So, the File->New menu item proudly displays Ctrl+N next to it in the menu, and is invoked when the user types Ctrl+N on the keyboard.
So, we can rewrite our menu and toolbar sections to look like this:
<DockPanel Grid.Row="0" Name="dockPanel1">
<Menu Name="menu1">
<MenuItem Header="_File">
<MenuItem Command="ApplicationCommands.New" />
<MenuItem Command="ApplicationCommands.Open" />
<MenuItem Command="ApplicationCommands.Close" />
<MenuItem Command="ApplicationCommands.Save" />
<MenuItem Command="ApplicationCommands.SaveAs" />
<Separator />
<MenuItem Header="E_xit"
InputGestureText="Alt-F4"
Click="OnFileExit"
ToolTip="Click here to exit"
Name="fileExit"/>
</MenuItem>
</Menu>
</DockPanel>
<ToolBarTray Grid.Row="1">
<ToolBar>
<Button Command="ApplicationCommands.New">
<Image Source="Resources/new.png" />
</Button>
<Button Command="ApplicationCommands.Open">
<Image Source="Resources/open.png" />
</Button>
<Button Command="ApplicationCommands.Save">
<Image Source="Resources/save.png" />
</Button>
</ToolBar>
</ToolBarTray>
If you compile and run now, you'll see all of the same things you saw before with a couple of exceptions. First, all of the commands are disabled except the exit command. Second, there aren't any tooltips. We'll solve this problem shortly. Since tooltips were one of the main reasons I gave for not using the previous version, I'll do my best to address that in the next segment.
In order to attach the application commands to our existing event handlers, we have to add a mapping between the commands and the methods we will use to execute them. Just above the first Grid definition, add the following:
<Window.CommandBindings>
<CommandBinding Command="New" Executed="OnFileNew" CanExecute="CanAlwaysExecute"/>
<CommandBinding Command="Open" Executed="OnFileOpen" CanExecute="CanAlwaysExecute"/>
<CommandBinding Command="Close" Executed="OnFileClose" CanExecute="CanAlwaysExecute"/>
<CommandBinding Command="Save" Executed="OnFileSave" CanExecute="CanAlwaysExecute"/>
<CommandBinding Command="SaveAs" Executed="OnFileSaveAs" CanExecute="CanAlwaysExecute"/>
</Window.CommandBindings>
Add the following method to the code behind:
void CanAlwaysExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
These two things are all it takes to hook up our menu items and our commands to our command handlers. "So what," you ask yourself, "Is this any better than what we had before?" That's a valid question. On one hand, I can now save my document with Ctrl+s. On the other hand, I can no longer save my document with Alt+f,s. Great. I've automatically added keyboard shortcuts (automatically localized to the client machine), but I have lost the ability to set Access Keys. We've added a level of indirection between the menus/buttons to the commands that get processed, and we've encapsulated the text seen by the user into a self-localizing package. Unfortunately, with the additional layer, I have no idea where to look to add back my keyboard shortcut thing (it used to be called mnemonic). We may have to skip this for now.
In the next segment, we'll take a look at making the Exit menu item look like the rest of the menu items. I just hate inconsistency.