Pro C#10 CHAPTER 26 WPF Controls, Layouts, Events, and Data Binding

CHAPTER 26

WPF Controls, Layouts, Events, and Data Binding

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)

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_26

1137

Table 26-1. (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:

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:

private void btnMyButton_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("You clicked the button!");
}

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:

<Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="285" Width="325">

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.

<!- This button is in the center of the window at all times ->
<Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Fun with Panels!" Height="285" Width="325">

Notice in the Button’s opening definition that you have handled the Click event by specifying the name of a method to be called when the event is raised. The Click event works with the RoutedEventHandler delegate, which expects an event handler that takes an object as the first parameter and a System.Windows. RoutedEventArgs as the second. Implement this handler as so:

public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
// Do something when button is clicked.
MessageBox.Show(“Clicked the button”);
}

If you run your application, you will see this message box display, regardless of which part of the button’s content you click (the green Ellipse, the yellow Ellipse, the Label, or the Button’s surface). This is a good thing. Imagine how tedious WPF event handling would be if you were forced to handle a Click event for every one of these subelements. Not only would the creation of separate event handlers for each aspect of the Button be labor intensive, you would end up with some mighty nasty code to maintain down the road.
Thankfully, WPF routed events take care of ensuring that your single Click event handler will be called regardless of which part of the button is clicked automatically. Simply put, the routed events model automatically propagates an event up (or down) a tree of objects, looking for an appropriate handler.
Specifically speaking, a routed event can make use of three routing strategies. If an event is moving from the point of origin up to other defining scopes within the object tree, the event is said to be a bubbling event. Conversely, if an event is moving from the outermost element (e.g., a Window) down to the point of origin, the event is said to be a tunneling event. Finally, if an event is raised and handled only by the originating element (which is what could be described as a normal CLR event), it is said to be a direct event.

The Role of Routed Bubbling Events
In the current example, if the user clicks the inner yellow oval, the Click event bubbles out to the next level of scope (the Canvas), then to the StackPanel, and finally to the Button where the Click event handler is handled. In a similar way, if the user clicks the Label, the event is bubbled to the StackPanel and then finally to the Button element.
Given this bubbling routed event pattern, you have no need to worry about registering specific Click event handlers for all members of a composite control. However, if you want to perform custom clicking logic for multiple elements within the same object tree, you can do so.

By way of illustration, assume you need to handle the clicking of the outerEllipse control in a unique manner. First, handle the MouseDown event for this subelement (graphically rendered types such as the Ellipse do not support a Click event; however, they can monitor mouse button activity via MouseDown, MouseUp, etc.).

Then implement an appropriate event handler, which for illustrative purposes will simply change the
Title property of the main window, like so:

public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Change title of window.
this.Title = “You clicked the outer ellipse!”;
}

With this, you can now take different courses of action depending on where the end user has clicked (which boils down to the outer ellipse and everywhere else within the button’s scope).

■Note Routed bubbling events always move from the point of origin to the next defining scope. Thus, in this example, if you click the innerEllipse object, the event will be bubbled to the Canvas, not to the outerEllipse because they are both Ellipse types within the scope of Canvas.

Continuing or Halting Bubbling
Currently, if the user clicks the outerEllipse object, it will trigger the registered MouseDown event handler for this Ellipse object, at which point the event bubbles to the button’s Click event. If you want to inform WPF to stop bubbling up the tree of objects, you can set the Handled property of the EventArgs parameter to true, as follows:

public void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
// Change title of window.
this.Title = “You clicked the outer ellipse!”;
// Stop bubbling!
e.Handled = true;
}

In this case, you would find that the title of the window is changed, but you will not see the MessageBox displayed by the Click event handler of the Button. In a nutshell, routed bubbling events make it possible to allow a complex group of content to act either as a single logical element (e.g., a Button) or as discrete items (e.g., an Ellipse within the Button).

The Role of Routed Tunneling Events
Strictly speaking, routed events can be bubbling (as just described) or tunneling in nature. Tunneling events (which all begin with the Preview suffix; e.g., PreviewMouseDown) drill down from the topmost element into the inner scopes of the object tree. By and large, each bubbling event in the WPF base class libraries is paired with a related tunneling event that fires before the bubbling counterpart. For example, before the bubbling MouseDown event fires, the tunneling PreviewMouseDown event fires first.
Handling a tunneling event looks just like the processing of handling any other events; simply assign the event handler name in XAML (or, if needed, use the corresponding C# event-handling syntax in your code file) and implement the handler in the code file. Just to illustrate the interplay of tunneling and bubbling events, begin by handling the PreviewMouseDown event for the outerEllipse object, like so:

Next, retrofit the current C# class definition by updating each event handler (for all objects) to append data about the current event into a string member variable named mouseActivity, using the incoming event args object. This will allow you to observe the flow of events firing in the background.

public partial class MainWindow : Window
{
string _mouseActivity = string.Empty; public MainWindow()
{
InitializeComponent();
}
public void btnClickMe_Clicked(object sender, RoutedEventArgs e)
{
AddEventInfo(sender, e); MessageBox.Show(_mouseActivity, “Your Event Info”);
// Clear string for next round.
_mouseActivity = “”;
}
private void AddEventInfo(object sender, RoutedEventArgs e)
{
_mouseActivity += string.Format(
“{0} sent a {1} event named {2}.\n”, sender, e.RoutedEvent.RoutingStrategy, e.RoutedEvent.Name);
}
private void outerEllipse_MouseDown(object sender, MouseButtonEventArgs e)
{
AddEventInfo(sender, e);
}

private void outerEllipse_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
AddEventInfo(sender, e);
}
}

Notice that you are not halting the bubbling of an event for any event handler. If you run this application, you will see a unique message box display based on where you click the button. Figure 26-15 shows the result of clicking the outer Ellipse object.

Figure 26-15. Tunneling first, bubbling second

So, why do WPF events typically tend to come in pairs (one tunneling and one bubbling)? The answer is that by previewing events, you have the power to perform any special logic (data validation, disable bubbling action, etc.) before the bubbling counterpart fires. By way of an example, assume you have a TextBox that should contain only numerical data. You could handle the PreviewKeyDown event, and if you see the user has entered non-numerical data, you could cancel the bubbling event by setting the Handled property to true.
As you would guess, when you are building a custom control that contains custom events, you could author the event in such a way that it can bubble (or tunnel) through a tree of XAML. For the purpose of this chapter, I will not be examining how to build custom routed events (however, the process is not that different from building a custom dependency property). If you are interested, check out the topic “Routed Events Overview” within the .NET Framework 4.7 SDK documentation. In it you will find a number of tutorials that will help you on your way.

A Deeper Look at WPF APIs and Controls
The remainder of this chapter will give you a chance to build a new WPF application using Visual Studio. The goal is to create a UI that consists of a TabControl widget containing a set of tabs. Each tab will illustrate some new WPF controls and interesting APIs you might want to make use of in your software projects. Along the way, you will also learn additional features of the Visual Studio WPF designers. To get started, create a new WPF application named WpfControlsAndAPIs.

Working with the TabControl
As mentioned, your initial window will contain a TabControl with three different tabs, each of which shows off a set of related controls and/or WPF APIs. Update the window’s Width to 800 and Height to 350. Locate the TabControl control in the Visual Studio Toolbox, drop one onto your designer, and update the markup to the following:








You will notice that you are given two tab items automatically. To add additional tabs, you simply need to right-click the TabControl node in the Document Outline window and select the Add TabItem menu option (you can also right-click the TabControl on the designer to activate the same menu option) or just start typing in the XAML editor. Add one additional tab using either approach.
Now, update each TabItem control through the XAML editor and change the Header property for each tab, naming them Ink API, Data Binding, and DataGrid. At this point, your window designer should look like what you see in Figure 26-16.

Figure 26-16. The initial layout of the tab system

Be aware that when you select a tab for editing, that tab becomes the active tab, and you can design that tab by dragging controls from the Toolbox window. Now that you have the core TabControl defined, you can work out the details tab by tab and learn more features of the WPF API along the way.

Building the Ink API Tab
The first tab will be used to show the overall role of WPF’s digital Ink API, which allows you to incorporate painting functionality into a program easily. Of course, the application does not literally need to
be a painting application; you can use this API for a wide variety of purposes, including capturing handwriting input.

■Note For most of the rest of this chapter (and the next WPF chapters as well), i will be editing the XaMl directly instead of using the various designer windows. While the dragging and dropping of controls works, more often than not the layout isn’t what you want (visual studio adds margins and padding based on where you drop the control), and you spend a significant amount of time cleaning up the XaMl anyway.

Begin by changing the Grid tag under the Ink API TabItem to a StackPanel and add a closing tag (make sure to remove “/” from the opening tag). Your markup should look like this:



Designing the Toolbar
Add a new ToolBar control into the StackPanel (using the XAML editor) named InkToolbar with a height of 60.

Add three RadioButton controls inside a WrapPanel, inside a Border control, to the ToolBar as follows:







When a RadioButton control is not placed inside of a parent panel control, it will take on a UI identical to a Button control! That’s why I wrapped the RadioButton controls in the WrapPanel.
Next, add a Separator and then a ComboBox with a Width of 175 and a Margin of 10,0,0,0. Add three ComboBoxItem tags with content of Red, Green, and Blue, and follow the entire ComboBox with another Separator control, as follows:







The RadioButton Control
In this example, you want these three RadioButton controls to be mutually exclusive. In other GUI frameworks, ensuring that a group of related controls (such as radio buttons) were mutually exclusive required that you place them in the same group box. You don’t need to do this under WPF. Instead, you can

simply assign them all to the same group name. This is helpful because the related items do not need to be physically collected in the same area but can be anywhere in the window.
The RadioButton class includes an IsChecked property, which toggles between true and false when the end user clicks the UI element. Furthermore, RadioButton provides two events (Checked and Unchecked) that you can use to intercept this state change.

Add the Save, Load, and Delete Buttons
The final controls in the ToolBar control will be a Grid holding three Button controls. Add the following markup after the last Separator control:






发表评论