Chapter 25 provided a foundation for the WPF programming model, including an examination of the Window and Application classes, the grammar of XAML, and the use of code files. Chapter 25 also introduced you to the process of building WPF applications using the designers of Visual Studio. In this chapter, you will dig into the construction of more sophisticated graphical user interfaces using several new controls and layout managers, learning about additional features of the WPF Visual Designer for XAML of Visual Studio along the way.
This chapter will also examine some important related WPF control topics such as the data-binding programming model and the use of control commands. You will also learn how to use the Ink and Documents APIs, which allow you to capture stylus (or mouse) input and build rich text documents using the XML Paper Specification, respectively.
A Survey of the Core WPF Controls
Unless you are new to the concept of building graphical user interfaces (which is fine), the general purpose of the major WPF controls should not raise too many issues. Regardless of which GUI toolkit you might have used in the past (e.g., VB6, MFC, Java AWT/Swing, Windows Forms, macOS, or GTK+/GTK# [among others]), the core WPF controls listed in Table 26-1 are likely to look familiar.
Table 26-1. The Core WPF Controls
WPF Control
Category Example Members Meaning in Life
Core user input controls Button, RadioButton, ComboBox, CheckBox, Calendar, DatePicker, Expander, DataGrid, ListBox, ListView, ToggleButton, TreeView, ContextMenu, ScrollBar, Slider, TabControl, TextBlock, TextBox, RepeatButton, RichTextBox, Label WPF provides an entire family of controls you can use to build the crux of a user interface.
Window and control adornments Menu, ToolBar, StatusBar, ToolTip, ProgressBar You use these UI elements to decorate the frame of a Window object with input devices (such as the Menu) and user informational elements (e.g., StatusBar and ToolTip).
(continued)
WPF Control
Category Example Members Meaning in Life
Media controls Image, MediaElement, SoundPlayerAction These controls provide support for audio/video playback and image display.
Layout controls Border, Canvas, DockPanel, Grid, GridView, GridSplitter, GroupBox, Panel, TabControl, StackPanel, Viewbox, WrapPanel WPF provides numerous controls that allow you to group and organize other controls for the purpose of layout management.
■Note The intent of this chapter is not to walk through each and every member of each and every WPF control. Rather, you will receive an overview of the various controls with an emphasis on the underlying programming model and key services common to most WPF controls.
The WPF Ink Controls
In addition to the common WPF controls listed in Table 26-1, WPF defines additional controls for working with the digital Ink API. This aspect of WPF development is useful during tablet PC development because it lets you capture input from the stylus. However, this is not to say a standard desktop application cannot leverage the Ink API because the same controls can capture input using the mouse.
The System.Windows.Ink namespace of PresentationCore.dll contains various Ink API support types (e.g., Stroke and StrokeCollection); however, a majority of the Ink API controls (e.g., InkCanvas and InkPresenter) are packaged up with the common WPF controls under the System.Windows.Controls namespace in the PresentationFramework.dll assembly. You’ll work with the Ink API later in this chapter.
The WPF Document Controls
WPF also provides controls for advanced document processing, allowing you to build applications that incorporate Adobe PDF–style functionality. Using the types within the System.Windows.Documents namespace (also in the PresentationFramework.dll assembly), you can create print-ready documents that support zooming, searching, user annotations (sticky notes), and other rich text services.
Under the covers, however, the document controls do not use Adobe PDF APIs; rather, they use the XML Paper Specification (XPS) API. To the end user, there will really appear to be no difference because PDF documents and XPS documents have an almost identical look and feel. In fact, you can find many free utilities that allow you to convert between the two file formats on the fly. Because of space limitation, these controls won’t be covered in this edition.
WPF Common Dialog Boxes
WPF also provides you with a few common dialog boxes such as OpenFileDialog and SaveFileDialog. These dialog boxes are defined within the Microsoft.Win32 namespace of the PresentationFramework. dll assembly. Working with either of these dialog boxes is a matter of creating an object and invoking the ShowDialog() method, like so:
using Microsoft.Win32;
//omitted for brevity
private void btnShowDlg_Click(object sender, RoutedEventArgs e)
{
// Show a file save dialog.
SaveFileDialog saveDlg = new SaveFileDialog(); saveDlg.ShowDialog();
}
As you would hope, these classes support various members that allow you to establish file filters and directory paths and gain access to user-selected files. You will put these file dialogs to use in later examples; you will also learn how to build custom dialog boxes to gather user input.
A Brief Review of the Visual Studio WPF Designer
A majority of these standard WPF controls have been packaged up in the System.Windows.Controls namespace of the PresentationFramework.dll assembly. When you build a WPF application using Visual Studio, you will find most of these common controls contained in the toolbox, provided you have a WPF designer open as the active window.
Similar to other UI frameworks created with Visual Studio, you can drag these controls onto the WPF window designer and configure them using the Properties window (which you learned about in Chapter 25). While Visual Studio will generate a good amount of the XAML on your behalf, it is not uncommon to edit the markup yourself manually. Let’s review the basics.
Working with WPF Controls Using Visual Studio
You might recall from Chapter 25 that when you place a WPF control onto the Visual Studio designer, you want to set the x:Name property through the Properties window (or through XAML directly) because this allows you to access the object in your related C# code file. You might also recall that you can use the Events tab of the Properties window to generate event handlers for a selected control. Thus, you could use Visual Studio to generate the following markup for a simple Button control:
Here, you set the Content property of the Button to a simple string with the value "Click Me!".
However, thanks to the WPF control content model, you could fashion a Button that contains the following complex content:
You might also recall that the immediate child element of a ContentControl-derived class is the implied content; therefore, you do not need to define a Button.Content scope explicitly when specifying complex content. You could simply author the following:
In either case, you set the button’s Content property to a StackPanel of related items. You can also author this sort of complex content using the Visual Studio designer. After you define the layout manager for a content control, you can select it on the designer to serve as a drop target for the internal controls. At this point, you can edit each using the Properties window. If you were to use the Properties window to handle the Click event for the Button control (as shown in the previous XAML declarations), the IDE would generate an empty event handler, to which you could add your own custom code, like so:
Working with the Document Outline Editor
You should recall from the previous chapter that the Document Outline window of Visual Studio (which you can open using the View ➤ Other Windows menu) is useful when designing a WPF control that has complex content. The logical tree of XAML is displayed for the Window you are building, and if you click any of these nodes, it is automatically selected in the visual designer and the XAML editor for editing.
With the current edition of Visual Studio, the Document Outline window has a few additional features that you might find useful. To the right of any node you will find an icon that looks similar to an eyeball.
When you toggle this button, you can opt to hide or show an item on the designer, which can be helpful when you want to focus in on a particular segment to edit (note that this will not hide the item at runtime; this only hides items on the designer surface).
Right next to the “eyeball icon” is a second toggle that allows you to lock an item on the designer. As you might guess, this can be helpful when you want to make sure you (or your co-workers) do not accidentally change the XAML for a given item. In effect, locking an item makes it read-only at design time (however, you can change the object’s state at runtime).
Controlling Content Layout Using Panels
A WPF application invariably contains a good number of UI elements (e.g., user input controls, graphical content, menu systems, and status bars) that need to be well organized within various windows. After you place the UI elements, you need to make sure they behave as intended when the end user resizes the window or possibly a portion of the window (as in the case of a splitter window). To ensure your WPF
controls retain their position within the hosting window, you can take advantage of a good number of panel types (also known as layout managers).
By default, a new WPF Window created with Visual Studio will use a layout manager of type Grid (more details in just a bit). However, for now, assume a Window with no declared layout manager, like so:
When you declare a control directly inside a window that doesn’t use panels, the control is positioned dead center in the container. Consider the following simple window declaration, which contains a single Button control. Regardless of how you resize the window, the UI widget is always equidistant from all four sides of the client area. The Button’s size is determined by the assigned Height and Width properties of the Button.
You might also recall that if you attempt to place multiple elements directly within the scope of a Window, you will receive markup and compile-time errors. The reason for these errors is that a window (or any descendant of ContentControl for that matter) can assign only a single object to its Content property. Therefore, the following XAML yields markup and compile-time errors:
Obviously, a window that can contain only a single control is of little use. When a window needs to contain multiple elements, those elements must be arranged within any number of panels. The panel will contain all of the UI elements that represent the window, after which the panel itself is used as the single object assigned to the Content property.
The System.Windows.Controls namespace provides numerous panels, each of which controls how
subelements are maintained. You can use panels to establish how the controls behave if the end user resizes the window, if the controls remain exactly where they were placed at design time, if the controls reflow horizontally from left to right or vertically from top to bottom, and so forth.
You can also intermix panel controls within other panels (e.g., a DockPanel that contains a StackPanel of other items) to provide a great deal of flexibility and control. Table 26-2 documents the role of some commonly used WPF panel controls.
Table 26-2. Core WPF Panel Controls
Panel
Control Meaning in Life
Canvas Provides a classic mode of content placement. Items stay exactly where you put them at design time.
DockPanel Locks content to a specified side of the panel (Top, Bottom, Left, or Right).
Grid Arranges content within a series of cells, maintained within a tabular grid.
StackPanel Stacks content in a vertical or horizontal manner, as dictated by the Orientation property.
WrapPanel Positions content from left to right, breaking the content to the next line at the edge of the containing box. Subsequent ordering happens sequentially from top to bottom or from right to left, depending on the value of the Orientation property.
In the next few sections, you will learn how to use these commonly used panel types by copying some predefined XAML data into the kaxaml.exe application you installed in Chapter 25. You can find all these loose XAML files contained inside the PanelMarkup subfolder of your Chapter 26 code download folder.
When working with Kaxaml, to simulate resizing a window, change the height or width of the Page element in the markup.
Positioning Content Within Canvas Panels
If you come from a WinForms background, you will probably feel most at home with the Canvas panel because it allows for absolute positioning of UI content. If the end user resizes the window to an area that is smaller than the layout maintained by the Canvas panel, the internal content will not be visible until the container is stretched to a size equal to or larger than the Canvas area.
To add content to a Canvas, you begin by defining the required controls within the scope of the opening and closing Canvas tags. Next, specify the upper-left corner for each control; this is where the rendering should begin using the Canvas.Top and Canvas.Left properties. You can specify the bottom-right area indirectly in each control by setting its Height and Width properties or directly by using the Canvas.Right and Canvas.Bottom properties.
To see Canvas in action, open the provided SimpleCanvas.xaml file using kaxaml.exe. You should see the following Canvas definition (if loading these examples into a WPF application, you will want to change the Page tag to a Window tag):
You should see the window shown in Figure 26-1 in the top half of the screen.
Figure 26-1. The Canvas layout manager allows for absolute positioning of content
Note that the order you declare content within a Canvas is not used to calculate placement; instead, placement is based on the control’s size and the Canvas.Top, Canvas.Bottom, Canvas.Left, and Canvas. Right properties.
■Note if subelements within a Canvas do not define a specific location using attached property syntax (e.g.,
Canvas.Left and Canvas.Top), they automatically attach to the extreme upper-left corner of Canvas.
Using the Canvas type might seem like the preferred way to arrange content (because it feels so familiar), but this approach does suffer from some limitations. First, items within a Canvas do not dynamically resize themselves when applying styles or templates (e.g., their font sizes are unaffected). Second, the Canvas will not attempt to keep elements visible when the end user resizes the window to a smaller surface.
Perhaps the best use of the Canvas type is for positioning graphical content. For example, if you were building a custom image using XAML, you certainly would want the lines, shapes, and text to remain in the same location, rather than see them dynamically repositioned as the user resizes the window! You’ll revisit Canvas in Chapter 27 when you examine WPF’s graphical rendering services.
Positioning Content Within WrapPanel Panels
A WrapPanel allows you to define content that will flow across the panel as the window is resized. When positioning elements in a WrapPanel, you do not specify top, bottom, left, and right docking values as you typically do with Canvas. However, each subelement is free to define a Height and Width value (among other property values) to control its overall size in the container.
Because content within a WrapPanel does not dock to a given side of the panel, the order in which you declare the elements is important (content is rendered from the first element to the last). If you were to load the XAML data found within the SimpleWrapPanel.xaml file, you would find it contains the following markup (enclosed within a Page definition):
When you load this markup, the content looks out of sorts because it flows from left to right across the window (see Figure 26-2).
Figure 26-2. Content in a WrapPanel behaves much like a traditional HTML page
By default, content within a WrapPanel flows from left to right. However, if you change the value of the
Orientation property to Vertical, you can have content wrap in a top-to-bottom manner.
You can declare a WrapPanel (as well as some other panel types) by specifying ItemWidth and ItemHeight values, which control the default size of each item. If a subelement does provide its own Height and/or Width value, it will be positioned relative to the size established by the panel. Consider the following markup:
The rendered code looks like Figure 26-3 (notice the size and position of the Button control, which has a specified unique Width value).
Figure 26-3. A WrapPanel can establish the width and height of a given item
As you might agree after looking at Figure 26-3, a WrapPanel is not typically the best choice for arranging content directly in a window because its elements can become scrambled as the user resizes the window. In most cases, a WrapPanel will be a subelement to another panel type, allowing a small area of the window to wrap its content when resized (e.g., a ToolBar control).
Positioning Content Within StackPanel Panels
Like a WrapPanel, a StackPanel control arranges content into a single line that can be oriented horizontally or vertically (the default), based on the value assigned to the Orientation property. The difference, however, is that the StackPanel will not attempt to wrap the content as the user resizes the window. Rather, the
items in the StackPanel will simply stretch (based on their orientation) to accommodate the size of the StackPanel itself. For example, the SimpleStackPanel.xaml file contains the following markup, which results in the output shown in Figure 26-4:
Figure 26-4. Vertical stacking of content
If you assign the Orientation property to Horizontal as follows, the rendered output will match that shown in Figure 26-5:
Figure 26-5. Horizontal stacking of content
Again, as is the case with the WrapPanel, you will seldom want to use a StackPanel to arrange content directly within a window. Instead, you should use StackPanel as a subpanel to a master panel.
Positioning Content Within Grid Panels
Of all the panels provided with the WPF APIs, Grid is far and away the most flexible. Like an HTML table, the Grid can be carved up into a set of cells, each one of which provides content. When defining a Grid, you perform these three steps:
1.Define and configure each column.
2.Define and configure each row.
3.Assign content to each cell of the grid using attached property syntax.
■Note if you do not define any rows or columns, the Grid defaults to a single cell that fills the entire surface of the window. Furthermore, if you do not assign a cell value (column and row) for a subelement within a Grid, it automatically attaches to column 0, row 0.
You achieve the first two steps (defining the columns and rows) by using the Grid.ColumnDefinitions and Grid.RowDefinitions elements, which contain a collection of ColumnDefinition and RowDefinition elements, respectively. Each cell within a grid is indeed a true .NET object, so you can configure the look and feel and behavior of each cell as you see fit.
Here is a Grid definition (that you can find in the SimpleGrid.xaml file) that arranges your UI content, as shown in Figure 26-6:
Figure 26-6. The Grid panel in action
Notice that each element (including a light green Rectangle element thrown in for good measure) connects itself to a cell in the grid using the Grid.Row and Grid.Column attached properties. By default, the ordering of cells in a grid begins at the upper left, which you specify using Grid.Column="0" Grid.Row="0". Given that your grid defines a total of four cells, you can identify the bottom-right cell using Grid.Column="1" Grid.Row="1".
Sizing Grid Columns and Rows
Columns and rows in a grid can be sized in one of three ways.
•Absolute sizing (e.g., 100)
•Autosizing
•Relative sizing (e.g., 3x)
Absolute sizing is exactly what you would expect; the column (or row) is sized to a specific number of device-independent units. Autosizing sizes each column or row based on the controls contained with the column or row. Relative sizing is pretty much equivalent to percentage sizing in CSS. The total count of the numbers in relatively sized columns or rows gets divided into the total amount of available space.
In the following example, the first row gets 25 percent of the space, and the second row gets 75 percent of the space:
Grids with GridSplitter Types
Grid objects can also support splitters. As you might know, splitters allow the end user to resize rows or columns of a grid type. As this is done, the content within each resizable cell will reshape itself based on how the items have been contained. Adding splitters to a Grid is easy to do; you simply define the GridSplitter control, using attached property syntax to establish which row or column it affects.
Be aware that you must assign a Width or Height value (depending on vertical or horizontal splitting) for the splitter to be visible on the screen. Consider the following simple Grid type with a splitter on the first column (Grid.Column = "0"). The contents of the provided GridWithSplitter.xaml file look like this:
First, notice that the column that will support the splitter has a Width property of Auto. Next, notice that the GridSplitter uses attached property syntax to establish which column it is working with. If you were
to view this output, you would find a five-pixel splitter that allows you to resize each Label. Note that the content fills up the entire cell because you have not specified Height or Width properties for either Label (see Figure 26-7).
Figure 26-7. Grid types containing splitters
Positioning Content Within DockPanel Panels
DockPanel is typically used as a container that holds any number of additional panels for grouping related content. DockPanels use attached property syntax (as shown with the Canvas or Grid types) to control where each item docks itself within the DockPanel.
The SimpleDockPanel.xaml file defines the following simple DockPanel definition that results in the output shown in Figure 26-8:
Figure 26-8. A simple DockPanel
■Note if you add multiple elements to the same side of a DockPanel, they will stack along the specified edge in the order they are declared.
The benefit of using DockPanel types is that, as the user resizes the window, each element remains connected to the specified side of the panel (through DockPanel.Dock). Also notice that the opening DockPanel tag in this example sets the LastChildFill attribute to true. Given that the Button control is indeed the “last child” in the container, it will therefore be stretched within the remaining space.
Enabling Scrolling for Panel Types
It is worth pointing out that WPF supplies a ScrollViewer class, which provides automatic scrolling behaviors for data within panel objects. The SimpleScrollViewer.xaml file defines the following:
You can see the result of the previous XAML definition in Figure 26-9 (notice the scrollbar on the right since the window isn’t sized to show all five buttons).
Figure 26-9. Working with the ScrollViewer type
As you would expect, each panel provides numerous members that allow you to fine-tune content placement. On a related note, many WPF controls support two properties of interest (Padding and Margin) that allow the control itself to inform the panel how it wants to be treated. Specifically, the Padding property controls how much extra space should surround the interior control, while Margin controls the extra space around the exterior of a control.
This wraps up this chapter’s look at the major panel types of WPF, as well as the various ways they position their content. Next, you’ll learn how to use the Visual Studio designers to create layouts.
Configuring Panels Using the Visual Studio Designers
Now that you have been given a walk-through of the XAML used to define some common layout managers, you will be happy to know that Visual Studio has very good design-time support for constructing your layouts. The key to doing so lies with the Document Outline window described earlier in this chapter. To illustrate some of the basics, create a new WPF application project named VisualLayoutTester.
Recall that the WFP Application project template doesn’t set the options for implicit using statements or nullable reference types. For each of the projects in this chapter, I’ve enabled global implicit using statements and disabled nullable reference types at the project level.
WinExe net6.0-windows true disable enable
Notice how your initial Window makes use of a Grid layout by default, as shown here:
If you are happy using the Grid layout system, notice in Figure 26-10 that you can easily carve out and resize the grid’s cells using the visual layout. To do so, first select the Grid component in your Document Outline window and then click the grid’s border to create new rows and columns.
Figure 26-10. The Grid control can be visually cut into cells using the IDE’s designer
Now, let’s say you have defined a grid with some number of cells. You can then drag and drop controls into a given cell of the layout system, and the IDE will automatically set the Grid.Row and Grid.Column properties of the control in question. Here is some possible markup generated by the IDE after dragging a Button into a predefined cell:
Now, let’s say you would rather not use a Grid at all. If you right-click any layout node in the Document Outline window, you will find a menu option that allows you to change the current container into another (see Figure 26-11). Be aware that when you do so, you will (most likely) radically change the positioning of the controls because the controls will conform to the rules of the new panel type.
Figure 26-11. The Document Outline window allows you to convert to new panel types
Another handy trick is the ability to select a set of controls on the visual designer and group them into a new, nested layout manager. Assume you have a Grid that contains a set of random objects. Now, select a set of items on the designer by holding down the Ctrl key and clicking each item with the left mouse button. If you then right-click the selection, you can group the selected items into a new subpanel (see Figure 26-12).
Figure 26-12. Grouping items into a new subpanel
After you have done so, examine the Document Outline window once again to verify the nested layout system. As you build full-featured WPF windows, you will most likely always need to make use of a nested layout system, rather than simply picking a single panel for all of the UI display (in fact, the remaining WPF examples in the text will typically do so). On a final note, the nodes in the Document Outline window are all drag and droppable. For example, if you wanted to move a control currently in the DockPanel into the parent panel, you could do so as suggested in Figure 26-13.
Figure 26-13. Relocating items via the Document Outline window
As you work through the remaining WPF chapters, I’ll point out additional layout shortcuts where possible. However, it’s definitely worth your time to experiment and test various features yourself. To keep you moving in the right direction, the next example in the chapter will illustrate how to build a nested layout manager for a custom text processing application (with spell-checking!).
Building a Window’s Frame Using Nested Panels
As mentioned, a typical WPF window will not use a single panel control but instead will nest panels within other panels to gain the desired layout system. Begin by creating a new WPF application named MyWordPad and enabling implicit using statements and disabling nullable reference types:
WinExe net6.0-windows true disable enable
Your goal is to construct a layout where the main window has a topmost menu system, a toolbar under the menu system, and a status bar mounted on the bottom of the window. The status bar will contain a pane to hold text prompts that are displayed when the user selects a menu item (or toolbar button), while the menu system and toolbar will offer UI triggers to close the application and display spelling suggestions in an Expander widget. Figure 26-14 shows the initial layout you are shooting for; it also shows the spell-checking capabilities within WPF.
Figure 26-14. Using nested panels to establish a window’s UI
To begin building this UI, update the initial XAML definition for your Window type so it uses a DockPanel
child element, rather than the default Grid, as follows:
Building the Menu System
Menu systems in WPF are represented by the Menu class, which maintains a collection of MenuItem objects. When building a menu system in XAML, you can have each MenuItem handle various events. The most notable of these events is Click, which occurs when the end user selects a subitem. In this example, you begin by building the two topmost menu items (File and Tools; you will build the Edit menu later in this example), which expose Exit and Spelling Hints subitems, respectively.
In addition to handling the Click event for each subitem, you also need to handle the MouseEnter and MouseExit events, which you will use to set the status bar text in a later step. Add the following markup within your DockPanel scope:
Notice that you dock the menu system to the top of the DockPanel. Also, you use the Separator element to insert a thin horizontal line in the menu system, directly before the Exit option. Also notice that the Header values for each MenuItem contain an embedded underscore token (e.g., _Exit). You use this token to establish which letter will be underlined when the end user presses the Alt key (for keyboard shortcuts). This is a change from the & character used in Windows Forms since XAML is based on XML, and the & character has meaning in XML.
So far you’ve implemented the complete menu system definition; next, you need to implement the various event handlers. First, you have the File Exit handler, FileExit_Click(), which simply closes the window, which in turn terminates the application because this is your topmost window. The MouseEnter
and MouseExit event handlers for each subitem will eventually update your status bar; however, for now, you will simply provide shells. Finally, the ToolsSpellingHints_Click() handler for the Tools Spelling Hints menu item will also remain a shell for the time being. Here are the current updates to your code- behind file (including the updated using statements):
using System.IO; using System.Windows;
using System.Windows.Controls; using System.Windows.Input; using Microsoft.Win32;
namespace MyWordPad;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
protected void FileExit_Click(object sender, RoutedEventArgs args)
{
// Close this window.
This.Close();
}
Building Menus Visually
While it is always good to know how to manually define items in XAML, it can be a tad on the tedious side. Visual Studio supports visual design support for menu systems, toolbars, status bars, and many other UI controls. If you right-click the Menu control, you will notice an Add MenuItem option. As the name suggests, this adds a new menu item to the Menu control. After you have added a set of topmost items, you can then add submenu items and separators, expand or collapse the menu itself, and perform other menu-centric operations via a second right-click.
As you see for the remainder of the current MyWordPad example, I’ll typically show you the final generated XAML; however, do take the time to experiment with the visual designers to simplify the task at hand.
Building the Toolbar
Toolbars (represented by the ToolBar class in WPF) typically provide an alternative manner for activating a menu option. Add the following markup directly after the closing scope of your Menu definition:
Your ToolBar control consists of two Button controls, which just so happen to handle the same events and are handled by the same methods in your code file. Using this technique, you can double up your handlers to serve both menu items and toolbar buttons. Although this toolbar uses the typical push buttons, you should appreciate that the ToolBar type “is-a” ContentControl; therefore, you are free to embed any types into its surface (e.g., drop-down lists, images, and graphics). The only other point of interest here is that the Check button supports a custom mouse cursor through the Cursor property.
■Note you can optionally wrap the ToolBar element within a ToolBarTray element, which controls layout, docking, and drag-and-drop operations for a set of ToolBar objects.
Building the Status Bar
A StatusBar control will be docked to the lower portion of the DockPanel and contain a single TextBlock control, which you have not used prior to this point in the chapter. You can use a TextBlock to hold text that supports numerous textual annotations, such as bold text, underlined text, line breaks, and so forth. Add the following markup directly after the previous ToolBar definition:
Finalizing the UI Design
The final aspect of your UI design is to define a splittable Grid that defines two columns. On the left, place an Expander control that will display a list of spelling suggestions, wrapped within a StackPanel. On the right, place a TextBox control that supports multiple lines and scrollbars and includes enabled spell-checking. You mount the entire Grid to the left of the parent DockPanel. Add the following XAML markup directly under the StatusBar markup to complete the definition of your window’s UI:
—-- Define the rows and columns -->
—-- This will be filled programmatically -->
—-- This will be the area to type within -->
Implementing the MouseEnter/MouseLeave Event Handlers
At this point, the UI of your window is complete. The only remaining tasks are to provide an implementation for the remaining event handlers. Begin by updating your C# code file so that each of the MouseEnter, MouseLeave, and MouseExit handlers sets the text pane of the status bar with a fitting message to help the end user, like so:
At this point, you can run your application. You should see your status bar change its text based on which menu item/toolbar button you hover your mouse over.
Implementing the Spell-Checking Logic
The WPF API ships with built-in spell-checker support, which is independent of Microsoft Office products. This means you don’t need to use the COM interop layer to use the spell-checker of Microsoft Word; instead, you can easily add the same type of support with only a few lines of code.
You might recall that when you defined the TextBox control, you set the SpellCheck.IsEnabled property to true. When you do this, misspelled words are underlined with a red squiggle, just as they are in Microsoft Office. Even better, the underlying programming model gives you access to the spell-checker engine, which allows you to get a list of suggestions for misspelled words. Add the following code to your ToolsSpellingHints_Click() method:
// Try to get a spelling error at the current caret location. SpellingError error = txtData.GetSpellingError(txtData.CaretIndex); if (error != null)
{
// Build a string of spelling suggestions.
foreach (string s in error.Suggestions)
{
spellingHints += $"{s}\n";
}
// Show suggestions and expand the expander. lblSpellingHints.Content = spellingHints; expanderSpelling.IsExpanded = true;
}
}
The preceding code is quite simple. You simply figure out the current location of the caret in the text box by using the CaretIndex property to extract a SpellingError object. If there is an error at said location (meaning the value is not null), you loop over the list of suggestions using the aptly named Suggestions property. After you have all the suggestions for the misspelled word, you connect the data to the Label in the Expander.
So there you have it! With only a few lines of procedural code (and a healthy dose of XAML), you have the beginnings of a functioning word processor. An understanding of control commands can help you add a bit more pizzazz.
Understanding WPF Commands
WPF provides support for what might be considered control-agnostic events via the command architecture. A typical .NET Core event is defined within a specific base class and can be used only by that class or a derivative thereof. Therefore, normal .NET Core events are tightly coupled to the class in which they are defined.
In contrast, WPF commands are event-like entities that are independent from a specific control and, in many cases, can be successfully applied to numerous (and seemingly unrelated) control types. By way of a few examples, WPF supports copy, paste, and cut commands, which you can apply to a wide variety of UI elements (e.g., menu items, toolbar buttons, and custom buttons), as well as keyboard shortcuts (e.g., Ctrl+C and Ctrl+V).
While other UI toolkits (such as Windows Forms) provided standard events for such purposes, using them typically left you with redundant and hard-to-maintain code. Under the WPF model, you can use commands as an alternative. The end result typically yields a smaller and more flexible code base.
The Intrinsic Command Objects
WPF ships with numerous built-in control commands, all of which you can configure with associated keyboard shortcuts (or other input gestures). Programmatically speaking, a WPF command is any object that supports a property (often called Command) that returns an object implementing the ICommand interface, as shown here:
public interface ICommand
{
// Occurs when changes occur that affect whether
// or not the command should execute.
event EventHandler CanExecuteChanged;
// Defines the method that determines whether the command
// can execute in its current state.
bool CanExecute(object parameter);
// Defines the method to be called when the command is invoked.
void Execute(object parameter);
}
WPF provides various command classes, which expose close to 100 command objects, out of the box. These classes define numerous properties that expose specific command objects, each of which implements ICommand. Table 26-3 documents some of the standard command objects available.
Table 26-3. The Intrinsic WPF Control Command Objects
WPF Class Command Objects Meaning in Life
ApplicationCommands Close, Copy, Cut, Delete, Find, Open, Paste, Save, SaveAs, Redo, Undo Various application-level commands
ComponentCommands MoveDown, MoveFocusBack, MoveLeft, MoveRight, ScrollToEnd, ScrollToHome Various commands common to UI components
MediaCommands BoostBase, ChannelUp, ChannelDown, FastForward, NextTrack, Play, Rewind, Select, Stop Various media-centric commands
NavigationCommands BrowseBack, BrowseForward, Favorites, LastPage, NextPage, Zoom Various commands relating to the WPF navigation model
EditingCommands AlignCenter, CorrectSpellingError, DecreaseFontSize, EnterLineBreak, EnterParagraphBreak, MoveDownByLine, MoveRightByWord Various commands relating to the WPF Documents API
Connecting Commands to the Command Property
If you want to connect any of the WPF command properties to a UI element that supports the Command property (such as a Button or MenuItem), you have very little work to do. You can see how to do this by updating the current menu system so it supports a new topmost menu item named Edit and three subitems to account for copying, pasting, and cutting of textual data, like so: