[C# Helper]
Index Books FAQ Contact About Rod
[Beginning Database Design Solutions, Second Edition]

[Beginning Software Engineering, Second Edition]

[Essential Algorithms, Second Edition]

[The Modern C# Challenge]

[WPF 3d, Three-Dimensional Graphics with WPF and C#]

[The C# Helper Top 100]

[Interview Puzzles Dissected]

[C# 24-Hour Trainer]

[C# 5.0 Programmer's Reference]

[MCSD Certification Toolkit (Exam 70-483): Programming in C#]

Title: Use menu commands with shortcuts to save and restore user-drawn polygons in C# and WPF

[Use menu commands with shortcuts to save and restore user-drawn polygons in C# and WPF]

The example Let the user edit polygons in WPF and C# shows how to let the user draw and edit polygons in WPF. This example shows how you can let the user save and restore those polygons.

While I was building this program, I wanted to use a menu to provide New, Open, and Save As commands. To make using the menu items easier, I wanted to let the user fire those commands by pressing Ctrl+N, Ctrl+O, and Ctrl+S. In addition to showing how to save and restore the user's polygons, this post explains how to provide those commands and provide shortcuts for them.

XAML Code

Probably the easiest way to arrange the controls in a WPF program that uses menus is to use a DockPanel instead of a Grid as the window's top-level container. Then you can dock the menu to the top of the panel and dock other controls below that.

The following XAML code shows how this example defines its window.

<Window x:Class="howto_wpf_serialize_polygon.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="howto_wpf_serialize_polygon" Height="300" Width="300"> <Window.CommandBindings> <CommandBinding Command="New" CanExecute="NewCommand_CanExecute" Executed="NewCommand_Executed" /> <CommandBinding Command="Open" CanExecute="OpenCommand_CanExecute" Executed="OpenCommand_Executed" /> <CommandBinding Command="SaveAs" CanExecute="SaveAsCommand_CanExecute" Executed="SaveAsCommand_Executed" /> </Window.CommandBindings> <Window.InputBindings> <KeyBinding Gesture="Ctrl+S" Command="SaveAs" /> </Window.InputBindings> <DockPanel> <Menu Height="22" Margin="2" DockPanel.Dock="Top"> <MenuItem Header="_File"> <MenuItem Command="New" /> <MenuItem Command="Open" /> <MenuItem Command="SaveAs" InputGestureText="Ctrl+S" /> <Separator /> <MenuItem Name="mnuFileExit" Header="_Exit" Click="mnuFileExit_Click" /> </MenuItem> </Menu> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <RadioButton Content="Draw" Margin="5" Name="radDraw" IsChecked="True" /> <RadioButton Content="Edit" Margin="5" Name="radEdit" Click="radEdit_Click" /> </StackPanel> <Border DockPanel.Dock="Bottom" Name="border1" Margin="10,0,10,10" BorderBrush="Gray" BorderThickness="1"> <Canvas Name="canDraw" Background="Transparent" ClipToBounds="True" MouseDown="canDraw_MouseDown" MouseMove="canDraw_MouseMove" MouseUp="canDraw_MouseUp"/> </Border> </DockPanel> </Window>

For the moment, ignore the Window.CommandBindings and Window.InputBindings sections. After those, the XAML code defines a DockPanel. The DockPanel contains a Menu that holds several MenuItem objects. Notice that the Menu is docked to the top of the DockPanel.

Below the menu, the DockPanel holds a StackPanel that contains the Draw and Edit radio buttons. The StackPanel is also docked to the top of the DockPanel, so it occupies the top of the panel's area that is not used by the Menu.

The last control inside the DockPanel is the Canvas control on which the user draws. (See the earlier post for information about how the program uses this control.) Notice that the Canvas control is not docked. You can dock it if you like, but by default the DockPanel control's final child fills any remaining space inside the panel.

Standard Commands

WPF uses a special commanding system to process user commands. The idea is that several different actions such as menus, shortcuts, and buttons might all trigger the same actions. The program can bind all of those controls to a single command object that then executes your code when it is invoked.

This follows the common WPF theme of "Twice as flexible and only five times as hard!" In this instance, however, this is only about three times as hard as it is in Windows Forms applications, at least for the standard commands.

You can make your own commands (back to "only fives times as hard"), but WPF provides a set of standard commands for common tasks such as Open, Copy, AlignCenter, and Zoom. Those commands are a bit easier to use.

Using the standard commands requires four pieces: creating command bindings, creating input bindings, creating controls that use the commands, and executing the commands.

Creating Command Bindings

Command bindings tell a control how to map commands to methods in the code behind. This example's window uses the following command binding section.

<Window.CommandBindings> <CommandBinding Command="New" CanExecute="NewCommand_CanExecute" Executed="NewCommand_Executed" /> <CommandBinding Command="Open" CanExecute="OpenCommand_CanExecute" Executed="OpenCommand_Executed" /> <CommandBinding Command="SaveAs" CanExecute="SaveAsCommand_CanExecute" Executed="SaveAsCommand_Executed" /> </Window.CommandBindings>

Each command binding has a command such as New or Open. Each binding identifies two methods for its command, one method to execute to determine whether the command should be enabled and one to execute to perform the command. For example, the program calls the NewCommand_CanExecute method to determine whether the New command should be allowed. It calls the NewCommand_Executed method when the user executes the command.

If the NewCommand_CanExecute method returns false (through its parameters; you'll see that shortly), then the program automatically disables the controls that are bound to the command. In this example, it would disable the New menu item and the Ctrl+N shortcut.

At this point, the program knows what methods to execute for the New, Open, and SaveAs commands. Next we need to assign controls to trigger those commands.

Creating Input Bindings

Input bindings let you define gestures to fire commands. This example uses the following code to make input bindings.

<Window.InputBindings> <KeyBinding Gesture="Ctrl+S" Command="SaveAs" /> </Window.InputBindings>

This code makes the gesture Ctrl+S fire the SaveAs command.

The program doesn't need input bindings for the New and Open commands because they are automatically bound to the Ctrl+N and Ctrl+O shortcuts.

Creating Controls That Use the Commands

To make a control trigger a command, you set the control's Command property. The following code shows how this example attaches commands to menu items.

<MenuItem Header="_File"> <MenuItem Command="New" /> <MenuItem Command="Open" /> <MenuItem Command="SaveAs" InputGestureText="Ctrl+S" /> <Separator /> <MenuItem Name="mnuFileExit" Header="_Exit" Click="mnuFileExit_Click" /> </MenuItem>

[Use menu commands with shortcuts to save and restore user-drawn polygons in C# and WPF]

Notice that this XAML code does not explicitly assign text for the menu items. These standard commands come with standard text and shortcuts. For example, a menu item attached to the New command automatically displays the text New and its shortcut Ctrl+N as shown in the picture on the right.

You can override a menu's text by setting its Header property. It's somewhat strange that the default commands do not define accelerator keys to let you navigate menus by pressing the Alt key. You can add that capability by including the Header property and adding the accelerators as in Header="_New". (I didn't do that for this example.)

The SaveAs command does not have a pre-defined shortcut, so the program's XAML code for that menu item explicitly gives it the input gesture Ctrl+S. This makes the menu item display Ctrl+S as shown in the earlier picture.

The InputGestureText property makes the menu item display the shortcut text Ctrl+S, and the KeyBinding in the Windows.InputBindings section described earlier makes Ctrl+S fire the SaveAs command, but those are two completely separate things. If you omit the input gesture, then Ctrl+S will still fire the action, it just won't appear in the menu item. Conversely if you omit the input gesture, then the menu item will display the shortcut Ctrl+S, but that key sequence won't do anything. Both off those situations could be confusing to the user, so I suggest that you do both or neither.

Now the program knows what methods to execute when a command fires and it has menus and gestures to fire those commands. The final piece to the example's basic commanding is the actual code that the commands execute.

Executing the Commands

The methods that the commands execute are relatively straightforward. The following code shows the methods for the New command.

private void NewCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = true; } private void NewCommand_Executed(object sender, ExecutedRoutedEventArgs e) { canDraw.Children.Clear(); }

The CanExecute method sets its e.CanExecute parameter to indicate whether the command should be allowed. The program calls this method whenever it might need to enable or disable the controls that are attached to the command. This example's CanExecute methods simply return true so the commands are always allowed.

The Executed method does whatever is necessary for the command. The New command simply clears the canDraw Canvas control's Children collection to remove any previously drawn polygons.

The following section describes the other commands' Execute methods.

Saving and Loading Polygons

The main new feature of this example is the ability to save and restore drawn polygons. Normally we might like to serialize and deserialize the program's Canvas control or perhaps its Children collection. Unfortunately the Canvas class is not marked as serializable. Fortunately WPF provides us a relatively simple alternative: the XamlWriter and XamlReader classes. The XamlWriter class can generate XAML code that creates a given WPF control. Conversely the XamlReader class can read XAML code and use it to create a control.

The following code executes when the SaveAs command fires.

SaveFileDialog sfdSerialization = new SaveFileDialog(); private void SaveAsCommand_Executed(object sender, ExecutedRoutedEventArgs e) { sfdSerialization.Filter = "DRW Files (*.drw)|*.drw|All files (*.*)|*.*"; sfdSerialization.DefaultExt = "drw"; if (sfdSerialization.ShowDialog().Value) { string xaml = XamlWriter.Save(canDraw); File.WriteAllText(sfdSerialization.FileName, xaml); } }

This code creates a SaveFileDialog at the form level. The SaveAsCommand_Executed method sets the dialog's filter and default extension, and then displays the dialog.

If the user selects a file and clicks Save, the dialog returns true. It returns the value through a nullable boolean, so we must use its Value property to get the actual result.

If the dialog returns true, the program uses the XamlWriter class's static Save method to get XAML code for the canDraw Canvas control. It then uses File.WriteAllText to save the XAML code into the file that the user selected.

The following text shows what this file might look like.

<Canvas Background="#00FFFFFF" Name="canDraw" Cursor="{x:Null}" Cli pToBounds="True" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/p resentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> ;<Polygon Points="142,32.04 70,127.04 220,131.04 99,39.04 116,157.0 4 220,56.04 58,81.04 178,161.04" Stroke="#FF0000FF" StrokeThickness="2 " /><Polygon Points="77,21.04 18,118.04 92,181.04 241,172.04 255 ,68.04 161,10.04" Stroke="#FF0000FF" StrokeThickness="2" /></Can vas>

The code is written all on one line so it's kind of hard to read, but if you look closely you can see the polygon's points and stroke properties.

The following code executes when the Open command fires.

OpenFileDialog ofdSerialization = new OpenFileDialog(); private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e) { ofdSerialization.Filter = "DRW Files (*.drw)|*.drw|All files (*.*)|*.*"; ofdSerialization.DefaultExt = "drw"; if (ofdSerialization.ShowDialog().Value) { string xaml = File.ReadAllText(ofdSerialization.FileName); canDraw = (Canvas)XamlReader.Parse(xaml); border1.Child = canDraw; canDraw.MouseMove += canDraw_MouseMove; canDraw.MouseDown += canDraw_MouseDown; canDraw.MouseUp += canDraw_MouseUp; } }

This code creates an OpenFileDialog at the form level. The OpenCommand_Executed method displays the dialog. If the user selects a file and clicks Open, the program uses File.ReadAllText to read the XAML code from the selected file. It then uses the XamlReader class's static Parse method to convert the XAML code into a Canvas control and saves it in the variable canDraw.

Note that the canDraw variable is the one created by the Window Designer generated code. That's just the name of the control originally defined by the window's XAML code.

The program places the new control inside the window's Border control border1.

The window's original XAML code wired the canDraw control up to some event handlers, so the program must do the same for the newly created canDraw control or else the user will no longer be able to create and edit polygons.

Refining CanExecute

At this point, the program can handle the New, Open, and SaveAs commands, more or less. Unfortunately the new commands cause some unexpected results if you invoke them while you are in the middle of editing a polygon. For example, suppose you are creating a new polygon, you click three points, and then you press Ctrl+N. At that point program clears the canDraw control's Children collection so the polygon that you are drawing is no longer visible, but the control still has a mouse capture and the program is still tracking mouse clicks to finish drawing the polygon. It seems as if nothing is happening until you press the right mouse button to finish creating the new polygon and that polygon suddenly appears.

Similarly if you press Ctrl+O while drawing a new polygon, you can load a file but you're still drawing. Or is you press Ctrl+S while creating a new polygon, the saved file will contain the partially drawn polygon.

Yet another weird bug happens if you're in the middle of editing a polygon when you invoke one of the commands. When you're editing a polygon, the canDraw has captured mouse events. If you load a saved file while you are editing, the capture remains with the old version of the canDraw control. If you click on the new control, you can remove the capture, but it's somewhat confusing.

To avoid all of these strange effects, you can modify the CanExecute methods. Instead of always returning true, you can make those methods call a new CommandsSafe method that can look more closely at what's happening to decide if it's safe to execute the commands. The following code shows the new version of the New command's CanExecute method.

private void NewCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e) { e.CanExecute = CommandsSafe(true); }

The following code shows the CommandsSafe method.

// Return true if the New, SaveAs, and Open // commands should be allowed. private bool CommandsSafe(bool require_polygons) { if (NewPolyline != null) return false; if (EditPolygon != null) return false; if (require_polygons) return canDraw.Children.Count > 0; return true; }

If NewPolygon is not null, then the user is creating a new polygon so the method returns false. Similarly if EditPolygon is not null, then the user is editing a polygon so the method also returns false.

If the require_polygons parameters is true, then the method returns true if the user has drawn at least one polygon and it returns false otherwise. The New and Save commands' CenExecute methods pass the value true into the method, so the program does not enable those commands unless the user has created at least one polygon. (There's no need to create a new drawing if the drawing is empty, and there's no reason to save a blank drawing.)

Finally, if require_polygons is not true, the method returns true.

A Quick Software Engineering Note

You could use a different approach to resolve the issue of the user invoking New, Open, or SaveAs while drawing or editing a polygon. For example, you could make the commands' Executed methods finalize the new polygon or stop editing before performing their main tasks. That would work, but I thought it would be more confusing.

In general programs that have complex states often have this kind pf problem. The program is in one state (such as creating a new polygon) and the user does something that should move it into a new state (such as pressing Ctrl+S). When that happens, the program must either finish the first state or refuse to move into the new state.

Another example that is common in data-intensive applications occurs when the user is editing text. For example, suppose the user is typing some text in some sort of data grid control and then presses Ctrl+S. Should the text that has been typed so far be committed to the database? Or should the edit be discarded?

To make handling this kind of transition easier, you should consider providing methods to commit or cancel changes. For example, the DataGrid and DataGridView controls both have an EndEdit method that finalizes the input and stops editing. In this posts, the example simply doesn't allow the user to perform an action that causes this kind of transition.

Conclusion

This is a faily long post to described a relatively simple change to the previous example. The XamlReader and XamlWriter classes make converting controls to and from XAML code easy. The saving and restoring that code in text file is just as easy.

It's the commanding code that takes some explaining. The following list shows the steps. There are several, but individually they're not too hard.

  • Use command bindings to bind command objects to the methods that they should execute.
  • Use input bindings to bind input gestures such as Ctrl+S to commands.
  • Make menu items.
    • Use the Command property to indicate the command the item should invoke.
    • Use the InputGestureText property to make the menu item display an input gesture such as Ctrl+S.
    • Use the Header property to change the item's caption if necessary, possibly to give it an accelerator.
  • Implement the commands' CanExecute and Executed methods.

As an exercise, try adding New, Open, and Save As buttons to the program's StackPanel. Bind them to the corresponding command objects and watch how the program enables and disables them as needed.

For information about how the program lets the user draw and edit polygons, see the previous example.

Download the example to experiment with it and to see additional details.

© 2009-2023 Rocky Mountain Computer Consulting, Inc. All rights reserved.