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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The StartupUri attributes points to the next file, which is FrameApplicationDemoWindow .xaml.
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.
The YellowPadHelp.xaml file contains the layout for the Help window. The root element is a NavigationWindow containing a three-column Grid.
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.
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.
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.
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.
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:
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.
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.
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.)
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.
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.
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.
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.
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.
The code-behind file for WizardPage3 is shown here.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
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. |