Chapter 22. Windows, Pages, and Navigation


In the chapters ahead, I explore the many features and capabilities of XAML, mostly using just small, stand-alone XAML files. These XAML files demonstrate important techniques, of course, but in focusing exclusively on small files, it's easy to lose sight of the big picture. For that reason, I'd like to present in this chapter a complete programwith a menu and dialog boxesthat combines XAML and C# code.

Another reason to present this conventional WPF program is to immediately contrast it with WPF navigation applications. You can structure a WPF application (or part of an application) so that it functions more like the interconnected pages of a Web site. Rather than having a single, fixed application window that is acted on by user input and commands from menus and dialog boxes, navigation applications frequently change the contents of their window (or parts of the window) through hyperlinks. These types of applications are still client applications, but they act very much like Web applications.

This chapter also discusses the three file formats you can use for distributing a WPF application. The first, of course, is the traditional .exe format, and you've already seen numerous WPF applications that result in .exe files. At the other extreme is the stand-alone XAML file that can be developed in XAML Cruncher (or a similar program) and hosted in Microsoft Internet Explorer.

Between these two extremes is the XAML Browser Application, which has a file name extension of .xbap. As the name implies, these XAML Browser Applications are hosted in Internet Explorer just like stand-alone XAML files. Yet they generally consist of both XAML and C# code, and they are compiled. Because they're intended to run in the context of Internet Explorer, security restrictions limit what these applications can do. In short, they can't do anything that could harm the user's computer. Consequently, they can be run on a user's computer without asking for specific permission or causing undue anxiety.

Let's begin with a traditionally structured application that is distributable as an .exe file. The program I'll be discussing is built around an InkCanvas element, which collects and displays stylus input on the Tablet PC. The InkCanvas also responds to mouse input on both Tablet PCs and non-tablet computers, as you can readily determine by running this tiny, stand-alone XAML file.

JustAnInkCanvas.xaml

[View full width]

<!-- === =============================================== JustAnInkCanvas.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <InkCanvas xmlns="http://schemas.microsoft.com /winfx/2006/xaml/presentation" />



My original intention was to display a window that resembled a small yellow legal pad so that the user could draw on multiple pages of the pad. When it became evident that I'd probably need a special file format for saving multiple pages, I decided to restrict the program to just one page. I'd originally chosen the name of YellowPad for the project, and even though the program saves only a single page, I liked the name and decided to keep it.

The YellowPadWindow.xaml file lays out the main application window. Most of this XAML file is devoted to defining the program's menu. Each menu item requires an element of type MenuItem, arranged in a hierarchy and all enclosed in a Menu element, which is docked at the top of a DockPanel. Notice that some of the menu items have their Command properties set to various static properties of the ApplicationCommands class, such as New, Open, and Save. Others have their Click events set.

YellowPadWindow.xaml

[View full width]

<!-- === =============================================== YellowPadWindow.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:src="/books/4/266/1/html/2/clr-namespace:Petzold.YellowPad" x: Title="Yellow Pad" SizeToContent="WidthAndHeight"> <DockPanel> <Menu DockPanel.Dock="Top"> <!-- File menu. --> <MenuItem Header="_File"> <MenuItem Header="_New" Command="New" /> <MenuItem Header="_Open..." Command="Open" /> <MenuItem Header="_Save..." Command="Save" /> <Separator /> <MenuItem Header="E_xit" Command="Close" /> </MenuItem> <!-- Edit menu. --> <MenuItem Header="_Edit" SubmenuOpened="EditOnOpened"> <MenuItem Header="Cu_t" Command="Cut" /> <MenuItem Header="_Copy" Command="Copy" /> <MenuItem Header="_Paste" Command="Paste" /> <MenuItem Header="_Delete" Command="Delete" /> <Separator /> <MenuItem Header="Select _All" Command="SelectAll" /> <MenuItem Header="_Format Selection..." Name="itemFormat" Click="FormatOnClick"/> </MenuItem> <!-- Stylus-Mode menu. --> <MenuItem Header="_Stylus-Mode" SubmenuOpened="StylusModeOnOpened"> <MenuItem Header="_Ink" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.Ink}" /> <MenuItem Header="Erase by _Point" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" /> <MenuItem Header="_Erase by Stroke" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" /> <MenuItem Header="_Select" Click="StylusModeOnClick" Tag="{x:Static InkCanvasEditingMode.Select}" /> </MenuItem> <!-- Eraser-Mode menu (hidden on non-tablet computers). --> <MenuItem Header="E_raser-Mode" SubmenuOpened="EraserModeOnOpened" Name="menuEraserMode"> <MenuItem Header="_Ink" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.Ink}" /> <MenuItem Header="Erase by _Point" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByPoint}" /> <MenuItem Header="_Erase by Stroke" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.EraseByStroke}" /> <MenuItem Header="_Select" Click="EraserModeOnClick" Tag="{x:Static InkCanvasEditingMode.Select}" /> </MenuItem> <!-- Tools menu. --> <MenuItem Header="_Tools"> <MenuItem Header="_Stylus..." Click="StylusToolOnClick" /> <MenuItem Header="_Eraser..." Click="EraserToolOnClick"/> </MenuItem> <!-- Help menu. --> <MenuItem Header="_Help"> <MenuItem Header="_Help..." Command="Help" /> <MenuItem Header="_About YellowPad ..." Click="AboutOnClick"/> </MenuItem> </Menu> <!-- ScrollViewer encloses InkCanvas element. --> <ScrollViewer VerticalScrollBarVisibility="Auto" > <InkCanvas Name="inkcanv" Width="{x:Static src :YellowPadWindow.widthCanvas}" Height="{x:Static src :YellowPadWindow.heightCanvas}" Background="LemonChiffon"> <Line Stroke="Red" X1="0.875in" Y1="0" X2="0.875in" Y2="{x:Static src :YellowPadWindow.heightCanvas}" /> <Line Stroke="Red" X1="0.9375in" Y1="0" X2="0.9375in" Y2="{x:Static src :YellowPadWindow.heightCanvas}" /> </InkCanvas> </ScrollViewer> </DockPanel> <!-- Accumulate all the CommandBinding objects . --> <Window.CommandBindings> <CommandBinding Command="New" Executed="NewOnExecuted" /> <CommandBinding Command="Open" Executed="OpenOnExecuted" /> <CommandBinding Command="Save" Executed="SaveOnExecuted" /> <CommandBinding Command="Close" Executed="CloseOnExecuted" /> <CommandBinding Command="Cut" CanExecute="CutCanExecute" Executed="CutOnExecuted" /> <CommandBinding Command="Copy" CanExecute="CutCanExecute" Executed="CopyOnExecuted" /> <CommandBinding Command="Paste" CanExecute="PasteCanExecute" Executed="PasteOnExecuted" /> <CommandBinding Command="Delete" CanExecute="CutCanExecute" Executed="DeleteOnExecuted" /> <CommandBinding Command="SelectAll" Executed="SelectAllOnExecuted" /> <CommandBinding Command="Help" Executed="HelpOnExecuted" /> </Window.CommandBindings> </Window>



Following the definition of the menu, the XAML file sets the interior of the DockPanel to a ScrollViewer enclosing an InkCanvas. The InkCanvas is given a background of LemonChiffon and two red vertical lines. (The blue horizontal lines come later.) Notice that the file obtains the dimensions of the InkCanvas from static members of the YellowPadWindow class. (These are defined in the C# file coming up next.) The XAML file concludes with all the CommandBinding elements needed to bind the Command properties of many menu items with CanExecute and OnExecuted handlers.

I've divided the C# portion of the YellowPadWindow class into six small files. One is named YellowPadWindow.cs and the others are named after the top-level menu item each one supports, such as YellowPadWindow.File.cs.

The YellowPadWindow.cs file begins by defining the desired dimensions of the InkCanvas as public, static, read-only fields referred to in YellowPadWindow.xaml.

YellowPadWindow.cs

[View full width]

//------------------------------------------------ // YellowPadWindow.cs (c) 2006 by Charles Petzold //------------------------------------------------ using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Make the pad 5 inches by 7 inches. public static readonly double widthCanvas = 5 * 96; public static readonly double heightCanvas = 7 * 96; [STAThread] public static void Main() { Application app = new Application(); app.Run(new YellowPadWindow()); } public YellowPadWindow() { InitializeComponent(); // Draw blue horizontal lines 1/4 inch apart. double y = 96; while (y < heightCanvas) { Line line = new Line(); line.X1 = 0; line.Y1 = y; line.X2 = widthCanvas; line.Y2 = y; line.Stroke = Brushes.LightBlue; inkcanv.Children.Add(line); y += 24; } // Disable the Eraser-Mode menu item if there's no tablet present. if (Tablet.TabletDevices.Count == 0) menuEraserMode.Visibility = Visibility.Collapsed; } } }



The YellowPadWindow constructor calls InitializeComponent (of course) but is also responsible for drawing the blue horizontal lines across the pad at one-quarter inch increments. I considered putting these Line elements in the XAML file, but I didn't like the idea of so much repetitive markup. I also realized that they'd need to be changed if the vertical dimension of the pad were ever changed.

The constructor concludes by disabling one of the top-level menu items if the program isn't running on a Tablet PC. Removing this menu item doesn't make the program any less functional on non-tablet computers.

As the user draws on the surface of the InkCanvas with the stylus or mouse, the InkCanvas stores the input in a property named Strokes of type StrokeCollection, a collection of Stroke objects. (Although InkCanvas is defined in the System.Windows.Controls namespace, Stroke and StrokeCollection are defined in System.Windows.Ink.) In the parlance of the Tablet PC, a stroke occurs when the user touches the stylus to the screen, moves it, and lifts it. When using a mouse, a stroke occurs when the user presses the left mouse button, moves the mouse, and releases the left mouse button. In either case, the InkCanvas tracks the movement of the stylus or mouse and renders a line on the screen.

In computer graphics terminology, a stroke is basically a polyline, a collection of short, connected lines defined by a series of points. Consequently, the Stroke object has a property named StylusPoints of type StylusPointCollection, which is a collection of StylusPoint objects. (StylusPoint and StylusPointCollection are defined in the System.Windows.Input namespace.) The StylusPoint structure contains X and Y properties indicating the coordinates of the point, as well as a PressureFactor property recording the pressure of the stylus on the screen. By default, InkCanvas draws wider lines when the pressure is higher. (This varying line width is absent when you use the mouse, of course.)

Tablet PCs of the future might record additional information about the stylus besides just position and pressure; this information is handled through the Description property of StylusPoint. See the static read-only fields of StylusPointProperties to get an idea of what other information might someday be recorded.

Besides the StylusPoints property, the Stroke class also defines a DrawingAttributes property. Each stroke can potentially have its own color, and this color is part of the DrawingAttributes object. DrawingAttributes also includes Width and Height properties that indicate the height and width of the line rendered by the stylus or mouse. These two values can be different, which can result in fancy, calligraphy-like effects. The shape of the stylus tip can be rectangular or elliptical, and it can even be rotated. The InkCanvas maintains a property named DefaultDrawingAttributes, which is the DrawingAttributes object applied to the current stroke and all future strokes until the property is changed.

StrokeCollection defines a Save method that saves the collection of strokes in the Ink Serialized Format (ISF), and a constructor that loads an ISF file. The Ink Serialized Format is also supported under version 1.7 of the Tablet PC Software Development Kit, so it's compatible with Tablet PC applications written for Windows Forms or the Win32 API.

The YellowPadWindow.File.cs portion of the YellowPadWindow class is responsible for all four items on the File menu. To keep the program reasonably short, I decided not to save the file name of the loaded file or to ask the user if it's all right to abandon a file that hasn't been saved. The Open and Save commands mostly implement file input and output using the Ink Serialized Format.

YellowPadWindow.File.cs

[View full width]

//--------- -------------------------------------------- // YellowPadWindow.File.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using Microsoft.Win32; using System; using System.IO; using System.Windows; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // File New command: just clear all the strokes. void NewOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.Strokes.Clear(); } // File Open command: display OpenFileDialog and load ISF file. void OpenOnExecuted(object sender, ExecutedRoutedEventArgs args) { OpenFileDialog dlg = new OpenFileDialog(); dlg.CheckFileExists = true; dlg.Filter = "Ink Serialized Format (* .isf)|*.isf|" + "All files (*.*)|*.*"; if ((bool)dlg.ShowDialog(this)) { try { FileStream file = new FileStream(dlg.FileName, FileMode.Open, FileAccess.Read); inkcanv.Strokes = new StrokeCollection(file); file.Close(); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } } } // File Save command: display SaveFileDialog. void SaveOnExecuted(object sender, ExecutedRoutedEventArgs args) { SaveFileDialog dlg = new SaveFileDialog(); dlg.Filter = "Ink Serialized Format (* .isf)|*.isf|" + "XAML Drawing File (* .xaml)|*.xaml|" + "All files (*.*)|*.*"; if ((bool)dlg.ShowDialog(this)) { try { FileStream file = new FileStream(dlg.FileName, FileMode.Create, FileAccess.Write); if (dlg.FilterIndex == 1 || dlg.FilterIndex == 3) inkcanv.Strokes.Save(file); else { // Save strokes as DrawingGroup object. DrawingGroup drawgrp = new DrawingGroup(); foreach (Stroke strk in inkcanv.Strokes) { Color clr = strk .DrawingAttributes.Color; if (strk .DrawingAttributes.IsHighlighter) clr = Color .FromArgb(128, clr.R, clr.G, clr.B); drawgrp.Children.Add( new GeometryDrawing( new SolidColorBrush(clr), null, strk .GetGeometry())); } XamlWriter.Save(drawgrp, file); } file.Close(); } catch (Exception exc) { MessageBox.Show(exc.Message, Title); } } } // File Exit item: just close window. void CloseOnExecuted(object sender, ExecutedRoutedEventArgs args) { Close(); } } }



As you'll note, the SaveFileDialog also includes an option to save the strokes as a "XAML Drawing File." This feature requires a bit more explanation.

One of the aspects of the Tablet PC that has always interested me is the potential for using stylus input in graphics programming. The Stroke class helps here because it defines a method named GetGeometry that returns an object of type Geometry. You'll learn about the Geometry object in Chapter 28; for now you should know that the most generalized kind of Geometry is quite similar to a traditional graphics patha collection of connected and disconnected straight lines and curves. It pleased me a great deal to discover that the Geometry object returned from the GetGeometry method of Stroke is not simply the polyline that defines the stroke. It's actually the outline of the rendered stroke, taking into account the shape and dimensions of the stylus tip and the pressure of the stylus against the screen.

A Geometry is pure analytic geometrypoints and lines and curves. A Geometry combined with an outline brush and a fill brush is an object of type GeometryDrawing, so the extra code in the SaveOnExecuted method uses the DrawingAttributes object for each Stroke to fill the geometry with that color. Each GeometryDrawing object corresponds to a Stroke object, and these are assembled in a DrawingGroup object, which is what the program saves to a file using the XamlWriter.Save method. (Both GeometryDrawing and DrawingGroup derive from the abstract Drawing class. I'll discuss these concepts in much more detail in Chapter 31.)

Now that you have a DrawingGroup object in a file, what can you do with it? From any object of type Drawing (named, perhaps, drawing) you can make a DrawingImage object:

DrawingImage drawimg = new DrawingImage(drawing); 


Now it gets interesting, because DrawingImage derives from ImageSource, and ImageSource is the type of the Source property defined by Image, a class you first encountered in the ShowMyFace program in Chapter 3 of this book. While Image is normally employed to display bitmapped images, it can display vector images as well if these images are stored in objects of type DrawingImage.

So, whatever you draw in the YellowPad program, you can save in a format that is easily displayed as a WPF graphics object. I used this feature to save a copyright notice with my signature that appears in YellowPad's About box.

The next item on the top-level menu is Edit, but I want to skip that one for now and come back to it later. Two items appear after the Edit itemStylus-Mode and Eraser-Mode. The stylus mode describes what happens when you draw on the screen with the stylus tip or the mouse, and it corresponds to the EditingMode property of InkCanvas. What I call the "eraser mode" is what happens when you turn the stylus upside down and use the eraser end on the screen. This option corresponds to the EditingModeInverted property of InkCanvas. Because this option makes no sense on a non-tablet computer, the Eraser-Mode item is present only when you run the program on a Tablet PC.

Both EditingMode and EditingModeInverted are set to members of the InkCanvasEditingMode enumeration. By default, EditingMode is set to InkCanvasEditingMode.Ink and EditingModeInverted is set to InkCanvasEditingMode.EraseByStroke, which means that an entire stroke is deleted when you erase part of it. For the sake of completenessand to allow a mouse user to erase strokesI put the same options on the Stylus-Mode and Eraser-Mode menus. Besides Ink and EraseByStroke, these menus let the user choose EraseByPoint (which cuts a stroke into two strokes rather than deleting an entire stroke) and Selection. The Selection option lets you select one or more strokes using a lasso-like object. I ignored the InkCanvasEditingMode members GestureOnly, InkAndGesture, and None.

The YellowPadWindow.Mode.cs file handles both the Stylus-Mode and Eraser-Mode menu items in very similar ways. When the submenu is opened, the code applies a checkmark to the item corresponding to the current property of the InkCanvas object. The Click handlers apply the selected item to the InkCanvas.

YellowPadWindow.Mode.cs

[View full width]

//--------- -------------------------------------------- // YellowPadWindow.Mode.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Stylus-Mode submenu opened: check one of the items. void StylusModeOnOpened(object sender, RoutedEventArgs args) { MenuItem item = sender as MenuItem; foreach (MenuItem child in item.Items) child.IsChecked = inkcanv .EditingMode == (InkCanvasEditingMode)child.Tag; } // Set the EditingMode property from the selected item. void StylusModeOnClick(object sender, RoutedEventArgs args) { MenuItem item = sender as MenuItem; inkcanv.EditingMode = (InkCanvasEditingMode)item.Tag; } // Eraser-Mode submenu opened: check one of the items. void EraserModeOnOpened(object sender, RoutedEventArgs args) { MenuItem item = sender as MenuItem; foreach (MenuItem child in item.Items) child.IsChecked = inkcanv .EditingModeInverted == (InkCanvasEditingMode)child.Tag; } // Set the EditingModeInverted property from the selected item. void EraserModeOnClick(object sender, RoutedEventArgs args) { MenuItem item = sender as MenuItem; inkcanv.EditingModeInverted = (InkCanvasEditingMode)item.Tag; } } }



Checking the item based on the actual property of the InkCanvas is the safest approach here because the EditingMode can change without invoking this menu. For example, if the Select method of the InkCanvas is called, the EditingMode becomes Select.

I mentioned earlier that InkCanvas includes a property named DefaultDrawingAttributes of type DrawingAttributes that contains the color, stylus dimensions, and shape that apply to all future strokes. The DrawingAttributes class also includes two Boolean properties named IgnorePressure and IsHighlighter. The first causes the InkCanvas to ignore stylus pressure when rendering strokes. The IsHighlighter property causes the selected color to be rendered with an alpha channel of 128, causing the color to be half transparent.

The eraser is less versatile. You can only change its shape and dimensions. The EraserShape property of InkCanvas is of type StylusShape, an abstract class that has Height, Width, and Rotation properties. From StylusShape descend the EllipseStylusShape and RectangleStylusShape classes.

Despite the differences in the way that the stylus and eraser are handled, I decided to implement interfaces to them both with the same basic dialog box. The program refers to the stylus and eraser collectively as "tools." The dialog box defined in the following XAML file has a title of "Stylus Tool" and it contains controls that correspond to most of the properties of DrawingAttributes.

StylusToolDialog.xaml

[View full width]

<!-- === ================================================ StylusToolDialog.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" xmlns:color="clr-namespace:Petzold .ListColorsElegantly" x: Title="Stylus Tool" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight" ResizeMode="NoResize"> <Grid Margin="6"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <!-- Three-by-three Grid displays three TextBox controls. --> <Grid Grid.Row="0" Grid.Column="0"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Label Content="_Width:" Grid.Row="0" Grid.Column="0" Margin="6 6 0 6" /> <TextBox Name="txtboxWidth" Grid .Row="0" Grid.Column="1" Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="points" Grid.Row="0" Grid.Column="2" Margin="0 6 6 6" /> <Label Content="_Height:" Grid.Row="1" Grid.Column="0" Margin="6 6 0 6" /> <TextBox Name="txtboxHeight" Grid .Row="1" Grid.Column="1" Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="points" Grid.Row="1" Grid.Column="2" Margin="0 6 6 6" /> <Label Content="_Rotation:" Grid .Row="2" Grid.Column="0" Margin="6 6 0 6" /> <TextBox Name="txtboxAngle" Grid .Row="2" Grid.Column="1" Width="50" TextAlignment="Right" Margin="0 6 0 6" /> <Label Content="degrees" Grid.Row="2" Grid.Column="2" Margin="0 6 6 6" /> </Grid> <!-- GroupBox has two RadioButton controls for stylus tip. --> <GroupBox Header="_Stylus Tip" Grid .Row="1" Grid.Column="0" Margin="6"> <StackPanel> <RadioButton Name="radioEllipse" Content="Ellipse" Margin="6" /> <RadioButton Name="radioRect" Content="Rectangle" Margin="6" /> </StackPanel> </GroupBox> <!-- Two CheckBox controls for pressure and highlighter flags. --> <CheckBox Name="chkboxPressure" Content="_Ignore pressure" Grid.Row="2" Grid.Column="0" Margin="12 6 6 6" /> <CheckBox Name="chkboxHighlighter" Content="_Highlighter" Grid.Row="3" Grid.Column="0" Margin="12 6 6 6" /> <!-- ColorListBox from ListColorsElegantly project. --> <color:ColorListBox x:Name="lstboxColor" Width="150" Height="200" Grid.Row="0" Grid .Column="1" Grid.RowSpan="3" Margin="6"/> <!-- OK and Cancel buttons. --> <UniformGrid Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Columns="2"> <Button Content="OK" Name="btnOk" Click="OkOnClick" IsDefault="True" MinWidth="60" Margin="6" HorizontalAlignment="Center" /> <Button Content="Cancel" IsCancel="True" MinWidth="60" Margin="6" HorizontalAlignment="Center" /> </UniformGrid> </Grid> </Window>



Notice that the ColorListBox control comes from the ListColorsElegantly program in Chapter 13, and the XAML file requires an XML namespace declaration for the namespace of that project.

The StylusToolDialog.cs file defines the remainder of the StylusToolDialog class. The DrawingAttributes property initializes the controls in its set accessor and creates a new DrawingAttributes object from the settings of its controls in its get accessor. The dialog displays the width and height of the tip in points (1/72 inch), so both the set and get accessor have conversion calculations.

StylusToolDialog.cs

[View full width]

//------------------------------------------------- // StylusToolDialog.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Media; namespace Petzold.YellowPad { public partial class StylusToolDialog : Window { // Constructor. public StylusToolDialog() { InitializeComponent(); // Set event handlers to enable OK button. txtboxWidth.TextChanged += TextBoxOnTextChanged; txtboxHeight.TextChanged += TextBoxOnTextChanged; txtboxAngle.TextChanged += TextBoxOnTextChanged; txtboxWidth.Focus(); } // Public property initializes controls and returns their values. public DrawingAttributes DrawingAttributes { set { txtboxHeight.Text = (0.75 * value .Height).ToString("F1"); txtboxWidth.Text = (0.75 * value .Width).ToString("F1"); txtboxAngle.Text = (180 * Math.Acos(value .StylusTipTransform.M11) / Math.PI).ToString("F1"); chkboxPressure.IsChecked = value .IgnorePressure; chkboxHighlighter.IsChecked = value.IsHighlighter; if (value.StylusTip == StylusTip .Ellipse) radioEllipse.IsChecked = true; else radioRect.IsChecked = true; lstboxColor.SelectedColor = value .Color; lstboxColor.ScrollIntoView (lstboxColor.SelectedColor); } get { DrawingAttributes drawattr = new DrawingAttributes(); drawattr.Height = Double.Parse (txtboxHeight.Text) / 0.75; drawattr.Width = Double.Parse (txtboxWidth.Text) / 0.75; drawattr.StylusTipTransform = new RotateTransform(Double .Parse(txtboxAngle.Text)).Value; drawattr.IgnorePressure = (bool)chkboxPressure.IsChecked; drawattr.IsHighlighter = (bool)chkboxHighlighter.IsChecked; drawattr.StylusTip = (bool)radioEllipse.IsChecked ? StylusTip.Ellipse : StylusTip.Rectangle; drawattr.Color = lstboxColor .SelectedColor; return drawattr; } } // Event handler enables OK button only if all fields are valid. void TextBoxOnTextChanged(object sender, TextChangedEventArgs args) { double width, height, angle; btnOk.IsEnabled = Double.TryParse (txtboxWidth.Text, out width) && width / 0.75 >= DrawingAttributes.MinWidth && width / 0.75 <= DrawingAttributes.MaxWidth && Double.TryParse (txtboxHeight.Text, out height) && height / 0.75 >= DrawingAttributes.MinHeight && height / 0.75 <= DrawingAttributes.MaxHeight && Double.TryParse (txtboxAngle.Text, out angle); } // OK button terminates dialog. void OkOnClick(object sender, RoutedEventArgs args) { DialogResult = true; } } }



The get accessor is able to call Double.Parse with impunity because the OK button of the dialog box isn't enabled unless all three TextBox controls contain valid double values. This logic occurs in the TextChanged event handler shared by all three TextBox controls.

The EraserToolDialog class inherits from StylusToolDialog. Its constructor applies a new Title property to the dialog and hides three controls with properties not supported for the eraser.

EraserToolDialog.cs

[View full width]

//------------------------------------------------- // EraserToolDialog.cs (c) 2006 by Charles Petzold //------------------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; namespace Petzold.YellowPad { public class EraserToolDialog : StylusToolDialog { // Constructor hides some irrelevant controls on StylusToolDialog. public EraserToolDialog() { Title = "Eraser Tool"; chkboxPressure.Visibility = Visibility .Collapsed; chkboxHighlighter.Visibility = Visibility.Collapsed; lstboxColor.Visibility = Visibility .Collapsed; } // Public property initializes controls and returns their values. public StylusShape EraserShape { set { txtboxHeight.Text = (0.75 * value .Height).ToString("F1"); txtboxWidth.Text = (0.75 * value .Width).ToString("F1"); txtboxAngle.Text = value.Rotation .ToString(); if (value is EllipseStylusShape) radioEllipse.IsChecked = true; else radioRect.IsChecked = true; } get { StylusShape eraser; double width = Double.Parse (txtboxWidth.Text) / 0.75; double height = Double.Parse (txtboxHeight.Text) / 0.75; double angle = Double.Parse (txtboxAngle.Text); if ((bool)radioEllipse.IsChecked) eraser = new EllipseStylusShape(width, height, angle); else eraser = new RectangleStylusShape(width, height, angle); return eraser; } } } }



This class defines a new property named EraserShape of type StylusShape (the same as the EraserShape property defined by InkCanvas), and the code roughly parallels that of the DrawingAttributes property in StylusToolDialog, except that the two different shapes are represented by two different classes.

The Tools menu contains the two items Stylus and Eraser. The YellowPadWindow.Tools.cs file is responsible for displaying the StylusToolDialog and EraserToolDialog windows in response to these commands.

YellowPadWindow.Tools.cs

[View full width]

//--------- --------------------------------------------- // YellowPadWindow.Tools.cs (c) 2006 by Charles Petzold //------------------------------------------------ ------ using System; using System.Windows; using System.Windows.Controls; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Display StylusToolDialog and use DrawingAttributes property. void StylusToolOnClick(object sender, RoutedEventArgs args) { StylusToolDialog dlg = new StylusToolDialog(); dlg.Owner = this; dlg.DrawingAttributes = inkcanv .DefaultDrawingAttributes; if ((bool)dlg.ShowDialog() .GetValueOrDefault()) { inkcanv.DefaultDrawingAttributes = dlg.DrawingAttributes; } } // Display EraserToolDialog and use EraserShape property. void EraserToolOnClick(object sender, RoutedEventArgs args) { EraserToolDialog dlg = new EraserToolDialog(); dlg.Owner = this; dlg.EraserShape = inkcanv.EraserShape; if ((bool)dlg.ShowDialog() .GetValueOrDefault()) { inkcanv.EraserShape = dlg.EraserShape; } } } }



I want to go back to the Edit menu now. I mentioned earlier that you can choose Select from the Stylus-Mode menu and lasso one or more strokes. InkCanvas has CopySelection and CutSelection methods to copy the selected strokes to the clipboard. The CutSelection method also deletes the selected strokes from the strokes collection. InkCanvas also defines a CanPaste method that indicates if some ink is in the clipboard, and a Paste method that pastes that ink to the InkCanvas. The standard items on the Edit menu are thus fairly easy to implement, as the following file demonstrates.

YellowPadWindow.Edit.cs

[View full width]

//--------- -------------------------------------------- // YellowPadWindow.Edit.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Ink; using System.Windows.Input; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Enable Format item if strokes have been selected. void EditOnOpened(object sender, RoutedEventArgs args) { itemFormat.IsEnabled = inkcanv .GetSelectedStrokes().Count > 0; } // Enable Cut, Copy, Delete items if strokes have been selected. void CutCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = inkcanv .GetSelectedStrokes().Count > 0; } // Implement Cut and Copy with methods in InkCanvas. void CutOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.CutSelection(); } void CopyOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.CopySelection(); } // Enable Paste item if the InkCanvas is cool with the clipboard. void PasteCanExecute(object sender, CanExecuteRoutedEventArgs args) { args.CanExecute = inkcanv.CanPaste(); } // Implement Paste with method in InkCanvas. void PasteOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.Paste(); } // Implement Delete "manually." void DeleteOnExecuted(object sender, ExecutedRoutedEventArgs args) { foreach (Stroke strk in inkcanv .GetSelectedStrokes()) inkcanv.Strokes.Remove(strk); } // Select All item: select all the strokes. void SelectAllOnExecuted(object sender, ExecutedRoutedEventArgs args) { inkcanv.Select(inkcanv.Strokes); } // Format Selection item: invoke StylusToolDialog. void FormatOnClick(object sender, RoutedEventArgs args) { StylusToolDialog dlg = new StylusToolDialog(); dlg.Owner = this; dlg.Title = "Format Selection"; // Try getting the DrawingAttributes of the first selected stroke. StrokeCollection strokes = inkcanv .GetSelectedStrokes(); if (strokes.Count > 0) dlg.DrawingAttributes = strokes[0] .DrawingAttributes; else dlg.DrawingAttributes = inkcanv .DefaultDrawingAttributes; if ((bool)dlg.ShowDialog() .GetValueOrDefault()) { // Set the DrawingAttributes of all the selected strokes. foreach (Stroke strk in strokes) strk.DrawingAttributes = dlg .DrawingAttributes; } } } }



For enabling menu items that require strokes to be already selected, I use the GetSelectedStrokes method of InkCanvas, which returns an object of type StrokeCollection.

I was initially reluctant to implement an item to change the formatting of the selected strokes until I realized it could be handled largely by the StylusToolDialog with yet another Title property. The code at the bottom of the file initializes the dialog box from the DrawingAttributes property of the first selected stroke, and then sets all the selected strokes from the new DrawingAttributes object created by the dialog box.

The YellowPadWindow.Help.cs file is responsible for the two items on the Help menu, both of which cause dialog boxes to be displayed. The Help item displays a modeless dialog box of type YellowPadHelp, while the About item displays a modal dialog of type YellowPadAboutDialog.

YellowPadWindow.Help.cs

[View full width]

//--------- -------------------------------------------- // YellowPadWindow.Help.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; namespace Petzold.YellowPad { public partial class YellowPadWindow : Window { // Help command: display YellowPadHelp as modeless dialog. void HelpOnExecuted(object sender, ExecutedRoutedEventArgs args) { YellowPadHelp win = new YellowPadHelp(); win.Owner = this; win.Show(); } // About command: display YellowPadAboutDialog. void AboutOnClick(object sender, RoutedEventArgs args) { YellowPadAboutDialog dlg = new YellowPadAboutDialog(); dlg.Owner = this; dlg.ShowDialog(); } } }



I'm going to hold off on showing you the YellowPadHelp class until later in this chapter when some necessary background has been illuminated. The following YellowPadAboutDialog.xaml file defines the layout of the About dialog.

YellowPadAboutDialog.xaml

[View full width]

<!-- === ==================================================== YellowPadAboutDialog.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.YellowPadAboutDialog" Title="About YellowPad" ShowInTaskbar="False" WindowStartupLocation="CenterOwner" SizeToContent="WidthAndHeight" ResizeMode="NoResize"> <StackPanel> <!-- Program name. --> <TextBlock HorizontalAlignment="Center" Margin="12" FontSize="48"> <Italic>Yellow Pad</Italic> </TextBlock> <!-- Cover of the book the program is from . --> <Image Source="Images\BookCover.jpg" Stretch="None" Margin="12" /> <!-- Another Image element for the copyright/signature file. --> <Image Name="imgSignature" Stretch="None" Margin="12" /> <!-- Web Site link. --> <TextBlock HorizontalAlignment="Center" FontSize="20"> <Hyperlink NavigateUri="http://www .charlespetzold.com" RequestNavigate="LinkOnRequestNavigate"> www.charlespetzold.com </Hyperlink> </TextBlock> <!-- OK button is both default and cancel button. --> <Button HorizontalAlignment="Center" MinWidth="60" Margin="12" IsDefault="True" IsCancel="True"> OK </Button> </StackPanel> </Window>



The file has two Image elements. The first loads a resource file from the Images directory of the project that is a bitmap image of the cover of this book. The second Image element has no Source property but does have a Name property of "imgSignature." The Source property for this element is set in C# code in the following file, YellowPadAboutDialog.cs. I created the Signature.xaml file from the YellowPad program by saving ink as a XAML Drawing File, and I made that file part of the YellowPad project with a build type of Resource. The code in the YellowPadAboutDialog constructor obtains a Stream for this resource and converts it into an object of type Drawing with XamlReader.Load. The Source of the second Image element in the XAML file is simply a DrawingImage object based on the Drawing object.

YellowPadAboutDialog.cs

[View full width]

//--------- -------------------------------------------- // YellowPadAboutDialog.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Diagnostics; // for Process class. using System.IO; using System.Windows; using System.Windows.Media; using System.Windows.Markup; using System.Windows.Navigation; // for RequestNavigateEventArgs. namespace Petzold.YellowPad { public partial class YellowPadAboutDialog { public YellowPadAboutDialog() { InitializeComponent(); // Load copyright/signature Drawing and set in Image element. Uri uri = new Uri("pack://application: ,,,/Images/Signature.xaml"); Stream stream = Application .GetResourceStream(uri).Stream; Drawing drawing = (Drawing)XamlReader .Load(stream); stream.Close(); imgSignature.Source = new DrawingImage (drawing); } // When hyperlink is clicked, go to my Web site. void LinkOnRequestNavigate(object sender, RequestNavigateEventArgs args) { Process.Start(args.Uri.OriginalString); args.Handled = true; } } }



Let me also call your attention to the Hyperlink element in the YellowPadAboutDialog.xaml file. Clicking the link causes your default Web browser to display my Web site. You've seen this feature before. The AboutDialog class in the NotepadClone program from Chapter 18 defined a similar Hyperlink element but set its Click event to the following handler:

void LinkOnClick(object sender, RoutedEventArgs args) {     Process.Start("http://www.charlespetzold.com"); } 


The YellowPadAboutDialog.xaml file instead assigns the NavigateUri property and the RequestNavigate event handler:

<Hyperlink NavigateUri="http://www.charlespetzold.com"            RequestNavigate="LinkOnRequestNavigate">     www.charlespetzold.com </Hyperlink> 


The LinkOnRequestNavigate handler in YellowPadAboutDialog.cs looks like this:

void LinkOnRequestNavigate(object sender, RequestNavigateEventArgs args) {     Process.Start(args.Uri.OriginalString);     args.Handled = true; } 


The event handler is able to snag the URI assigned to the NavigateUri property for passing to Process.Start. That makes the code in the event handler a little more generalized, but otherwise it seems unnecessarily more verbose than the approach using our old friend Click.

Besides, it seems reasonable that if you give the Hyperlink element in the XAML the actual URI you want it to go to, Hyperlink should be able to go to that link by itself without any additional code. Yet if you remove the RequestNavigate event attribute from the Hyperlink element and recompile, nothing happens when you click the link. But what would you like to happen? Should Hyperlink launch your default Web browser as Process.Start does? Or should the desired Web page actually replace the entire content of the About box window?

If the latter approach appeals to you, you're in luck, for Hyperlink can do precisely that. All it needs is a proper home. Or rather, a window (or even just a frame).

Try this: In YellowPadAboutDialog.xaml, change both occurrences of Window to NavigationWindow, and enclose the StackPanel in a NavigationWindow.Content property element. NavigationWindow is the only class defined in the Windows Presentation Foundation that derives from Window. Now remove the RequestNavigate event attribute from the Hyperlink element (if you haven't already done so) and recompile.

Now when you invoke the About box, it appears in a slightly different kind of window. Two disabled buttons appear near the top, labeled with left and right arrows. These are Back and Forward buttons, and the horizontal strip they appear on is known as navigation chrome. When you click the link to my Web site, the Web site replaces the contents of the About box. Now the Back button has become enabled. Click it to go back to the About box.

Welcome to the world of WPF navigation applications. As you've discovered, the Hyperlink element normally doesn't work unless you install an event handler for it. However, if the Hyperlink is inside a NavigationWindow (or a Frame element), clicking the link automatically navigates to the page specified in the NavigateUri property. The NavigateUri property can reference a URI of a Web site, but it's more commonly the name of another XAML file in the program.

Taken to the extreme, the use of NavigationWindow and Frame lets you structure your entire WPF application much like a Web site, but without giving up any of the power of WPF elements, controls, and graphics.

The NavigationDemo project demonstrates some basic navigation techniques. The project has five XAML files and one C# file. The first XAML file is this application definition file.

NavigationDemoApp.xaml

[View full width]

<!-- === ================================================= NavigationDemoApp.xaml (c) 2006 by Charles Petzold ============================================= ======= --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: StartupUri="NavigationDemoWindow.xaml" />



The StartupUri attribute indicates that the NavigationDemoWindow.xaml is to be loaded as the initial application window. The root element of NavigationDemoWindow.xaml is a NavigationWindow. The NavigationWindow element has its Source attribute set to yet another XAML file.

NavigationDemoWindow.xaml

[View full width]

<!-- === ==================================================== NavigationDemoWindow.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <NavigationWindow xmlns="http://schemas.microsoft. com/winfx/2006/xaml/presentation" Title="Navigation Demo" FontSize="24" Source="Page1.xaml" />



Source is one of several properties that NavigationWindow defines beyond the properties it inherits from Window. You'll recall that Frame also has a Source property. Both NavigationWindow and Frame derive from ContentControl, but if you set the Content of either element, that Content property will take precedence over the Source property. Generally you'll be using NavigationWindow or Frame to take advantage of their navigational abilities, so you'll want to focus on the Source property rather than on Content.

The Source property of the preceding NavigationWindow references the Page1.xaml file shown here.

Page1.xaml

[View full width]

<!-- ======================================== Page1.xaml (c) 2006 by Charles Petzold ======================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" Title="Page 1" WindowTitle="Navigation Demo: Page 1"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"> Go to <Hyperlink NavigateUri="Page2.xaml">Page 2< /Hyperlink>. </TextBlock> </Page>



It is not necessary for the Source property of NavigationWindow or Frame to be set to a XAML file with a root element of Page. (In code, it is not necessary to set the Source property to an object of type Page.) However, Page has several features that make it quite suitable for navigation applications. Two of these features are shown in Page1.xaml. The Title property is the text that appears in the lists of visited pages displayed by the Back and Forward buttons; these lists facilitate jumping to a previously navigated page. The WindowTitle property overrides the Title property of the NavigationWindow. (Here's another way you can tell that Page is specifically designed for navigation applications: A Page element can be a child only of a NavigationWindow or a Frame.)

Otherwise, Page1.xaml simply contains a TextBlock with an embedded Hyperlink element that has a NavigateUri of Page2.xaml. That's the file shown here.

Page2.xaml

[View full width]

<!-- ======================================== Page2.xaml (c) 2006 by Charles Petzold ======================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x: Title="Page 2" WindowTitle="Navigation Demo: Page 2"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="33*" /> <RowDefinition Height="33*" /> <RowDefinition Height="33*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center"> RequestNavigate event handled for navigation to <Hyperlink NavigateUri="Page3.xaml" RequestNavigate="HyperlinkOnRequestNavigate"> Page 3</Hyperlink>. </TextBlock> <Button Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Click="ButtonOnClick"> Click to go to Page 1 </Button> <TextBlock Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center"> Go to <Hyperlink NavigateUri="http://www .charlespetzold.com"> Petzold's Web site</Hyperlink>. </TextBlock> </Grid> </Page>



The Page element in the Page2.xaml file is a bit more complex than the first page. It has two TextBlock elements and one Button, all labeled as if they contained active links.

The first TextBlock has an embedded Hyperlink element that specifies a handler for its RequestNavigate event. The Button indicates a handler for its Click event. The third Hyperlink contains only a NavigateUri property, but it points to my Web site rather than to a local XAML page.

Page2.xaml requires a code-behind file for the two event handlers. That's why it includes an x:Class attribute, which Page1.xaml doesn't need. The code-behind file shown here includes a call to InitializeComponent in its constructor to wire up the event handlers, and the two event handlers themselves.

Page2.cs

[View full width]

//-------------------------------------- // Page2.cs (c) 2006 by Charles Petzold //-------------------------------------- using System; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Navigation; namespace Petzold.NavigationDemo { public partial class Page2 { public Page2() { InitializeComponent(); } void ButtonOnClick(object sender, RoutedEventArgs args) { NavigationService.Navigate( new Uri("Page1.xaml", UriKind.Relative)); } void HyperlinkOnRequestNavigate(object sender, RequestNavigateEventArgs args) { NavigationService.Navigate(args.Uri); args.Handled = true; } } }



The ButtonOnClick method needs to tell the NavigationWindow to navigate to the Page1.xaml file, and it can do this by calling the Navigate method defined by NavigationWindow. However, the Page2 class doesn't have direct access to the NavigationWindow object with that crucial Navigate method. One approach to obtaining the NavigationWindow is through the static Application.Current property and then the MainWindow property of Application:

NavigationWindow navwin =     (Application.Current.MainWindow as NavigationWindow); 


You can then call the Navigate method of the navwin object to navigate to the file. However, the Page2.cs file demonstrates a somewhat easier approach. The Page class defines a property named NavigationService that itself has a Navigate method. The NavigationService object is accessible through the Page object and provides a conduit to the navigational abilities of the NavigationWindow of which the Page is a part. (The existence of this NavigationService property is another reason why Page is well suited for navigation applications. However, if you need to perform navigation in a class other than Page, you can obtain a NavigationService object from the static NavigationService.GetNavigationService method. The class calling Navigate still needs to be somewhere inside a NavigationWindow or Frame, however.)

The Navigate method requires a Uri or an object. In the ButtonOnClick method, the second argument of the Uri constructor indicates that the path given for the Page1.xaml file is relative to the path of the current XAML file, which is Page2.xaml.

The RequestNavigate event handler handles the navigation job similarly except that the Uri that the handler passes to the Navigate method is the one originally specified in the Hyperlink element of the Page2.xaml file. The event handler sets the Handled property to true to indicate that it has performed the navigation and nothing more needs to be done.

In this example, nothing is gained by taking over the call to Navigate in the event handler rather than having the Hyperlink perform it automatically. But you might have a need to examine a particular link as it's occurring.

The final Page3.xaml file that completes the NavigationDemo project is nearly as simple as Page1.xaml. It contains a TextBox at the top and a link at the bottom to navigate back to Page1.xaml.

Page3.xaml

[View full width]

<!-- ======================================== Page3.xaml (c) 2006 by Charles Petzold ======================================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" Title="Page 3" WindowTitle="Navigation Demo: Page 3"> <NavigationWindow.Content> <Grid> <Grid.RowDefinitions> <RowDefinition Height="50*" /> <RowDefinition Height="50*" /> </Grid.RowDefinitions> <TextBox Grid.Row="0" MinWidth="2in" Margin="48" HorizontalAlignment="Center" VerticalAlignment="Top" /> <TextBlock Grid.Row="2" Margin="48" HorizontalAlignment="Right" VerticalAlignment="Bottom"> Go back to <Hyperlink NavigateUri="Page1.xaml"> Page 1</Hyperlink>. </TextBlock> </Grid> </NavigationWindow.Content> </Page>



As you navigate through this application, keep note of the Back and Forward buttons displayed at the top of the NavigationWindow. The NavigationWindow maintains a journal of the visited pages in two stacks corresponding to the two buttons. Whenever a program uses the Navigate method to navigate to a page, the page you navigated from is pushed onto the back stack and the forward stack is cleared. When you click the Back button, the current page is pushed onto the forward stack, and you navigate to the page popped from the back stack. When you click the Forward button, the current page is pushed onto the back stack, and you navigate to the page popped from the forward stack. You also have programmatic access to back and forward navigation, as you'll see later in this chapter.

Page3.xaml includes a TextBox at the top. While navigating, type something into it. If you navigate back to Page3.xaml again through hyperlinks, you'll find the TextBox empty. However, if you navigate to Page3.xaml through the Back or Forward buttons, you'll find the text you typed still there.

When you use NavigationWindow, the entire contents of the window change with each navigation. It could be that you only want to devote part of the window to navigation, or to perform independent navigation in several parts of the window. In those cases, you can use Frame.

The FrameNavigationDemo project has two files named FrameNavigationDemoApp.xaml and FrameNavigationWindow.xaml and also includes links to the four Page files from NavigationDemo project. Because the Page2 class has a namespace of Petzold.NavigationDemo, the FrameApplicationDemoApp.xaml application definition file defines that same namespace to make integration with the earlier code a bit easier.

FrameNavigationDemoApp.xaml

[View full width]

<!-- === ====================================================== FrameNavigationDemoApp.xaml (c) 2006 by Charles Petzold ============================================= ============ --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: StartupUri="FrameNavigationDemoWindow .xaml" />



The StartupUri attributes points to the next file, which is FrameApplicationDemoWindow .xaml.

FrameNavigationDemoWindow.xaml

[View full width]

<!-- === =========== ============================================== FrameNavigationDemoWindow.xaml (c) 2006 by Charles Petzold ============================================= =============== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" Title="Frame Navigation Demo" FontSize="24"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Frame Grid.Row="0" Background="Aqua" Source="Page1.xaml" /> <Frame Grid.Row="1" Background="Pink" Source="Page2.xaml" /> </Grid> </Window>



The root element of this file is Window, but the window contains two Frame elements. They are given different colors so that you can tell them apart, and each points to a different Page file. When you compile and run this program, you'll note that each Frame has its own navigation chrome, but that chrome doesn't appear until the first time you navigate from the initial page.

As you can see, the navigation among the pages is entirely independent in the two Frame elements. But here's an interesting experiment: In FrameNavigationDemoWindow.xaml, change Window to NavigationWindow and recompile. Now there's only one piece of navigation chrome at the top of the window. However, both frames still navigate independently, and the Back and Forward buttons know which frame is which!

It is now time to return to the YellowPad project and create a Help system for it. The YellowPad project contains a directory named Help that contains ten XAML files and three PNG files with images of the program's dialog boxes. The XAML files are all fairly similar. They all have a root element of Page containing a FlowDocument inside a FlowDocumentReader with some help text and perhaps a link to one of the images or the other help files. Here's a typical file from the Help directory.

EraserToolDialog.xaml

[View full width]

<!-- === ================================================ EraserToolDialog.xaml (c) 2006 by Charles Petzold ============================================= ====== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" Title="The Eraser Tool Dialog"> <FlowDocumentReader ViewingMode="Scroll"> <FlowDocument> <Paragraph TextAlignment="Center" FontSize="16pt"> The Eraser Tool Dialog </Paragraph> <Paragraph> The <Bold>Eraser Tool</Bold> dialog lets you change the dimensions and shape of the eraser. </Paragraph> <BlockUIContainer> <Image Source="EraserToolDialog.png" Stretch="None"/> </BlockUIContainer> <Paragraph> Use the <Bold>Width</Bold> and <Bold>Height</Bold> fields to specify the dimensions of the eraser in points (1/72<Run BaselineAlignment="Superscript">nd</Run> inch). </Paragraph> <Paragraph> Use the <Bold>Rotation</Bold> field to specify a rotation of the eraser. The rotation only makes sense when the horizontal and vertical dimensions of the eraser are unequal. </Paragraph> </FlowDocument> </FlowDocumentReader> </Page>



The YellowPadHelp.xaml file contains the layout for the Help window. The root element is a NavigationWindow containing a three-column Grid.

YellowPadHelp.xaml

[View full width]

<!-- ================================================ YellowPadHelp.xaml (c) 2006 by Charles Petzold ============================================= === --> <NavigationWindow xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x: Title="YellowPad Help" Width="800" Height="600" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"> <NavigationWindow.Content> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="25*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="75*" /> </Grid.ColumnDefinitions> <TreeView Name="tree" FontSize="10pt" SelectedItemChanged="HelpOnSelectedItemChanged"> <TreeViewItem Header="Program Overview" Tag="Help /Overview.xaml" /> <TreeViewItem Header="Exploring the Menus"> <TreeViewItem Header="The File Menu" Tag="Help /FileMenu.xaml" /> <TreeViewItem Header="The Edit Menu" Tag="Help /EditMenu.xaml" /> <TreeViewItem Header="The Stylus-Mode Menu" Tag="Help /StylusModeMenu.xaml" /> <TreeViewItem Header="The Eraser-Mode Menu" Tag="Help /EraserModeMenu.xaml" /> <TreeViewItem Header="The Tools Menu" Tag="Help /ToolsMenu.xaml"> <TreeViewItem Header="The Stylus Tool Dialog" Tag="Help/StylusToolDialog.xaml" /> <TreeViewItem Header="The Eraser Tool Dialog" Tag="Help/EraserToolDialog.xaml" /> </TreeViewItem> <TreeViewItem Header="The Help Menu" Tag="Help /HelpMenu.xaml" /> </TreeViewItem> <TreeViewItem Header="Copyright Information" Tag="Help/Copyright .xaml" /> </TreeView> <GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Center" VerticalAlignment="Stretch" /> <Frame Name="frame" Grid.Column="2" /> </Grid> </NavigationWindow.Content> </NavigationWindow>



The first column of the Grid contains a TreeView that serves as a contents list. I've set the all-purpose Tag property of each TreeViewItem to the XAML file associated with that item. On the right side of the GridSplitter is a Frame with the name of frame. The SelectedItemChanged event of the TreeView is assigned a handler that is implemented in the YellowPadHelp.cs code-behind file.

YellowPadHelp.cs

[View full width]

//---------------------------------------------- // YellowPadHelp.cs (c) 2006 by Charles Petzold //---------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.YellowPad { public partial class YellowPadHelp { public YellowPadHelp() { InitializeComponent(); // Select first item in TreeView and give it the focus. (tree.Items[0] as TreeViewItem) .IsSelected = true; tree.Focus(); } void HelpOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args) { TreeViewItem item = args.NewValue as TreeViewItem; if (item.Tag == null) return; // Navigate to the selected item's Tag property. frame.Navigate(new Uri(item.Tag as string, UriKind.Relative)); } } }



Whenever the user selects an item in the TreeView on the left, the event handler obtains the Tag property, converts it into a Uri, and passes it to the Navigate method of the Frame. The result is a fairly simple and elegant Help system. In Chapter 25 I'll show you a rather more generalized approach to displaying Help information.

The NavigationWindow, Frame, and NavigationServices classes all support several events that let you monitor the progress of a navigation and stop it if necessary. Obviously these are more important when you're loading pages over a network than when everything is on the user's hard drive.

One common type of navigation application is the wizard. A wizard is generally a series of pages occupying the same window that accumulate information. Each page of the wizard typically has buttons labeled Previous and Next, except for the first page and the last page and possibly the penultimate page. The Previous and Next buttons essentially navigate through the pages.

Because a wizard uses the Previous and Next buttons to perform navigation, a WPF wizard built inside a NavigationWindow or a Frame should not display the normal navigation chrome. (For NavigationWindow, set ShowsNavigationUI to false; for Frame, set NavigationUIVisibility to Hidden.) The program itself has to manage navigation in the Click event handlers for the Previous and Next buttons. Regardless of this "manual" handling of navigation, using WPF navigation facilities simplifies the job of writing wizards enormously. In short, you don't need to keep track of the user's journey forward and backward through the pages.

It is very desirable that the pages of a wizard do not lose information during navigation. A user who clicks a Previous button to return to an earlier page shouldn't find a blank page that requires filling in from scratch. A user who then clicks the Next button to return to a page previously visited shouldn't encounter a similar disregard for earlier work. We've all seen wizardsand, much more commonly, Web pagesthat don't implement navigation well, and these programs are not to be emulated.

I'm going to show you a fairly simple wizard designed around the concept of a computer dating service. Don't let the 15 source code files in this project scare you! Each of the five pages of the wizard requires a XAML file and a C# file. One of the pages has an option to display another page, which requires another XAML file and C# file. The window that holds the whole thing together is yet another XAML file and another C# file.

The fifteenth file of the project is the one shown here. This is the file with public fields for all the information to be accumulated by the wizard.

Vitals.cs

[View full width]

//--------------------------------------- // Vitals.cs (c) 2006 by Charles Petzold //--------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public class Vitals { public string Name; public string Home; public string Gender; public string FavoriteOS; public string Directory; public string MomsMaidenName; public string Pet; public string Income; public static RadioButton GetCheckedRadioButton(GroupBox grpbox) { Panel pnl = grpbox.Content as Panel; if (pnl != null) { foreach (UIElement el in pnl.Children) { RadioButton radio = el as RadioButton; if (radio != null && (bool)radio.IsChecked) return radio; } } return null; } } }



These are the "vital statistics" that the wizard accumulates in its journey through the pages. The fields are string variables for reasons of simplicity, but some of these fields derive from RadioButton controls grouped in a StackPanel within a GroupBox. I have therefore included a static method that helps extract the checked RadioButton from the group.

During the entire time it's running, the Computer Dating Wizard program displays a single small window, which is based on the following XAML file.

ComputerDatingWizard.xaml

[View full width]

<!-- === ==================================================== ComputerDatingWizard.xaml (c) 2006 by Charles Petzold ============================================= ========== --> <Window xmlns="http://schemas.microsoft.com/winfx/ 2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.ComputerDatingWizard" WindowStartupLocation="CenterScreen" Title="Computer Dating Wizard" Width="300" Height="300" ResizeMode="NoResize"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Margin="12" FontSize="16" FontStyle="Italic" HorizontalAlignment="Center"> Computer Dating Wizard </TextBlock> <Frame Grid.Row="1" Name="frame" NavigationUIVisibility="Hidden" Padding="6" /> </Grid> </Window>



The window has a fixed size, which is common for wizards. The client area is divided between the text "Computer Dating Wizard" and a Frame in which the various pages will appear. The text string simply symbolizes an area of the window that can remain the same throughout the wizard's existence. It doesn't need to be at the top, and it doesn't have to be text.

Notice that the NavigationUIVisibility property of the Frame is set to Hidden. You don't want the Frame to display its navigation chrome because the buttons on the various pages will be handling navigation instead.

The C# part of this class contains the Main methodI could have used an application definition file instead, of courseand a constructor that calls InitializeComponent.

ComputerDatingWizard.cs

[View full width]

//--------- -------------------------------------------- // ComputerDatingWizard.cs (c) 2006 by Charles Petzold //------------------------------------------------ ----- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public partial class ComputerDatingWizard { [STAThread] public static void Main() { Application app = new Application(); app.Run(new ComputerDatingWizard()); } public ComputerDatingWizard() { InitializeComponent(); // Navigate to the greeting page. frame.Navigate(new WizardPage0()); } } }



The constructor concludes with an alternative form of the Navigate method that requires an object rather than a Uri instance. Although the argument to Navigate is defined as object, it's really the root element of a tree, so it corresponds closely to a XAML file specified as a URI. However, this form of Navigate is a little more versatile in some cases. Rather than passing the newly created object directly to Navigate, you might set some properties of the object. Or, you might create the object with a constructor that requires parameters. I'll be using this latter technique to pass an instance of the Vitals class through the various pages of the wizard.

The ComputerDatingWizard class is able to call Navigate directly on the Frame control because the Frame control is part of the class. Future pages won't have direct access to that Frame object, so they will have to use a NavigationServices object to perform navigation.

The WizardPage0 class is simply an introductory message with a single button labeled Begin:

WizardPage0.xaml

[View full width]

<!-- ============================================== WizardPage0.xaml (c) 2006 by Charles Petzold ============================================= = --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.WizardPage0"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <FlowDocumentScrollViewer Grid.Row="0" Margin="6" VerticalScrollBarVisibility="Hidden"> <FlowDocument FontSize="10pt"> <Paragraph> Welcome to the Computer Dating Wizard. This program probes the <Italic>Inner You</Italic> to match you with the mate of your dreams. </Paragraph> <Paragraph> To begin, click the Begin button. </Paragraph> </FlowDocument> </FlowDocumentScrollViewer> <!-- Navigation button at bottom-right corner of page. --> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="BeginButtonOnClick" MinWidth="72" Margin="6" Content="Begin &gt;" /> </Grid> </Grid> </Page>



Keep in mind that the Frame in which this page appears is a fixed size, governed by the total size of the window set in ComputerDatingWizard.xaml minus the size of the TextBlock that appears in the window above the Frame. But the navigation buttons that appear in each page should be at the bottom of the client area. The Grid in WizardPage0.xaml defines three rows, the first and third with a size of Auto and the second occupying all leftover space. The FlowDocumentScrollViewer occupies the first row and the third row is occupied by a second Grid panel defined toward the bottom of the file. The second row is essentially unused but its existence serves to position the third row at the bottom. Similarly, the second Grid defines two columns, the first using all leftover space and the second having an Auto width. The first column is unused and the Begin button occupies the second column. The result is that the button is positioned at the far right of the window.

I use this technique throughout this project to keep the buttons in the same position relative to the window.

The code-behind file for WizardPage0 has the event handler for the Begin button.

WizardPage0.cs

[View full width]

//-------------------------------------------- // WizardPage0.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public partial class WizardPage0 { public WizardPage0() { InitializeComponent(); } void BeginButtonOnClick(object sender, RoutedEventArgs args) { if (NavigationService.CanGoForward) NavigationService.GoForward(); else { Vitals vitals = new Vitals(); WizardPage1 page = new WizardPage1 (vitals); NavigationService.Navigate(page); } } } }



There are two ways that a user can be looking at the WizardPage0 page. The first way is when the program starts up. The second way is by clicking the Previous button on the next page (WizardPage1.xaml). Because this program is using the Navigate method to move from page to page, the pages are accumulated in the journal. The NavigationWindow, Frame, and NavigationServices classes all define two properties that indicate whether the journal contains an entry to go back to the previous page, or an entry to go forward to a previously visited page. These properties are CanGoBack and CanGoForward, and the NavigationWindow, Frame, and NavigationServices classes also have GoBack and GoForward methods to actually navigate to those pages.

If the user starts up the wizard, clicks the Begin button on WizardPage0.xaml, clicks the Previous button on WizardPage1.xaml, and then clicks the Begin button on WizardPage0.xaml again, the Click handler for the Begin button knows what's happened because the CanGoForward property will be true. The Click handler can then call GoForward to navigate to WizardPage1.xaml. The advantage of calling GoBack and GoFoward (when they're available) is that the page has already been created, loaded, and perhaps modified by the user, and navigating with these methods preserves the user's input to the page.

If the CanGoForward property is false, the user is seeing WizardPage0.xaml for the first time. When the user clicks the Begin button, the WizardPage0 class creates a new object of type Vitals. This single object will persist throughout the wizard. The handler next creates an object of type WizardPage1, passing the Vitals object to its constructor. It concludes by navigating to that page.

The WizardPage1 class has a layout governed by this XAML file.

WizardPage1.xaml

[View full width]

<!-- ============================================== WizardPage1.xaml (c) 2006 by Charles Petzold ============================================= = --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.WizardPage1"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="50*" /> <ColumnDefinition Width="50*" /> </Grid.ColumnDefinitions> <!-- TextBox for Name. --> <StackPanel Orientation="Horizontal" Grid. ColumnSpan="2" Margin="12"> <Label> Name: </Label> <TextBox Name="txtboxName" Width="200" /> </StackPanel> <!-- GroupBox for Home. --> <GroupBox Grid.Row="1" Grid.Column="0" Name="grpboxHome" Header="Home" Margin="12"> <StackPanel> <RadioButton Content="House" Margin="6" IsChecked="True" /> <RadioButton Content="Apartment" Margin="6" /> <RadioButton Content="Cave" Margin="6" /> </StackPanel> </GroupBox> <!-- GroupBox for Gender. --> <GroupBox Grid.Row="1" Grid.Column="1" Name="grpboxGender" Header="Gender" Margin="12"> <StackPanel> <RadioButton Content="Male" Margin="6" IsChecked="True" /> <RadioButton Content="Female" Margin="6" /> <RadioButton Content="Flexible" Margin="6" /> </StackPanel> </GroupBox> <!-- Navigation buttons at bottom-right corner of page. --> <Grid Grid.Row="3" Grid.ColumnSpan="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="PreviousButtonOnClick" MinWidth="72" Margin="6" Content="&lt; Previous" /> <Button Grid.Column="2" Click="NextButtonOnClick" MinWidth="72" Margin="6" Content="Next &gt;" /> </Grid> </Grid> </Page>



This page contains a TextBox and two GroupBox controls containing three RadioButton controls each. The two buttons at the bottom are labeled Previous and Next.

The code-behind file for the WizardPage1 class has a single-parameter constructor. The Vitals object passed to the constructor is stored as a field. The constructor also needs to call InitializeComponent. (Alternatively, the code that creates the page can call InitializeComponent for the page.)

WizardPage1.cs

[View full width]

//-------------------------------------------- // WizardPage1.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public partial class WizardPage1: Page { Vitals vitals; // Constructors. public WizardPage1(Vitals vitals) { InitializeComponent(); this.vitals = vitals; } // Event handlers for Previous and Back buttons. void PreviousButtonOnClick(object sender, RoutedEventArgs args) { NavigationService.GoBack(); } void NextButtonOnClick(object sender, RoutedEventArgs args) { vitals.Name = txtboxName.Text; vitals.Home = Vitals.GetCheckedRadioButton (grpboxHome).Content as string; vitals.Gender = Vitals.GetCheckedRadioButton (grpboxGender).Content as string; if (NavigationService.CanGoForward) NavigationService.GoForward(); else { WizardPage2 page = new WizardPage2(vitals); NavigationService.Navigate(page); } } } }



The Previous button is handled by a simple call to GoBack. The event handler is very sure that GoBack will always return to WizardPage0.xaml because that's the route taken to arrive at WizardPage1.xaml.

The event handler for the Next button obtains user input from the page and stores that information in the Vitals object. This event handler is the last opportunity to get this information directly from the page. It could be that the user will return to this page, but the only way to continue the wizard is to click the Next button again, at which time the event handler will store any updated items.

The Next handler then navigates to the WizardPage2 class in the same way that WizardPage0 navigated to WizardPage1. However, notice that the event handler can make some decisions here based on the inputted values. Perhaps there are separate routes through the wizard, depending on the user's selection in one of the groups of radio buttons.

The WizardPage2 class begins with a XAML file, of course.

WizardPage2.xaml

[View full width]

<!-- ============================================== WizardPage2.xaml (c) 2006 by Charles Petzold ============================================= = --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.WizardPage2"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <!-- TextBox for favorite operating system . --> <TextBlock Grid.Row="0" Margin="0, 12, 0, 0"> Favorite operating system: </TextBlock> <TextBox Grid.Row="1" Name="txtboxFavoriteOS" Text="Microsoft Windows Vista, of course!" /> <!-- TextBox for favorite disk directory. --> <TextBlock Grid.Row="2" Margin="0, 12, 0, 0"> Favorite disk directory: </TextBlock> <TextBox Grid.Row="3" Name="txtboxFavoriteDir" Text="C:\"/> <Button Grid.Row="4" Click="BrowseButtonOnClick" HorizontalAlignment="Right" MinWidth="72" Margin="0, 2, 0, 0" Content="Browse..." /> <!-- Navigation buttons at bottom-right corner of page. --> <Grid Grid.Row="6"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="PreviousButtonOnClick" MinWidth="72" Margin="6" Content="&lt; Previous" /> <Button Grid.Column="2" Click="NextButtonOnClick" MinWidth="72" Margin="6" Content="Next &gt;" /> </Grid> </Grid> </Page>



This page is a little different. It has the normal Previous and Next buttons, but it also has a TextBox for the user's favorite disk directory together with a button labeled Browse. It's likely the user will surmise that clicking this Browse button will invoke a window or page containing a TreeView control. The user can then pick a favorite directory rather than laboriously typing it.

Clicking the Browse button is optional, so it functions as a little side excursion from the main journey through the wizard. The Browse button could be handled in one of two ways: You could display a modal dialog box that would appear as a separate window on top of the ComputerDatingWizard window, or you could implement it in a Page class that the program navigates to just like the regular wizard pages. I chose the latter approach.

The window or page invoked by the Browse button must return a value. In this case, a value of DirectoryInfo would be appropriate. The value must be returned to the code in WizardPage2 so that WizardPage2 can then fill in the TextBox with the selected disk directory. If you use a dialog box, you can define a property for this information and signal whether the user clicked OK or Cancel by setting the normal DialogResult property. You could also define a DirectoryInfo property in a Page class, of course, but returning from the page is a little more complicated. You want to remove this little side excursion from the journal so that clicking the Next button on WizardPage2.xaml doesn't execute a GoForward that puts the user back in the TreeView page selecting a favorite disk directory.

This process is facilitated by a class that derives from Page named PageFunction. The purpose of PageFunction is to display a page that returns a value. The class is generic: You define the type of the data the class returns in the definition of your PageFunction class. PageFunction also takes care of altering the journal so that the side excursion doesn't inadvertently repeat itself.

The class that I derived from PageFunction to display the TreeView is named DirectoryPage because it returns an object of type DirectoryInfo. Here's the DirectoryPage.xaml file.

DirectoryPage.xaml

[View full width]

<!-- ================================================ DirectoryPage.xaml (c) 2006 by Charles Petzold ============================================= === --> <PageFunction xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft .com/winfx/2006/xaml" xmlns:io="clr-namespace:System .IO;assembly=mscorlib" xmlns:tree="clr-namespace:Petzold .RecurseDirectoriesIncrementally" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.ComputerDatingWizard.DirectoryPage" x:TypeArguments="io:DirectoryInfo"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" FontSize="16" FontStyle="Italic" HorizontalAlignment="Center"> Computer Dating Wizard </TextBlock> <tree:DirectoryTreeView x:Name="treevue" Grid.Row="1" /> <!-- Buttons at bottom-right corner of page. --> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="CancelButtonOnClick" IsCancel="True" MinWidth="60" Margin="6"> Cancel </Button> <Button Grid.Column="2" Name="btnOk" Click="OkButtonOnClick" IsEnabled="False" IsDefault="True" MinWidth="60" Margin="6"> OK </Button> </Grid> </Grid> </PageFunction>



Notice that the x:Class attribute on the root element defines the name of the class as usual, but because PageFunction is a generic class, the root element also requires an x:TypeArguments attribute to indicate the generic type, in this case DirectoryInfo. A namespace declaration for System.IO (in which DirectoryInfo is defined) is required for the namespace prefix. Another namespace declaration associates the tree prefix with the namespace of the RecurseDirectoriesIncrementally project from Chapter 16. That's where the DirectoryTreeView class comes from that forms the bulk of this page. The PageFunction concludes with buttons labeled "Cancel" and "OK."

Here's the DirectoryPage.cs file that completes the DirectoryPage class.

DirectoryPage.cs

[View full width]

//---------------------------------------------- // DirectoryPage.cs (c) 2006 by Charles Petzold //---------------------------------------------- using Petzold.RecurseDirectoriesIncrementally; using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Navigation; namespace Petzold.ComputerDatingWizard { public partial class DirectoryPage : PageFunction<DirectoryInfo> { // Constructor. public DirectoryPage() { InitializeComponent(); treevue.SelectedItemChanged += TreeViewOnSelectedItemChanged; } // Event handler to enable OK button. void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> args) { btnOk.IsEnabled = args.NewValue != null; } // Event handlers for Cancel and OK. void CancelButtonOnClick(object sender, RoutedEventArgs args) { OnReturn(new ReturnEventArgs<DirectoryInfo>()); } void OkButtonOnClick(object sender, RoutedEventArgs args) { DirectoryInfo dirinfo = (treevue.SelectedItem as DirectoryTreeViewItem).DirectoryInfo; OnReturn(new ReturnEventArgs<DirectoryInfo>(dirinfo)); } } }



The constructor attaches a SelectionChanged event handler to the DirectoryTreeView control so that the OK button is enabled only if a disk directory has been selected. It would also be possible for the constructor to have an argument so that the PageFunction derivative could initialize itself based on information from the invoking page.

A class derived from PageFunction terminates by calling OnReturn with an argument of type ReturnEventArgs, another generic class that requires the type of object the PageFunction derivative is returning. The object itself is passed to the ReturnEventArgs constructor. Notice that the event handler for the Cancel button doesn't pass an argument to the ReturnEventArgs constructor. When the PageFunction is finished, it automatically removes itself from the journal, as indicated by the default true value of its RemoveFromJournal property.

The WizardPage2 class has the Click event handler for the Browse button that navigates to the DirectoryPage, as shown in the WizardPage2.cs code-behind file.

WizardPage2.cs

[View full width]

//-------------------------------------------- // WizardPage2.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Navigation; namespace Petzold.ComputerDatingWizard { public partial class WizardPage2 { Vitals vitals; // Constructor. public WizardPage2(Vitals vitals) { InitializeComponent(); this.vitals = vitals; } // Event handlers for optional Browse button. void BrowseButtonOnClick(object sender, RoutedEventArgs args) { DirectoryPage page = new DirectoryPage(); page.Return += DirPageOnReturn; NavigationService.Navigate(page); } void DirPageOnReturn(object sender, ReturnEventArgs<DirectoryInfo> args) { if (args.Result != null) txtboxFavoriteDir.Text = args .Result.FullName; } // Event handlers for Previous and Back buttons. void PreviousButtonOnClick(object sender, RoutedEventArgs args) { NavigationService.GoBack(); } void NextButtonOnClick(object sender, RoutedEventArgs args) { vitals.FavoriteOS = txtboxFavoriteOS.Text; vitals.Directory = txtboxFavoriteDir.Text; if (NavigationService.CanGoForward) NavigationService.GoForward(); else { WizardPage3 page = new WizardPage3 (vitals); NavigationService.Navigate(page); } } } }



When the user clicks the Browse button, the event handler creates a DirectoryPage object, sets a handler for the Return event, and navigates to that page. The handler for the Return event obtains the value returned from the PageFunction from the Result property of the ReturnEventArgs argument to the handler. For the PageFunction derivative DirectoryPage, that Result property is an object of type DirectoryInfo. It will be null if the user ended the page by clicking Cancel; otherwise it indicates the user's favorite directory. The event handler transfers the directory name into the TextBox.

The remainder of WizardPage2.cs is normal. The handler for the Next button saves the user's input and goes on to WizardPage3.xaml.

The WizardPage3.xaml page is very much like WizardPage1.xaml except that the Next button is replaced by Finish, indicating that this is the last page of input. As usual, the XAML file defines the layout of controls and buttons.

WizardPage3.xaml

[View full width]

<!-- ============================================== WizardPage3.xaml (c) 2006 by Charles Petzold ============================================= = --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.WizardPage3"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="50*" /> <ColumnDefinition Width="50*" /> </Grid.ColumnDefinitions> <!-- TextBox for mother's maiden name. --> <StackPanel Orientation="Horizontal" Grid. ColumnSpan="2" Margin="12"> <Label> Mother's Maiden Name: </Label> <TextBox Name="txtboxMom" Width="100" /> </StackPanel> <!-- GroupBox for pet. --> <GroupBox Grid.Row="1" Grid.Column="0" Name="grpboxPet" Header="Favorite Pet" Margin="12"> <StackPanel> <RadioButton Content="Dog" Margin="6" IsChecked="True" /> <RadioButton Content="Cat" Margin="6" /> <RadioButton Content="Iguana" Margin="6" /> </StackPanel> </GroupBox> <!-- GroupBox for income level. --> <GroupBox Grid.Row="1" Grid.Column="1" Name="grpboxIncome" Header="Income Level" Margin="12"> <StackPanel> <RadioButton Content="Rich" Margin="6" IsChecked="True" /> <RadioButton Content="So-so" Margin="6" /> <RadioButton Content="Freelancer" Margin="6" /> </StackPanel> </GroupBox> <!-- Navigation buttons at bottom-right corner of page. --> <Grid Grid.Row="3" Grid.ColumnSpan="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="PreviousButtonOnClick" MinWidth="72" Margin="6" Content="&lt; Previous" /> <Button Grid.Column="2" Click="FinishButtonOnClick" MinWidth="72" Margin="6" Content="Finish &gt;" /> </Grid> </Grid> </Page>



The code-behind file for WizardPage3 is shown here.

WizardPage3.cs

[View full width]

//-------------------------------------------- // WizardPage3.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public partial class WizardPage3: Page { Vitals vitals; // Constructor. public WizardPage3(Vitals vitals) { InitializeComponent(); this.vitals = vitals; } // Event handlers for Previous and Finish buttons. void PreviousButtonOnClick(object sender, RoutedEventArgs args) { NavigationService.GoBack(); } void FinishButtonOnClick(object sender, RoutedEventArgs args) { // Save information from this page. vitals.MomsMaidenName = txtboxMom.Text; vitals.Pet = Vitals.GetCheckedRadioButton (grpboxPet).Content as string; vitals.Income = Vitals.GetCheckedRadioButton (grpboxIncome).Content as string; // Always re-create the final page. WizardPage4 page = new WizardPage4 (vitals); NavigationService.Navigate(page); } } }



As usual, the event handler for the Next button (here called the Finish button) pulls user input from the page and stores it in the Vitals object. However, rather than determining whether it can call GoForward to navigate to the last page, this event handler always re-creates an object of type WizardPage4 before navigating there. You'll see why shortly.

The last page is based on the WizardPage4 class and simply displays all the information accumulated in the wizard. I choose a fairly simple approach to displaying textual information by defining a bunch of Run objects that are part of the same TextBlock element.

WizardPage4.xaml

[View full width]

<!-- ============================================== WizardPage4.xaml (c) 2006 by Charles Petzold ============================================= = --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.WizardPage4"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0"> <LineBreak /> <Run Text="Name: " /> <Run Name="runName" /> <LineBreak /> <Run Text="Home: " /> <Run Name="runHome" /> <LineBreak /> <Run Text="Gender: " /> <Run Name="runGender" /> <LineBreak /> <Run Text="Favorite OS: " /> <Run Name="runOS" /> <LineBreak /> <Run Text="Favorite Directory: " /> <Run Name="runDirectory" /> <LineBreak /> <Run Text="Mother's Maiden Name: " /> <Run Name="runMomsMaidenName" /> <LineBreak /> <Run Text="Favorite Pet: " /> <Run Name="runPet" /> <LineBreak /> <Run Text="Income Level: " /> <Run Name="runIncome" /> </TextBlock> <!-- Navigation button at bottom-right corner of page. --> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Button Grid.Column="1" Click="PreviousButtonOnClick" MinWidth="72" Margin="6" Content="&lt; Previous" /> <Button Grid.Column="2" Click="SubmitButtonOnClick" MinWidth="72" Margin="6" Content="Submit!" /> </Grid> </Grid> </Page>



The two buttons are now labeled "Previous" and "Submit." The user is allowed to go back and change something after viewing this information. The code-behind file completes the WizardPage4 class.

WizardPage4.cs

[View full width]

//-------------------------------------------- // WizardPage4.cs (c) 2006 by Charles Petzold //-------------------------------------------- using System; using System.Windows; using System.Windows.Controls; namespace Petzold.ComputerDatingWizard { public partial class WizardPage4: Page { // Constructor. public WizardPage4(Vitals vitals) { InitializeComponent(); // Set text in the page. runName.Text = vitals.Name; runHome.Text = vitals.Home; runGender.Text = vitals.Gender; runOS.Text = vitals.FavoriteOS; runDirectory.Text = vitals.Directory; runMomsMaidenName.Text = vitals .MomsMaidenName; runPet.Text = vitals.Pet; runIncome.Text = vitals.Income; } // Event handlers for Previous and Submit buttons. void PreviousButtonOnClick(object sender, RoutedEventArgs args) { NavigationService.GoBack(); } void SubmitButtonOnClick(object sender, RoutedEventArgs args) { MessageBox.Show("Thank you!\n\nYou will be contacted by email " + "in four to six months.", Application.Current .MainWindow.Title, MessageBoxButton.OK, MessageBoxImage.Exclamation); Application.Current.Shutdown(); } } }



As you can see, the WizardPage4 class uses its constructor to set all the Run objects on the page with the final results of the Vitals class. This is why the WizardPage3 class re-creates the WizardPage4 object whenever it navigates to it. If the user decides to click the Previous button and change something, the WizardPage4 object must display the new information, and the most convenient place to do that is its constructor. (If you'd prefer, you can let the Page class be created only once and then set the information in the Loaded event handler, which is called whenever the page is displayed.)

Clicking Submit concludes the wizard (but doesn't get you a date).

Although wizards require significant code to accompany the XAML files, some navigation applications need no code at all. Using Windows Explorer, take a look at the BookReader directory of Chapter 22 of the companion content for this book. You'll find a collection of XAML files that include a few paragraphs from a few chapters of two books by Lewis Carroll. Launch the BookReaderPage.xaml file, which is shown here.

BookReaderPage.xaml

[View full width]

<!-- ===================== BookReaderPage.xaml ===================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" WindowTitle="Book Reader"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="10*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="90*" /> </Grid.RowDefinitions> <!-- Frame for list of books. --> <Frame Grid.Row="0" Source="BookList.xaml" /> <GridSplitter Grid.Row="1" Height="6" HorizontalAlignment="Stretch" VerticalAlignment="Center" /> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="25*" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="75*" /> </Grid.ColumnDefinitions> <!-- Frame for table of contents. --> <Frame Grid.Column="0" Name="frameContents" /> <GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Center" VerticalAlignment="Stretch" /> <!-- Frame for the actual text. --> <Frame Grid.Column="2" Name="frameChapter" /> </Grid> </Grid> </Page>



When you launch this file, Internet Explorer displays the page. The two Grid panels divide the page into three areas separated by splitters. Each area is occupied by a Frame control. The Frame at the left has the name frameContents. The Frame at the right has the name frameChapter. The Frame at the top has its Source property set to BookList.xaml, which is the Page file shown here.

BookList.xaml

[View full width]

<!-- =============== BookList.xaml ===============--> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation"> <WrapPanel TextBlock.FontSize="10pt"> <TextBlock Margin="12"> <Hyperlink NavigateUri="AliceInWonderland.xaml" TargetName="frameContents"> <Italic>Alice's Adventures in Wonderland</Italic> by Lewis Carroll </Hyperlink> </TextBlock> <TextBlock Margin="12"> <Hyperlink NavigateUri="ThroughTheLookingGlass.xaml" TargetName="frameContents"> <Italic>Through the Looking-Glass< /Italic> by Lewis Carroll </Hyperlink> </TextBlock> <TextBlock Margin="12"> ... </TextBlock> </WrapPanel> </Page>



The two Hyperlink elements contain not only NavigateUri attributes that reference two XAML files, but also TargetName attributes that reference the Frame at the left of the BookList.xaml page. This is how a Hyperlink can deposit a XAML page in another Frame.

The AliceInWonderland.xaml file and ThroughTheLookingGlass.xaml files are very similar. Here's the first:

AliceInWonderland.xaml

[View full width]

<!-- ======================== AliceInWonderland.xaml ======================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" Title="Alice's Adventures in Wonderland"> <StackPanel TextBlock.FontSize="10pt"> <TextBlock Margin="12 12 12 6"> <Hyperlink NavigateUri="AliceChapter01 .xaml" TargetName="frameChapter"> Chapter I </Hyperlink> </TextBlock> <TextBlock Margin="12 6 12 6"> <Hyperlink NavigateUri="AliceChapter02 .xaml" TargetName="frameChapter"> Chapter II </Hyperlink> </TextBlock> <TextBlock Margin="12 6 12 6"> <Hyperlink NavigateUri="AliceChapter03 .xaml" TargetName="frameChapter"> Chapter III </Hyperlink> </TextBlock> <TextBlock Margin="12 6 12 6"> ... </TextBlock> </StackPanel> </Page>



This is another Page that contains several Hyperlink elements. The NavigateUri attributes reference individual chapters, and the TargetName indicates that they should be displayed by the Frame at the right of the original page. Here's AliceChapter01.xaml.

AliceChapter01.xaml

[View full width]

<!-- ===================== AliceChapter01.xaml ===================== --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" Title="I. Down the Rabbit-Hole"> <FlowDocumentReader> <FlowDocument> <Paragraph TextAlignment="Center" FontSize="16pt"> Chapter I </Paragraph> <Paragraph TextAlignment="Center" FontSize="16pt"> Down the Rabbit-Hole </Paragraph> <Paragraph TextIndent="24"> Alice was beginning to get very tired of sitting by her sister on the bank, and of having nothing to do: once or twice she had peeped into the book her sister was reading, but it had no pictures or conversations in it, &#x201C;and what is the use of a book,&#x201D; thought Alice, &#x201C;without pictures or conversations?&#x201D; </Paragraph> <Paragraph TextIndent="24"> So she was considering, in her own mind (as well as she could, for the hot day made her feel very sleepy and stupid), whether the pleasure of making a daisy-chain would be worth the trouble of getting up and picking the daisies, when suddenly a White Rabbit with pink eyes ran close by her. </Paragraph> <Paragraph TextIndent="24"> There was nothing so <Italic>very< /Italic> remarkable in that; nor did Alice think it so <Italic>very</Italic> much out of the way to hear the Rabbit say to itself &#x201C;Oh dear! Oh dear! I shall be too late!&#x201D; (when she thought it over afterwards, it occurred to her that she ought to have wondered at this, but at the time it all seemed quite natural); but, when the Rabbit actually <Italic>took a watch out of its waistcoat-pocket</Italic>, and looked at it, and then hurried on, Alice started to her feet, for it flashed across her mind that she had never seen a rabbit with either a waistcoat-pocket, or a watch to take out of it, and, burning with curiosity, she ran across the field after it, and was just in time to see it pop down a large rabbit-hole under the hedge. </Paragraph> <Paragraph TextIndent="24"> In another moment down went Alice after it, never once considering how in the world she was to get out again. </Paragraph> <Paragraph TextIndent="24"> ... </Paragraph> </FlowDocument> </FlowDocumentReader> </Page>



So, when you click one of the book titles in the top frame, the contents of that book are displayed in the left frame. When you click one of the chapters in the left frame, the contents of that chapter are displayed in the right frame.

This collection of XAML files also includes an example of fragment navigation, which is similar to HTML bookmarks. The first chapter of Through the Looking-Glass includes the poem "Jabberwocky." The title of the poem is its own paragraph, which appears in the LookingGlassChapter01.xaml file like this:

<Paragraph ... Name="Jabberwocky">    Jabberwocky </Paragraph> 


The presence of the Name attribute allows navigation to that element within the page. The ThroughTheLookingGlass.xaml chapter list also includes an entry for "Jabberwocky" and navigates to that poem by separating the file name and the text in the Name attribute with a number sign:

<Hyperlink NavigateUri="LookingGlassChapter01.xaml#Jabberwocky"            TargetName="frameChapter">     "Jabberwocky" </Hyperlink> 


When organizing data into pages, some other controls might be useful. The TabControl derives from Selector and maintains a collection of TabItem controls. TabItem derives from HeaderedContentControl and the content of each TabItem occupies the same area of the page. The Header property is a tab, and the user selects which TabItem to view by clicking the tab. By default these tabs are arranged horizontally across the top, but that's changeable by the TabStripPlacement property. (I use TabControl in the TextGeometryDemo program in Chapter 28.

Another useful control of this sort is the Expander, which also derives from HeaderedContentControl. In the case of the Expander, the content is toggled between visible and collapsed by clicking the header. Often the content contains hyperlinks much like the list of chapters in the AliceInWonderland.xaml file.

Although launching the BookReaderPage.xaml file successfully allows you to browse the various books and chapters that are part of the project, it only works because all the files are in the same directory and that's the current directory associated with Internet Explorer when you launch the BookReaderPage.xaml file. It might be preferable to package all the XAML files together into one executable, and of course that's certainly possible. The XAML files you've been looking at are part of a project named BookReader, and the project also contains the BookReaderApp.xaml application definition file.

BookReaderApp.xaml

[View full width]

<!-- ================================================ BookReaderApp.xaml (c) 2006 by Charles Petzold ============================================= === --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" StartupUri="BookReaderPage.xaml" />



This file simply starts the BookReaderPage.xaml file, which is the same file you launched from Windows Explorer.

The BookReader project is a normal WPF application project containing only XAML files, and when you build the project, you get a BookReader.exe file, and when you run that file you'll see a NavigationWindow hosting the XAML files. Where did that NavigationWindow come from? None of these XAML files makes any reference to a NavigationWindow. It turns out that when the application definition file has a StartupUri of a XAML file with a root element of Page, a NavigationWindow is automatically created to host the page.

So now you've seen how Internet Explorer can host the BookReaderPage.xaml file just by launching the XAML file from Windows Explorer, and how the XAML files can be compiled into BookReader.exe, which automatically creates a NavigationWindow to host BookReaderPage.xaml page.

There is a third option, and that's the XAML Browser Application. The easiest way to create a XAML Browser Application is with Visual Studio. In the collection of projects that comprise Chapter 22, I used Visual Studio to create a XAML Browser Application named BookReader2. I deleted the MyApp.xaml, MyApp.cs, Page1.xaml, and Page1.cs files that Visual Studio created for me and instead added links in the project for all the XAML files in BookReader, making sure to flag BookReaderApp.xaml as the application definition file.

The BookReader and BookReader2 projects contain all the same XAML source code files, but when you build the BookReader2 project, Visual Studio creates a file named BookReader2.xbap. The extension stands for XAML Browser Application. When Visual Studio launches this file, PresentationHost.exe takes over. This is the same program that lets Internet Explorer host stand-alone XAML files. Internet Explorer can host the XAML Browser Application as well, and it looks the same as when it hosts the individual XAML files. The NavigationWindow created by BookReader.exe is no longer present.

Although the file that launches the application is BookReader2.xbap, two other files must also be present: BookReader2.exe and BookReader2.exe.manifest. (The file with the extension of .exe is not a stand-alone executable, however.)

Because XAML Browser Applications are hosted in the Web browser, there are certain limitations to what they can do. These programs cannot create objects of type Window or NavigationWindow. They are essentially organized around Page objects. If necessary, they move from page to page through navigation. XAML Browser Applications can have menus, but they cannot create dialog boxes or objects of type Popup. These programs run with security permissions associated with the Internet zone. They cannot use the file system.

On the other hand, these applications are very easy to run from a Web site. To install a XAML Browser Application on your Web site, use Visual Studio to bring up the project properties, click the Publish tab on the left, and in the Publishing Location field, type the FTP address you normally use for copying files to your site. You'll probably include the directory that contains the HTML page you want to run the application from, and an additional directory named after the application. Click the Publish Now button. You'll be asked for your FTP user name and password as Visual Studio copies the various files to your Web site. Now you can include a link on a Web page to the .xbap file. Users who have the .NET Framework 3.0 installed who click that link will download and run the application without requiring any permissions. However, the application is not automatically installed on the user's computer.

It's probably best if you decide early on whether a particular application should be a full-fledged Windows application or a XAML Browser Application, because they tend to be structured very differently. XAML Browser Application can't use popups or dialog boxes, so they're usually structured as navigation applications. Although XAML Browser Applications are inherently limited in comparison to Windows applications, the navigational structure may suggest different types of program features. You may discover that what appear to be limitations may turn out to be opportunities.

The last program I want to show you in this chapter is a case in point. I decided to make a XAML Browser Application version of the PlayJeuDeTacquin program from Chapter 7. In the process of altering the original program to turn it into a XAML Browser Application, a feature came to mind that had never occurred to me before.

The new project is named simply JeuDeTacquin. It begins with an application definition file.

JeuDeTacquinApp.xaml

[View full width]

<!-- === =============================================== JeuDeTacquinApp.xaml (c) 2006 by Charles Petzold ============================================= ===== --> <Application xmlns="http://schemas.microsoft.com/ winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com /winfx/2006/xaml" x:0" width="14" height="9" align="left" src="/books/4/266/1/html/2/images/ccc.gif" />.JeuDeTacquinApp" StartupUri="JeuDeTacquin.xaml"> </Application>



The JeuDeTacquin.xaml file has a root element of Page, as is normal for XAML Browser Applications, and the file describes the layout of the page. I gave the program a big title, a UniformGrid object named unigrid, and two buttons labeled Scramble and Next Larger.

JeuDeTacquin.xaml

[View full width]

<!-- =============================================== JeuDeTacquin.xaml (c) 2006 by Charles Petzold ============================================= == --> <Page xmlns="http://schemas.microsoft.com/winfx /2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx /2006/xaml" x: WindowTitle="Jeu de Tacquin" Background="LightGray" Focusable="True" KeepAlive="True"> <Grid HorizontalAlignment="Center" VerticalAlignment="Center" Background="LightGray"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" FontFamily="Garamond" FontSize="96" FontStyle="Italic" Margin="12"> Jeu de Tacquin </TextBlock> <Border Grid.Row="1" BorderBrush="Black" BorderThickness = "1" HorizontalAlignment="Center" VerticalAlignment="Center"> <UniformGrid Name="unigrid" /> </Border> <Button Grid.Row="2" HorizontalAlignment="Left" Margin="12" MinWidth="1in" Click="ScrambleOnClick"> Scramble </Button> <Button Grid.Row="2" HorizontalAlignment="Right" Margin="12" MinWidth="1in" Click="NextOnClick"> Next Larger >> </Button> <TextBlock Grid.Row="3" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="12"> (c) 2006 by Charles Petzold </TextBlock> </Grid> </Page>



The JeuDeTacquin class is completed with the following code-behind file. The two fields that indicate the number of rows and columns are no longer constants, and an additional field named isLoaded is defined. The constructor installs an event handler for the Loaded event and calls InitializeComponent. Recall that the Loaded event is fired whenever a Page appears as the subject of a navigation.

The WindowOnLoaded event handler initializes the UniformGrid with Tile objects and an Empty object. (This project includes links to Tile.cs and Empty.cs from the original PlayJeuDeTacquin project.) Notice that it sets the Title property of the Page to the text "Jeu de Tacquin" with the number of rows and columns in parentheses. This is the text that appears in the Back and Forward journal lists. The PageOnLoaded event handler also sets the isLoaded field to true so that this initialization isn't performed again for the same Page object.

JeuDeTacquin.xaml.cs

[View full width]

//--------------------------------------------- // JeuDeTacquin.cs (c) 2006 by Charles Petzold //--------------------------------------------- using Petzold.PlayJeuDeTacquin; using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace Petzold.JeuDeTacquin { public partial class JeuDeTacquin : Page { public int NumberRows = 4; public int NumberCols = 4; bool isLoaded = false; int xEmpty, yEmpty, iCounter; Key[] keys = { Key.Left, Key.Right, Key.Up , Key.Down }; Random rand; UIElement elEmptySpare = new Empty(); public JeuDeTacquin() { Loaded += PageOnLoaded; InitializeComponent(); } void PageOnLoaded(object sender, RoutedEventArgs args) { if (!isLoaded) { Title = String.Format("Jeu de Tacquin ({0}\x00D7{1})", NumberCols, NumberRows); unigrid.Rows = NumberRows; unigrid.Columns = NumberCols; // Create Tile objects to fill all but one cell. for (int i = 0; i < NumberRows * NumberCols - 1; i++) { Tile tile = new Tile(); tile.Text = (i + 1).ToString(); tile.MouseLeftButtonDown += TileOnMouseLeftButtonDown; ; unigrid.Children.Add(tile); } // Create Empty object to fill the last cell. unigrid.Children.Add(new Empty()); xEmpty = NumberCols - 1; yEmpty = NumberRows - 1; isLoaded = true; } Focus(); } void TileOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args) { Focus(); Tile tile = sender as Tile; int iMove = unigrid.Children.IndexOf (tile); int xMove = iMove % NumberCols; int yMove = iMove / NumberCols; if (xMove == xEmpty) while (yMove != yEmpty) MoveTile(xMove, yEmpty + (yMove - yEmpty) / Math.Abs (yMove - yEmpty)); if (yMove == yEmpty) while (xMove != xEmpty) MoveTile(xEmpty + (xMove - xEmpty) / Math.Abs (xMove - xEmpty), yMove); args.Handled = true; } protected override void OnKeyDown (KeyEventArgs args) { base.OnKeyDown(args); switch (args.Key) { case Key.Right: MoveTile(xEmpty - 1, yEmpty); break; case Key.Left: MoveTile(xEmpty + 1, yEmpty); break; case Key.Down: MoveTile(xEmpty, yEmpty - 1); break; case Key.Up: MoveTile(xEmpty, yEmpty + 1); break; default: return; } args.Handled = true; } void ScrambleOnClick(object sender, RoutedEventArgs args) { rand = new Random(); iCounter = 16 * NumberCols * NumberRows; DispatcherTimer tmr = new DispatcherTimer(); tmr.Interval = TimeSpan .FromMilliseconds(10); tmr.Tick += TimerOnTick; tmr.Start(); } void TimerOnTick(object sender, EventArgs args) { for (int i = 0; i < 5; i++) { MoveTile(xEmpty, yEmpty + rand .Next(3) - 1); MoveTile(xEmpty + rand.Next(3) - 1 , yEmpty); } if (0 == iCounter--) (sender as DispatcherTimer).Stop(); } void MoveTile(int xTile, int yTile) { if ((xTile == xEmpty && yTile == yEmpty) || xTile < 0 || xTile >= NumberCols || yTile < 0 || yTile >= NumberRows) return; int iTile = NumberCols * yTile + xTile; int iEmpty = NumberCols * yEmpty + xEmpty; UIElement elTile = unigrid .Children[iTile]; UIElement elEmpty = unigrid .Children[iEmpty]; unigrid.Children.RemoveAt(iTile); unigrid.Children.Insert(iTile, elEmptySpare); unigrid.Children.RemoveAt(iEmpty); unigrid.Children.Insert(iEmpty, elTile); xEmpty = xTile; yEmpty = yTile; elEmptySpare = elEmpty; } void NextOnClick(object sender, RoutedEventArgs args) { if (!NavigationService.CanGoForward) { JeuDeTacquin page = new JeuDeTacquin(); page.NumberRows = NumberRows + 1; page.NumberCols = NumberCols + 1; NavigationService.Navigate(page); } else NavigationService.GoForward(); } } }



The new feature that came to mind when I was converting this program to a XAML Browser Application was the Next Larger button. This button is implemented by the event handler at the very bottom of the C# file. The first time you click that Next Larger button, the program creates a new object of type JeuDeTacquin, and sets the number of rows and columns to one greater than the existing grid for a dimension of five rows and five columns. A call to NavigationService.Navigate navigates to that new page. However, the previous game is retained and is accessible through the Back button, where the dimensions of the grid are identified. If you go back to that game and click Next Larger again, a new five-by-five game is not created, but you navigate to the one previously created. In this way, you can have multiple games going at once, each with a different dimension.

It would never have occurred to me to implement such a feature in the earlier version of the program. Certainly a menu option to set the number of rows and columns is an obvious feature, but not the ability to have several different games going at once. It would have been too awkward to manage. But with the WPF navigation facilities, such a feature becomes almost trivial.




Applications = Code + Markup. A Guide to the Microsoft Windows Presentation Foundation
Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation (Pro - Developer)
ISBN: 0735619573
EAN: 2147483647
Year: 2006
Pages: 72

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net