Chapter 10. Windows and Dialogs

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.

Window

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.

Window Look and Feel

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).

Window styles in Windows Vista and Windows XP
Figure 10-1. Window styles in Windows Vista and Windows XP

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.”

Window Lifetime

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).

Example 10-1. Calling InitializeComponent
<!-- 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).

Example 10-2. Showing a window modelessly and modally
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.

Window lifetime
Figure 10-2. Window lifetime

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.

Window Location and Size

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.

Window resize modes
Figure 10-3. Window resize modes

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.

Window Owners

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.

Tip

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.

Window Visibility and State

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.

Example 10-3. Saving and restoring window state
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.

Dialogs

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.

Common Dialogs

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.

Example 10-4. Using the OpenFileDialog common dialog
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.

Example 10-5. Showing the FolderBrowserDialog common Windows Forms dialog
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.

Available common dialogs

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]

Tip

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.

Custom Dialogs

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.

Example 10-6. A simple custom settings dialog
<!-- 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.

Using standard WPF techniques on a dialog
Figure 10-4. Using standard WPF techniques on a dialog

To take advantage of our custom dialog, we have but to create an instance and show it modally, as in Example 10-7.

Example 10-7. Showing a custom dialog
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.”

Dialog look and feel

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.

Example 10-8. Setting dialog-related Window properties
<!-- 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.

A good-looking dialog in WPF
Figure 10-5. A good-looking dialog in WPF

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.

Dialog data exchange

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:

  1. Create and initialize the dialog with initial values.

  2. Show the dialog, letting the user choose new, validated values.

  3. 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.

Example 10-9. Exchanging data with a modal dialog
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.

Example 10-10. Managing custom dialog data
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.

Example 10-11. Binding property data in a custom dialog’s GUI
<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.

The custom settings dialog in action ()
Figure 10-6. The custom settings dialog in action (Figure F-18)

Handling OK and Cancel

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.

Example 10-12. Closing a dialog manually
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.

Example 10-13. Changing the return value of ShowDialog
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.

Example 10-14. The nullable results of showing a dialog
namespace System.Windows {
  public class Window : ... {
  ...
    public bool? ShowDialog( );
    public bool? 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.

Tip

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).

Example 10-15. Closing a modal dialog automatically by changing DialogResult
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.

Example 10-16. Cancel buttons transition DialogResult automatically
<!-- 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.

Example 10-17. Default buttons still need to transition DialogResult manually
...
<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).

Data validation

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.

Example 10-18. Data validation and dialogs
...
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.

Example 10-19. Some example validation rules
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.

An error in a dialog validation rule ()
Figure 10-7. An error in a dialog validation rule (Figure F-19)

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.

Example 10-20. Validate all controls in the OK button
// 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.

Validating all controls in the OK button handler ()
Figure 10-8. Validating all controls in the OK button handler (Figure F-20)

Modeless dialogs

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.

Example 10-21. Apply and Close buttons for a modeless dialog
<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.

Example 10-22. Handling the Apply and Close buttons in a modeless dialog
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.

Example 10-23. Handling the Apply event on a custom modeless dialog
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.

Where Are We?

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.

[61] * Chapter 2 describes user settings.

[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.

[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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset