The Window
class, mentioned in
Chapter 2, is the required class for
the application’s main window. To fit in with the rest of the Windows
applications, the WPF Window
class is
flexible. It supports building top-level style main windows and dialogs,
which we’ll discuss in this chapter, as well as serving as the base class
for the NavigationWindow
, which we’ll
discuss in the next chapter.
In Chapter 2, we looked at the
lifetime and services of an application, often as related to the windows
that constitute the UI of the application. However, we didn’t talk much
about the windows themselves. The Window
class derives from the ContentControl
(described in Chapter 5) and adds the chrome around the edges that
contains the title; the minimize, maximize, and close buttons; and so
on. The content can look however you want. The chrome itself has more
limited options.
The look and feel of the frame of a window is largely determined
by the Icon
, Title
, and WindowStyle
properties, the latter of which
has four options (None
, SingleBorderWindow
, ThreeDBorderWindow
, and ToolWindow
), shown in Figure 10-1 (the default is
SingleBorderWindow
).
You’ll notice that the icon and/or title are shown or not
depending on the window style. You’ll also notice that the None WindowStyle
still contains a border.
This is because, by default, a window can be resized, so it needs
resizing edges. If you turn off resizing with the ResizeMode
property, as discussed later in
this chapter, the None WindowStyle
will remove all window “crust,” leaving only the “gooey
center.”
Before you can see a window, you need to create it and show it.
Creating a window is as easy as calling new
on the Window
class or a class derived from
Window
. As you can see in Example 10-1, if you’ve defined a custom
window in XAML—Visual Studio 2005 does this when you right-click on a
project in the Solution Explorer and select Add → New Item and then
choose Window (WPF)—the constructor will need to call the InitializeComponent
method (as the Visual
Studio 2005-generated Window
classes already do).
<!-- Window1.xaml -->
<Window x:Class="WindowsApplication1.Window1"
...>
... <!-- XAML to initialize instance of custom Window class -->
</Window>
// Window1.xaml.cs
...
namespace WindowsApplication1 {
public partial class Window1 : System.Windows.Window {
public Window1( ) {
// Use XAML to initialize object of custom Window class first
InitializeComponent( );
...
}
...
}
}
In the presence of the x:Class
property, the WPF build task will
generate a partial class with an InitializeComponent
implementation. This
will load the window’s XAML and use it to initialize the properties
that make up the window object and hook up the events. If you forget
to call InitializeComponent
or do
something else before calling InitializeComponent
, your window object will
not be fully initialized.
After the window has been constructed, you can show it modally or modelessly (see Example 10-2).
Window1 window = new Window1( );
// Show modelessly
window.Show( );
// Show modally
if( window.ShowDialog( ) == true ) {
// user clicked OK (or the equivalent)
}
When a window is shown modelessly using the Show
method, the Show
method returns immediately, not waiting
for the newly shown window to close. This is the method to use to
create a new top-level window in your application.
When a window is shown modally using the ShowDialog
method, the ShowDialog
method doesn’t return until the
newly shown window is closed, disabling the other windows in your
application. The ShowDialog
method
is mostly used to show windows that act as dialogs, which we’ll see in
the "Dialogs" section, later in this
chapter.
Once a window is created, it exposes a series of events to let you monitor and affect its lifetime, as shown in Figure 10-2.
The following is a description of these events:
Initialized
Raised when the FrameworkElement
base of the Window
is initialized (essentially
when the InitializeComponent
method is called).
LocationChanged
Fired when the window is moved.
Activated
Raised when the window is activated (e.g., clicked on). If
the window is never activated, you won’t get this event (or the
corresponding Deactivated
event).
Deactivated
Raised when some other window in the system is activated.
Loaded
Raised just before the window is shown.
ContentRendered
Raised when the window’s content is visually rendered.
Closing
Raised when the window attempts to close itself. You can
cancel this by setting the CancelEventArgs
argument’s Cancel
property to true.
Closed
Raised when the window has been closed (cannot be canceled).
Unloaded
Raised after the window has been closed and removed from the visual tree. If closing the window causes the application to shut down, you won’t see this event.
You can manage the x and
y locations of a window with the Top
and Left
properties, whereas you can influence
the Z order with the TopMost
property. Across the entire desktop, all windows with the TopMost
property set to true appear above
all of the windows with the TopMost
property set to false, although the Z order of the windows within
their layer is determined by user interaction. For example, clicking
on a non-topmost window will bring it to the top of the non-topmost
layer of windows, but will not bring it in front of any topmost
windows.
If you’d like to set the startup location of a window manually,
you can do so by setting the Top
and Left
properties before showing
the window. However, if you’d prefer to simply have the window
centered on the screen or on the owner, you can set the WindowStartupLocation
property to CenterScreen
or CenterOwner
as appropriate before showing
the window:
Window1 window = new Window1( ); window.WindowStartupLocation = WindowStartupLocation.CenterScreen; window.Show( ); // will be centered on the screen
If you don’t change the WindowStartupLocation
, Manual
is the default. Manual
lets you determine the initial
position by setting Top
and
Left
. If you don’t care about the
initial position, the Manual
value
lets the Windows shell determine where your window goes.
You can get the size of the window from the ActualWidth
and ActualHeight
properties of the Window
class after (not during) the Initialized
event (e.g., Loaded
is a good place to get them). The
actual size properties are expressed (like all Window
-related sizes) in device-independent
pixels measuring 1/96th of an inch.[60] However, the ActualWidth
and ActualHeight
properties are read-only, and
therefore, you cannot use them to set the width and height. For that,
you need the Width
and Height
properties.
The size properties (Width
and Height
) are separate from the
“actual” size properties (ActualWidth
and ActualHeight
) because the actual size is
calculated based on the size, the minimum size (MinWidth
and MinHeight
), and the maximum size (MaxWidth
and MaxHeight
). For example:
Window1 window = new Window1( ); window.Show( ); // render window so ActualWidth is calculated window.MinWidth = 200; window.MaxWidth = 400; window.Width = 100; Debug.Assert(window.Width == 100); // regardless of min/max settings Debug.Assert(window.ActualWidth == 200); // bound by min/max settings
Here, we’ve set the width of the window outside the min/max
bounds. The value of the Width
property doesn’t change based on the min/max bounds, but the value of
the ActualWidth
property does. This
behavior lets you set a size outside of the min/max bounds, and that
size will stick and potentially take effect if the min/max bounds
change. This behavior also means that you should set the Width
and Height
properties to influence the size of
your window, but you’ll probably want to get the ActualWidth
and ActualHeight
properties to see what kind of
influence you’ve had.
If, instead of sizing the window manually, you’d like the size
of the window to be initially governed by the size of the content, you
can change the SizeToContent
property from the default SizeToContent
enumeration value Manual
to one of the other three enumeration
values: Width
, Height
, or WidthAndHeight
. If the content size falls
outside the min/max bounds for one or both dimensions, the min/max
bounds will still be honored. Likewise, if Width
or Height
is set manually, the SizeToContent
setting will be
ignored.
By default, all windows are resizable; however, you can change
the behavior by setting the ResizeMode
property with one of the values
from the ResizeMode
enumeration:
NoResize
, CanMinimize
, CanResize
, or CanResizeWithGrip
, as shown in Figure 10-3.
You’ll notice from Figure 10-3 that
the NoResize
resize mode causes the
Minimize and Maximize buttons to go away. It also doesn’t resize at
the edges. Likewise, CanMinimize
doesn’t allow resizing at the edges, but it shows the Minimize and
Maximize buttons (although only the Minimize button is functional). On
the other hand, the CanResize
mode
makes the window fully resizable, whereas CanResizeWithGrip
is just like CanResize
except that it puts the grip in
the lower-righthand corner. Because of the huge advances that WPF has
provided in the area of layout functionality, there should be almost
no reason to use a resize-disabling resize mode.
We use the CenterOwner
value
of the WindowStartupLocation
enumeration only if we also set the window’s Owner
property. The owner of a window dictates certain
characteristics of the windows that are owned by it:
An owned window always shows in front of its owner unless the owned window is minimized.
When an owner window is minimized, all of the owned windows are minimized (and likewise for restoration).
When the owner window is closed, so are all of the windows the owner owns.
When an owner window is activated, all of the owned windows are brought to the foreground with it.
In practice, the chief visual use of an owned window is to create a floating tool window or modeless dialog (i.e., something that has minimizing and restoration behavior shared with the owner window).
However, you should get into the practice of setting the
Owner
property because it enables
better accessibility support in WPF (which you can read about at
http://msdn2.microsoft.com/en-us/library/ms753388.aspx
or http://www.tinysells.com/102).
An owner window’s set of owned windows is available from the
OwnedWindows
collection
property.
You can control the visibility of a window with the Visibility
property, which has the following
values from the Visibility
enumeration: Visible
, Hidden
, and Collapsed
. (The Window
class treats Hidden
and Collapsed
as the same.) You can also use the
Show
and Hide
methods. In addition to stopping the
window from rendering, the Hide
method takes it out of the taskbar (assuming the ShowInTaskbar
property is set in the first
place).
If you’d like to hide the window but leave it in the taskbar,
you can set the WindowState
property to the WindowState
enumeration value Minimized
. To
restore it or make it take up the whole desktop (minus the taskbars
and the sidebar), you can use Normal
or Maximized
.
Likewise, as the user interacts with the window, the WindowState
will reflect the current state
of the window. If, while the window is minimized or maximized, you’d
like to know the location and size of what it will be upon
restoration, you can use the RestoreBounds
property. Unlike Left
, Top
, Width
, Height
, ActualWidth
, and ActualHeight
, all of which will reflect the
current window state within the min/max bounds, RestoreBounds
changes only if the window is
moved or resized while it’s in the Normal
state. This makes RestoreBounds
a handy property to keep in a
user setting for window restoration,[61] as you can see in Example 10-3.
public partial class MainWindow : System.Windows.Window { public MainWindow( ) { InitializeComponent( ); try { // Restore state from user settings Rect restoreBounds = Properties.Settings.Default.MainRestoreBounds; WindowState = WindowState.Normal; Left = restoreBounds.Left; Top = restoreBounds.Top; Width = restoreBounds.Width; Height = restoreBounds.Height; WindowState = Properties.Settings.Default.MainWindowState; } catch { } // Watch for main window to close Closing += window_Closing; } // Save state as window closes void window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { Properties.Settings.Default.MainRestoreBounds = RestoreBounds; Properties.Settings.Default.MainWindowState = WindowState; Properties.Settings.Default.Save( ); } ... }
This code assumes a couple of user settings variables named
MainRestoreBounds
and MainWindowState
of type Rect
and WindowState
, respectively. With these in
place, when the main window starts up, we set the window state
temporarily to Normal
so that we
can set the left, top, width, and height from the restored bounds.
After we do that, we can set the window state to whatever it was when
we last ran the application, all of which happens before the window is
shown, so there’s no shake ‘n’ shimmy as the window goes between
normal and another state. When the window is closing (but before it’s
closed), we tuck the data away for the next session and save the
settings. Now, no matter what state the main window is in when we
close it, we properly remember that state and the restored
state.[62]
So far, we’ve talked about windows in the abstract, referring to some usages (e.g., top-level windows, toolbox windows, main windows, etc.). One other large use for windows in Windows applications is the humble dialog, which is the subject of the rest of this chapter.
Unlike the main window, where users can interact with any number of menu items, toolbar buttons, and data controls at will, a dialog is most often meant to be a short, focused conversation with the user to get some specific data before the rest of the application can continue. The File dialog is the classic example; when the application needs the name of a file, the File dialog provides a way for the user to specify it.
In Windows, dialogs were originally designed as modal (i.e., the application has entered a “mode” where the user must answer the questions posed by the dialog and click OK, or must abort the operation by clicking the Cancel button). However, it didn’t take long for dialogs to need to continue running in concert with other accessible windows, leading to modeless operation, where the user can go back and forth between the dialog and other windows, using each at will. The Find dialog is the exemplar in this area. As users find something they’ve specified in the Find dialog, they’re free to interact with that data without dismissing the Find dialog so that they can find the next thing that matches their query without starting over.
In WPF, there is no special dialog class. Dialog interactions,
both modal and modeless, are provided by either the Window
or the NavigationWindow
(discussed in the next
chapter) as you choose. However, there is built-in support for
dialog-like interactions with WPF application users, including modal
operation, dialog styles, and data validation, as we’ll explore in this
chapter. And there are a few common dialog classes, which we’ll explore
now.
Since Windows 3.1, the operating system itself has provided
common dialogs for use by applications to keep a consistent look and
feel. WPF has three intrinsic dialogs that map to those provided by
Windows—OpenFileDialog
, SaveFileDialog
, and PrintDialog
—and they work the way you’d
expect them to.[63] Example 10-4 shows the OpenFileDialog
in action.
using Microsoft.Win32; // home of OpenFileDialog and SaveFileDialog using System.Windows.Controls; // home of PrintDialog ... string filename; void openFileDialogButton_Click(...) { OpenFileDialog dlg = new OpenFileDialog( ); dlg.FileName = filename; if (dlg.ShowDialog( ) == true) { filename = dlg.FileName; // open the file... } }
If you’d like access to other common Windows Forms dialogs, like
the folder browser dialog, you can bring in the System.Windows.Forms
assembly[64] and use them without much trouble,[65] as shown in Example 10-5.
string folder; void folderBrowserDialogButton_Click(...) { System.Windows.Forms.FolderBrowserDialog dlg = new System.Windows.Forms.FolderBrowserDialog( ); dlg.SelectedPath = folder; if( dlg.ShowDialog( ) == System.Windows.Forms.DialogResult.OK ) { folder = dlg.SelectedPath; // do something with the folder... } }
In Example 10-5,
we’re creating an instance of the FolderBrowserDialog
class from the System.Windows.Forms
namespace. The fully
qualified type name is useful instead of the typical using
statement at the top of the file so
that we don’t collide with any of the WPF types and thereby require
those to be fully qualified, too.
After creation, the code in Example 10-5 calls the
ShowDialog
method, which shows the
dialog modally until the dialog has closed. When that happens,
ShowDialog
returns with a
result—generally, OK
or Cancel
(not true
or false
, like WPF dialogs). When it’s OK
, we can use the properties of the dialog
set by the user.
Here’s the list of common dialogs provided by a combination of WPF and Windows Forms (in case it isn’t available in your personal short-term storage):
ColorDialog
(Windows
Forms, but not good for use in WPF)
FolderBrowserDialog
(Windows Forms)
FontDialog
(Windows
Forms, but not good for use in WPF)
OpenFileDialog
(WPF)
SaveFileDialog
(WPF)
PageSetupDialog
(Windows Forms)
PrintDialog
(WPF)
PrintPreviewDialog
(Windows Forms, but not good for use in WPF)
Most of them work exactly the way you’d expect them to because
they deal in types that you can easily use in either Windows Forms
or WPF (e.g., strings containing folder names or filenames work
equally well in either). On the other hand, three of them (ColorDialog
, FontDialog
, and PrintPreviewDialog
) don’t work very well
at all with WPF.
The problem with the ColorDialog
is that it passes colors in
and out using the GDI+ 4-byte representation of color from the
System.Drawing
namespace’s
Color
class. WPF, on the other
hand, has a much higher-fidelity Color
class from the System.Windows.Media
class that uses a
four-float representation. Although it’s possible to “dumb down” a
WPF color to work with the Windows Forms color dialog,[66] that’s not your best option. Instead, if I were you,
I’d check out the Color Picker Dialog sample that the WPF SDK team
has put together and hosted on their blog.[67]
Things are similarly mismatched with the Windows Forms
FontDialog
, which relies on the
GDI+ Font
class from System.Drawing
. WPF has many more
font-rendering options than GDI+ and groups them differently, as
individual properties instead of together into a single Font
property (e.g., FontFamily
, FontSize
, FontStretch
, FontWeight
, etc.). It’s possible to
shoehorn some of the WPF font properties into the Windows Forms font
dialog, and again, you’ll want to check out a sample—this time
bundled with the SDK itself.[68]
Although it’s possible to use the Windows Forms color and
font dialog from within your WPF programs (and in fact, the
samples that come with this book show how to do it), I never did
find a reasonable way to use the Windows Forms PrintPreviewDialog
from WPF. The problem
is that Windows Forms uses a completely different printing model
than the one WPF uses, so you should check out Chapter 15 for the best way to render WYSIWYG
documents to the screen in WPF.
When the standard dialogs in WPF don’t do the trick, you can, of
course, implement your own custom dialogs. You can most easily define
a custom dialog in WPF as a Window
,
like the Settings
dialog for a
mythical reporting application shown in Example 10-6.
<!-- SettingsDialog.xaml -->
<Window ...
ResizeMode="CanResizeWithGrip"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner">
<Window.Resources>
<Style TargetType="Label">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="TextBox">
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="10" />
<Setter Property="Padding" Value="5,2" />
</Style>
</Window.Resources>
<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="reportBrush" Color="{Binding ReportColor}" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition MinWidth="200" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- 1st row: report folder setting -->
<Label Grid.Row="0" Grid.Column="0"
Target="{Binding ElementName=reportFolderTextBox}">Report _Folder</Label>
<TextBox Grid.Row="0" Grid.Column="1" Name="reportFolderTextBox"
Text="{Binding ReportFolder}" />
<Button Grid.Row="0" Grid.Column="2" Name="folderBrowseButton">...</Button>
<!-- 2nd row: report color setting -->
<Button Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left"
Name="reportColorButton">
<StackPanel Orientation="Horizontal">
<Rectangle Width="15" Height="15" SnapsToDevicePixels="True"
Fill="{StaticResource reportBrush}" />
<AccessText Text="Report _Color..." Margin="10,0,0,0" />
</StackPanel>
</Button>
<!-- 3rd row: buttons -->
<StackPanel Grid.Row="2" Grid.ColumnSpan="3" Orientation="Horizontal"
HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Button Name="okButton" Width="72">OK</Button>
<Button Name="cancelButton" Width="72">Cancel</Button>
</StackPanel>
</Grid>
</Window>
In Example 10-6, we
haven’t done anything that you wouldn’t want to do in any window
layout, as you’ve seen previously in this book. About the only thing
we’ve done with a nod toward building a modal dialog box is to set the
CenterOwner
startup location and
use a horizontal stack panel to keep the OK and Cancel buttons
clustered together along the bottom right of the dialog. We’re also
using labels, access keys, and a resize grip, but those are just good
practices and not dialog-specific. With the grid lines turned on, our
settings dialog looks like Figure 10-4.
To take advantage of our custom dialog, we have but to create an instance and show it modally, as in Example 10-7.
void settingsButton_Click(object sender, RoutedEventArgs e) { // Create dialog and show it modally, centered on the owner SettingsDialog dlg = new SettingsDialog( ); dlg.Owner = this; if (dlg.ShowDialog( ) == true) { // Do something with the dialog properties ... } }
Although we’ve gotten a pretty good dialog using standard window techniques, dialogs need more to fit into a Windows world:
The initial focus is set on the correct element.
The dialog doesn’t show in the taskbar.
Data is passed in and out of the dialog.
The OK button is shown as the default button (and is activated when the Enter key is pressed).
Cancel is activated when the Esc key is pressed.
Data is validated before “OK” is really “OK.”
Addressing issues on this list, we realize our dialog doesn’t really feel like a dialog (e.g., the initial focus isn’t set). Plus, like any window, the dialog shows as another window in the taskbar (which it shouldn’t). We can fix both of these by setting two properties on the window, as shown in Example 10-8.
<!-- SettingsDialog.xaml --> <Window ... ResizeMode="CanResizeWithGrip" WindowStartupLocation="CenterOwner" FocusManager.FocusedElement="{Binding ElementName=reportFolderTextBox}" ShowInTaskbar="False"> ... </Window>
In Example 10-8,
the FocusedElement
property
allows us to bind to the element that we’d like to give the initial
focus, and the ShowInTaskBar
lets
us keep the dialog out of the taskbar. Figure 10-5 shows the results.
The only traditional dialog feature that WPF doesn’t support is the ? icon on the caption bar. However, as alternatives you can handle F1 (as described in Chapter 4), add a Help button, or use tool tips (as we’ll see) as alternatives.
Now that we’ve got our dialog looking like a dialog, we also want it to behave like one. A dialog’s behavior is governed by its lifetime, which looks roughly like the following:
Create and initialize the dialog with initial values.
Show the dialog, letting the user choose new, validated values.
Harvest the values for use in your application.
Modal dialogs are generally provided to get data from a user so that some operation can be handled on his behalf. In the case of the sample settings dialog, I’m asking for a folder to store reports and specifying what color those reports should be. That information, like the information exposed from the standard dialogs, should be exposed as properties that I can set with initial values before showing the dialog and get after the user has changed them and clicked OK, as shown in Example 10-9.
class Window1 : Window { ... Color reportColor; string reportFolder; void settingsButton_Click(object sender, RoutedEventArgs e) { // 1. Create and initialize the dialog with initial values SettingsDialog dlg = new SettingsDialog( ); dlg.Owner = this; dlg.ReportColor = reportColor; dlg.ReportFolder = reportFolder; // 2. Show the dialog, letting the user choose new, validated values if (dlg.ShowDialog( ) == true) { // 3. Harvest the values for use in your application reportColor = dlg.ReportColor; reportFolder = dlg.ReportFolder; // Do something with these values... } } }
You’ll notice that we’re using a degree of good old-fashioned
object-oriented encapsulation here, passing in and harvesting the
values using .NET properties, but having no say about how the dialog
shows those values to the user or how they’re changed. All of this
happens during the call to ShowDialog
(which we’ll get to directly).
If the user clicks the OK button (or equivalent), we trust the
dialog to let us know by returning a result of True
so that we know to make use of the
approved new values.
When you’re implementing a custom dialog, how you implement the dialog properties is a matter of taste. Because I want to use data binding as part of the dialog implementation, I built a little class (Example 10-10) to hold the property data and fire change notifications.
public partial class SettingsDialog : System.Windows.Window { // Data for the dialog that supports notification for data binding class DialogData : INotifyPropertyChanged { Color reportColor; public Color ReportColor { get { return reportColor; } set { reportColor = value; Notify("ReportColor"); } } string reportFolder; public string ReportFolder { get { return reportFolder; } set { reportFolder = value; Notify("ReportFolder"); } } // INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; void Notify(string prop) { if( PropertyChanged != null ) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); } } } DialogData data = new DialogData( ); public Color ReportColor { get { return data.ReportColor; } set { data.ReportColor = value; } } public string ReportFolder { get { return data.ReportFolder; } set { data.ReportFolder = value; } } public SettingsDialog( ) { InitializeComponent( ); // Allow binding to the data to keep UI bindings up-to-date DataContext = data; reportColorButton.Click += reportColorButton_Click; folderBrowseButton.Click += folderBrowseButton_Click; ... } void reportColorButton_Click(object sender, RoutedEventArgs e) { // Set the ReportColor property, triggering a change notification // and updating the dialog UI ... } void folderBrowseButton_Click(object sender, RoutedEventArgs e) { // set the ReportFolder property, triggering a change notification // and updating the dialog UI ... } ... }
The DialogData
class in
Example 10-10 is private to the
dialog class and only serves as storage for the data that allows
binding, which we enable by setting the DataContext
to an instance of the class in
the dialog’s constructor. The dialog properties expose the data with
properties that merely redirect to the instance of the DialogData
class. When the data changes
during the operation of the dialog (like when the user browses to a
new folder or changes the color in a subdialog), setting the
ReportColor
and ReportFolder
properties triggers a change
notification, updating the dialog UI, which you’ll recall is data
bound as shown in Example 10-11.
<Window ...> ... <Grid> <Grid.Resources> <SolidColorBrush x:Key="reportBrush" Color="{Binding ReportColor}" /> </Grid.Resources> ... <TextBox ...Text="{Binding ReportFolder}"
/> ... <Button ...Background="{StaticResource reportBrush}"
>Report _Color...</Button> ... </Grid> </Window>
In Example 10-11,
the report folder text box is binding to the ReportFolder
property of the DialogData
object. The button’s background
brush is binding to the brush (using the StaticResource
markup extension described
in Chapter 12) constructed via a binding to the
ReportColor
property (also of the
DialogData
object).
Figure 10-6 shows our settings dialog in various stages of data change.
Now that we know how to get the dialog running the way we want
it to, we need to let the calling code know whether to process the
data exposed after the dialog is closed. You can always close a
dialog with the Window
object’s
Close
method, shown in Example 10-12.
class SettingsDialog : Window { ... void cancelButton_Click(object sender, RoutedEventArgs e) { // The result from ShowDialog will be false Close( ); } }
By default, when calling the Close
method, or if the user clicks the
Close button or presses Alt-F4, the result from ShowDialog
will be false, indicating
“cancel.” If you’d like the return from ShowDialog
to be true, to indicate “OK,”
you need to set the dialog’s DialogResult
property, as shown in Example 10-13.
class SettingsDialog : Window { ... void okButton_Click(object sender, RoutedEventArgs e) { // The result from ShowDialog will be true DialogResult = true; Close( ); } }
The DialogResult
property
is public, so it’s available to users of your custom dialogs. The
vast majority of the time, the dialog’s DialogResult
property will be the same as
the return from ShowDialog
. To
understand the corner case, let’s look at Example 10-14, which shows
the definitions of ShowDialog
and
DialogResult
.
namespace System.Windows { public class Window : ... { ... publicbool?
ShowDialog( ); publicbool?
DialogResult { get; set; } ... }
If you’re not familiar with the ? syntax, it designates the
Boolean type of ShowDialog
and
DialogResult
to be nullable (i.e., one of the legal values
is null). However, even though both ShowDialog
and DialogResult
are of type bool?
, ShowDialog
will always return true or
false.[69] Likewise, DialogResult
will always be true or false
after the dialog has been closed. Only after a dialog has been shown
but before it’s been closed is DialogResult
null. This is useful when
you’re dealing with a modeless dialog while the dialog itself is
still showing.
You’ll notice that ShowDialog
doesn’t return an enum with
OK
, Cancel
, Yes
, No
, and so on, like Windows Forms does.
ShowDialog
indicates only
whether the user OK’d the operation of the dialog in some way—what
way that was is up to the implementer of the dialog to
communicate.
Because DialogResult
is
null while the dialog is shown, WPF checks the DialogResult
property after each Window
event so that when it transitions
to something non-null, the dialog will be closed (see Example 10-15).
void okButton_Click(object sender, RoutedEventArgs e) { // The return from ShowDialog will be true DialogResult = true; // No need to explicitly call the Close method // when DialogResult transitions to non-null //Close( ); }
As a further shortcut, you can set the IsCancel
property on the Cancel button to
true, causing the Cancel button to automatically close the dialog
without handling the Click
event,
as Example 10-16
illustrates.
<!-- no need to handle the Click event to close dialog -->
<Button Name="cancelButton" IsCancel="True"
>Cancel</Button>
In addition to closing the dialog, setting IsCancel
to true enables the Esc key as a
shortcut to closing the dialog (and setting the DialogResult
to false). However, whereas
setting IsCancel
is enough to
cause the dialog to close when the Cancel button is clicked, the
corresponding setting on the OK button, IsDefault
, isn’t enough to do the same.
Transitioning the DialogResult
to
true, causing the dialog to close, must be handled manually, as
shown in Example 10-17.
...
<Button Name="okButton" IsDefault="True"
...>OK</Button>
...
void okButton_Click(object sender, RoutedEventArgs e) {
// Need this to reflect "OK" back to dialog owner
DialogResult = true;
}
Setting IsDefault
provides
a visual indication of the default button and enables the Enter key
as a shortcut to the OK button (assuming the control with focus
doesn’t use the Enter key itself).
Just because the user clicks the OK button doesn’t mean that everything’s OK: the data the user entered generally needs validation. You’ll recall from Chapter 6 that WPF provides per-control validation as part of the binding engine. Dialogs, an example of which is shown in Example 10-18, are an excellent place to apply this technique.
... lt;!-- 1st row: report folder setting --> <Label ...>Report _Folder</Label> <TextBox ... Name="reportFolderTextBox" ToolTip=" {Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"> <TextBox.Text> <Binding Path="ReportFolder"> <Binding.ValidationRules> <local:FolderMustExist /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ... <!-- 3rd row: reporter setting --> <Label ...>_Reporter</Label> <TextBox ... Name="reporterTextBox" ToolTip=" {Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"> <TextBox.Text> <Binding Path="Reporter"> <Binding.ValidationRules> <local:NonZeroLength /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ...
In Example 10-18, I’ve added a validation rule to the report folder field that requires it to exist on disk. I’ve also added another field, this time to keep track of who’s reporting the reports. The validation rule for this field is a class that makes sure something is entered in this field. The validation rule implementations, some of which are shown in Example 10-19, should not surprise you.
public class FolderMustExist : ValidationRule { public override ValidationResult Validate(object value, ...) { if (!Directory.Exists((string)value)) { return new ValidationResult(false, "Folder doesn't exist"); } return new ValidationResult(true, null); } } public class NonZeroLength : ValidationRule { public override ValidationResult Validate(object value, ...) { if (string.IsNullOrEmpty((string)value)) { return new ValidationResult(false, "Please enter something"); } return new ValidationResult(true, null); } }
With these rules in place, as the user makes changes to the fields, the validation rules are fired and the controls are highlighted and tool-tipped with error indications, as shown in Figure 10-7.
However, if we don’t make any changes or if we skip some
fields before clicking the OK button, the user will have no way of
knowing that some of the fields are invalid. Even worse, in the OK
button handler, the Window
class
provides no facilities for manually checking all of the bindings to
see whether there is any invalid data on the dialog. I assume a
future version of WPF will provide this functionality, but in the
meantime, I’ve built a little method called ValidateBindings
that provides a first cut
at this functionality, which you can use in your own custom dialogs
in the OK button handler, as shown in Example 10-20.
// This is here 'til future versions of WPF provide this functionality public static bool ValidateBindings(DependencyObject parent) { // Validate all the bindings on the parent bool valid = true; LocalValueEnumerator localValues = parent.GetLocalValueEnumerator( ); while( localValues.MoveNext( ) ) { LocalValueEntry entry = localValues.Current; if( BindingOperations.IsDataBound(parent, entry.Property) ) { Binding binding = BindingOperations.GetBinding(parent, entry.Property); foreach( ValidationRule rule in binding.ValidationRules ) { ValidationResult result = rule.Validate(parent.GetValue(entry.Property), null); if( !result.IsValid ) { BindingExpression expression = BindingOperations.GetBindingExpression(parent, entry.Property); Validation.MarkInvalid(expression, new ValidationError(rule, expression, result.ErrorContent, null)); valid = false; } } } } // Validate all the bindings on the children for( int i = 0; i != VisualTreeHelper.GetChildrenCount(parent); ++i ) { DependencyObject child = VisualTreeHelper.GetChild(parent, i); if( !ValidateBindings(child) ) { valid = false; } } return valid; } void okButton_Click(object sender, RoutedEventArgs e) { // Validate all controls if (ValidateBindings(this)) { DialogResult = true; } }
With our validation helper method in place, when the user clicks the OK button, she gets a notification of all of the fields in error, not just the ones she’s changed or the ones she’s given focus to, as shown in Figure 10-8.
We’ve been talking about modal dialogs so far, mostly because the similarities between a modal and a modeless dialog outweigh the differences. We still create an instance of a dialog class with style choices that make it look like a dialog. We still pass the data into and out of the dialog with properties. We still validate the data before notifying anyone that it’s available. The only real differences between a modal and modeless dialog is that with a modeless dialog, we need some slightly different UI choices (generally “Apply” and “Close” instead of “OK” and “Cancel”), and we need to fire an event when the Apply button is clicked so that interested parties can pull out validated data.
Updating our settings dialog to operate modelessly starts with new buttons, as shown in Example 10-21.
<StackPanel ...> <Button Name="applyButton" IsDefault="True" ...>Apply</Button> <Button Name="closeButton" IsCancel="True" ...>Close</Button> </StackPanel>
Handling the button clicks is slightly different, too, as you can see in Example 10-22.
public partial class SettingsDialog : System.Windows.Window { ... public SettingsDialog( ) { ... applyButton.Click += applyButton_Click; closeButton.Click += closeButton_Click; } // Fired when the Apply button is clicked public event EventHandler Apply; void applyButton_Click(object sender, RoutedEventArgs e) { // Validate all controls if (ValidateBindings(this)) { // Let modeless clients know if (Apply != null) { Apply(this, EventArgs.Empty); } // Don't close the dialog 'til Close is clicked // DialogResult = true; } } void closeButton_Click(object sender, RoutedEventArgs e) { // The IsCancel button doesn't close automatically // in the modeless case Close( ); } }
In Example 10-22,
we use the ValidateBindings
helper method, and if everything is valid, we fire the Apply
event to let the owner know, keeping
the dialog open until the Close button is clicked. In the handler
for the Close button, there’s no more automatic closing of the
dialog in the modeless case, so we close it ourselves.
The owner of the dialog changes a bit as well, as Example 10-23 illustrates.
public partial class Window1 : System.Windows.Window { ... Color reportColor; string reportFolder; string reporter; void settingsButton_Click(object sender, RoutedEventArgs e) { // Initialize the dialog SettingsDialog dlg = new SettingsDialog( ); dlg.Owner = this; dlg.ReportColor = reportColor; dlg.ReportFolder = reportFolder; dlg.Reporter = reporter; // Listen for the Apply button and show the dialog modelessly dlg.Apply += dlg_Apply; dlg.Show( ); } void dlg_Apply(object sender, EventArgs e) { // Pull the dialog out of the event args and apply the new settings SettingsDialog dlg = (SettingsDialog)sender; reportColor = dlg.ReportColor; reportFolder = dlg.ReportFolder; reporter = dlg.Reporter; // Do something with the dialog properties } }
In Example 10-23,
when the user asks to see the settings dialog, it’s initialized as
before, but because it’s shown modelessly, we need to know when the
user clicks Apply (and the data has been validated), so we subscribe
to the Apply
event. When that’s
fired, we pull the SettingsDialog
object back out of the sender argument to the Apply
event and pull out our settings as
before. The only other thing you might want to do is to set a flag
that a settings dialog is being shown so that you don’t show more
than one of them.
The base class for top-level window functionality is Window
, providing the features and flexibility
you need to fit in with the rest of the applications on your desktop. In
addition, the Window
class provides
for dialog-style user interactions, both modal and modeless. Finally,
the NavigationWindow
class, which
derives from the Window
class, forms
the core of most standalone navigation-based applications, which we’ll
cover in the next chapter.
[60] * For example, if you set the size of a WPF window to 400 × 400 in 120dpi mode, according to Spy++, the size of that window will be 500 × 500 as far as the operating system is concerned. However, at 96dpi, 400 × 400 is 400 × 400 for both WPF and the OS.
[62] * For a multiple-monitor-safe version of this sample, check out the “Save Window Placement State Sample” in the SDK: http://msdn2.microsoft.com/en-gb/library/aa972163.aspx (http://www.tinysells.com/103).
[63] * Unlike the common dialogs
provided by Windows Forms, the WPF ShowDialog
returns a nullable Boolean
value, not an enumerated value. We’ll talk more about that
later.
[64] † Of course, now you’re paying the cost of loading an additional assembly into your process, but that beats reimplementing the Windows Forms common dialogs in many scenarios.
[65] ‡ If you’d like to take advantage of new Vista features in your common dialog usage, you can do so with samples provided in the SDK in the topic titled “Vista Bridge Samples,” available at http://msdn2.microsoft.com/en-us/library/ms756482.aspx (http://tinysells.com/95).
[66] * The FromArgb
method and the A
, R
, G
, and B
properties on both Color
classes is the key to making the
conversion between the GDI+ and WPF Color
classes work.
[67] † You can download it from http://blogs.msdn.com/wpfsdk/archive/2006/10/26/uncommon-dialogs--font-chooser-and-color-picker-dialogs.aspx (http://tinysells.com/89).
[68] ‡ You can search for the “Font Dialog Box Demo” topic in your offline SDK or download it from http://msdn2.microsoft.com/en-gb/library/ms771765.aspx (http://tinysells.com/90).
[69] * I wish the return type from
ShowDialog
was just a plain
bool to indicate that it can return only true or false and never
null.