WPF 4.5 Application and Windows

0
129
14 min read

(For more resources on this topic, see here.)

Creating a window

Windows are the typical top level controls in WPF. By default, a MainWindow class is created by the application wizard and automatically shown upon running the application. In this recipe, we’ll take a look at creating and showing other windows that may be required during the lifetime of an application.

Getting ready

Make sure Visual Studio is up and running.

How to do it…

We’ll create a new class derived from Window and show it when a button is clicked:

  1. Create a new WPF application named CH05.NewWindows.
  2. Right-click on the project node in Solution explorer, and select Add | Window…:
  3. In the resulting dialog, type OtherWindow in the Name textbox and click on Add.
  4. A file named OtherWindow.xaml should open in the editor. Add a TextBlock to the existing Grid, as follows:

    <TextBlock Text="This is the other window" FontSize="20"
       VerticalAlignment="Center" HorizontalAlignment="Center" />

  5. Open MainWindow.xaml. Add a Button to the Grid with a Click event handler:

    <Button Content="Open Other Window" FontSize="30"
            Click="OnOpenOtherWindow" />

  6. In the Click event handler, add the following code:

    void OnOpenOtherWindow(object sender, RoutedEventArgs e) {
       var other = new OtherWindow();
       other.Show();
    }

  7. Run the application, and click the button. The other window should appear and live happily alongside the main window:
  8. How it works…

    A Window is technically a ContentControl, so can contain anything. It’s made visible using the Show method. This keeps the window open as long as it’s not explicitly closed using the classic close button, or by calling the Close method. The Show method opens the window as modeless—meaning the user can return to the previous window without restriction. We can click the button more than once, and consequently more Window instances would show up.

    There’s more…

    The first window shown can be configured using the Application.StartupUri property, typically set in App.xaml. It can be changed to any other window. For example, to show the OtherWindow from the previous section as the first window, open App.xaml and change the StartupUri property to OtherWindow.xaml:

    StartupUri="OtherWindow.xaml"
    

    Selecting the startup window dynamically

    Sometimes the first window is not known in advance, perhaps depending on some state or setting. In this case, the StartupUri property is not helpful. We can safely delete it, and provide the initial window (or even windows) by overriding the Application.OnStartup method as follows (you’ll need to add a reference to the System.Configuration assembly for the following to compile):

    protected override void OnStartup(StartupEventArgs e) {
       Window mainWindow = null;
       // check some state or setting as appropriate
             if(ConfigurationManager.AppSettings["AdvancedMode"] == "1")
          mainWindow = new OtherWindow();
       else
          mainWindow = new MainWindow();
       mainWindow.Show();
    }

    This allows complete flexibility in determining what window or windows should appear at application startup.

    Accessing command line arguments

    The WPF application created by the New Project wizard does not expose the ubiquitous Main method. WPF provides this for us – it instantiates the Application object and eventually loads the main window pointed to by the StartupUri property.

    The Main method, however, is not just a starting point for managed code, but also provides an array of strings as the command line arguments passed to the executable (if any). As Main is now beyond our control, how do we get the command line arguments?

    Fortunately, the same OnStartup method provides a StartupEventArgs object, in which the Args property is mirrored from Main. The downloadable source for this chapter contains the project CH05.CommandLineArgs, which shows an example of its usage. Here’s the OnStartup override:

    protected override void OnStartup(StartupEventArgs e) {
       string text = "Hello, default!";
       if(e.Args.Length > 0)
          text = e.Args[0];
     
       var win = new MainWindow(text);
       win.Show();
    }

    The MainWindow instance constructor has been modified to accept a string that is later used by the window. If a command line argument is supplied, it is used.

Creating a dialog box

A dialog box is a Window that is typically used to get some data from the user, before some operation can proceed. This is sometimes referred to as a modal window (as opposed to modeless, or non-modal). In this recipe, we’ll take a look at how to create and manage such a dialog box.

Getting ready

Make sure Visual Studio is up and running.

How to do it…

We’ll create a dialog box that’s invoked from the main window to request some information from the user:

  1. Create a new WPF application named CH05.Dialogs.
  2. Add a new Window named DetailsDialog.xaml (a DetailsDialog class is created).
  3. Visual Studio opens DetailsDialog.xaml. Set some Window properties: FontSize to 16, ResizeMode to NoResize, SizeToContent to Height, and make sure the Width is set to 300: ResizeMode=”NoResize” SizeToContent=”Height” Width=”300″ FontSize=”16″
  4. Add four rows and two columns to the existing Grid, and add some controls for a simple data entry dialog as follows:

    <Grid.RowDefinitions>
      <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition />
    </Grid.ColumnDefinitions>
    <TextBlock Text="Please enter details:" Grid.ColumnSpan="2"
      Margin="4,4,4,20" HorizontalAlignment="Center"/>
    <TextBlock Text="Name:" Grid.Row="1" Margin="4"/>
    <TextBox Grid.Column="1" Grid.Row="1" Margin="4"
             x_Name="_name"/>
    <TextBlock Text="City:" Grid.Row="2" Margin="4"/>
    <TextBox Grid.Column="1" Grid.Row="2" Margin="4"
             x_Name="_city"/>
    <StackPanel Grid.Row="3" Orientation="Horizontal"
                Margin="4,20,4,4" Grid.ColumnSpan="2"
                HorizontalAlignment="Center"> <Button Content="OK" Margin="4"  />
        <Button Content="Cancel" Margin="4" />
    </StackPanel>

  5. This is how it should look in the designer:

  6. The dialog should expose two properties for the name and city the user has typed in. Open DetailsDialog.xaml.cs. Add two simple properties:

    public string FullName { get; private set; }
    public string City { get; private set; }

  7. We need to show the dialog from somewhere in the main window. Open MainWindow.xaml, and add the following markup to the existing Grid:
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Button Content="Enter Data" Click="OnEnterData"
            Margin="4" FontSize="16"/>
    <TextBlock FontSize="24" x_Name="_text" Grid.Row="1"
        VerticalAlignment="Center" HorizontalAlignment="Center"/>
  8. In the OnEnterData handler, add the following:

    private void OnEnterData(object sender, RoutedEventArgs e) {
       var dlg = new DetailsDialog();
       if(dlg.ShowDialog() == true) {
          _text.Text = string.Format(
              "Hi, {0}! I see you live in {1}.",   
              dlg.FullName, dlg.City);
       }
    }

  9. Run the application. Click the button and watch the dialog appear. The buttons don’t work yet, so your only choice is to close the dialog using the regular close button. Clearly, the return value from ShowDialog is not true in this case.
  10. When the OK button is clicked, the properties should be set accordingly. Add a Click event handler to the OK button, with the following code:

    private void OnOK(object sender, RoutedEventArgs e) {
       FullName = _name.Text;
       City = _city.Text;
       DialogResult = true;
       Close();
    }

    The Close method dismisses the dialog, returning control to the caller. The DialogResult property indicates the returned value from the call to ShowDialog when the dialog is closed.

  11. Add a Click event handler for the Cancel button with the following code:

    private void OnCancel(object sender, RoutedEventArgs e) {
       DialogResult = false;
       Close();
    }

  12. Run the application and click the button. Enter some data and click on OK:

  13. You will see the following window:

  14. How it works…

    A dialog box in WPF is nothing more than a regular window shown using ShowDialog instead of Show. This forces the user to dismiss the window before she can return to the invoking window. ShowDialog returns a Nullable (can be written as bool? in C#), meaning it can have three values: true, false, and null. The meaning of the return value is mostly up to the application, but typically true indicates the user dismissed the dialog with the intention of making something happen (usually, by clicking some OK or other confirmation button), and false means the user changed her mind, and would like to abort. The null value can be used as a third indicator to some other application-defined condition.

    The DialogResult property indicates the value returned from ShowDialog because there is no other way to convey the return value from the dialog invocation directly. That’s why the OK button handler sets it to true and the Cancel button handler sets it to false (this also happens when the regular close button is clicked, or Alt + F4 is pressed).

    Most dialog boxes are not resizable. This is indicated with the ResizeMode property of the Window set to NoResize. However, because of WPF’s flexible layout, it certainly is relatively easy to keep a dialog resizable (and still manageable) where it makes sense (such as when entering a potentially large amount of text in a TextBox – it would make sense if the TextBox could grow if the dialog is enlarged).

    There’s more…

    Most dialogs can be dismissed by pressing Enter (indicating the data should be used) or pressing Esc (indicating no action should take place). This is possible to do by setting the OK button’s IsDefault property to true and the Cancel button’s IsCancel property to true. The default button is typically drawn with a heavier border to indicate it’s the default button, although this eventually depends on the button’s control template.

    If these settings are specified, the handler for the Cancel button is not needed. Clicking Cancel or pressing Esc automatically closes the dialog (and sets DiaglogResult to false). The OK button handler is still needed as usual, but it may be invoked by pressing Enter, no matter what control has the keyboard focus within the Window. The CH05.DefaultButtons project from the downloadable source for this chapter demonstrates this in action.

    Modeless dialogs

    A dialog can be show as modeless, meaning it does not force the user to dismiss it before returning to other windows in the application. This is done with the usual Show method call – just like any Window. The term dialog in this case usually denotes some information expected from the user that affects other windows, sometimes with the help of another button labelled “Apply”.

    The problem here is mostly logical—how to convey the information change. The best way would be using data binding, rather than manually modifying various objects. We’ll take an extensive look at data binding in the next chapter.

Using the common dialog boxes

Windows has its own built-in dialog boxes for common operations, such as opening files, saving a file, and printing. Using these dialogs is very intuitive from the user’s perspective, because she has probably used those dialogs before in other applications. WPF wraps some of these (native) dialogs. In this recipe, we’ll see how to use some of the common dialogs.

Getting ready

Make sure Visual Studio is up and running.

How to do it…

We’ll create a simple image viewer that uses the Open common dialog box to allow the user to select an image file to view:

  1. Create a new WPF Application named CH05.CommonDialogs.
  2. Open MainWindow.xaml. Add the following markup to the existing Grid:
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>
    <Button Content="Open Image" FontSize="20" Click="OnOpenImage"
            HorizontalAlignment="Center" Margin="4" />
    <Image Grid.Row="1" x_Name="_img" Stretch="Uniform" />
  3. Add a Click event handler for the button. In the handler, we’ll first create an OpenFileDialog instance and initialize it (add a using to the Microsoft.Win32 namespace):

    void OnOpenImage(object sender, RoutedEventArgs e) {
       var dlg = new OpenFileDialog {
          Filter = "Image files|*.png;*.jpg;*.gif;*.bmp",
          Title = "Select image to open",
          InitialDirectory = Environment.GetFolderPath(
            Environment.SpecialFolder.MyPictures)
       };

  4. Now we need to show the dialog and use the selected file (if any):

    if(dlg.ShowDialog() == true) {
       try {
          var bmp = new BitmapImage(new Uri(dlg.FileName));
          _img.Source = bmp;
       }
       catch(Exception ex) {
          MessageBox.Show(ex.Message, "Open Image");
       }
    }

  5. Run the application. Click the button and navigate to an image file and select it. You should see something like the following:

How it works…

The OpenFileDialog class wraps the Win32 open/save file dialog, providing easy enough access to its capabilities. It’s just a matter of instantiating the object, setting some properties, such as the file types (Filter property) and then calling ShowDialog. This call, in turn, returns true if the user selected a file and false otherwise (null is never returned, although the return type is still defined as Nullable for consistency).

The look of the Open file dialog box may be different in various Windows versions. This is mostly unimportant unless some automated UI testing is done. In this case, the way the dialog looks or operates may have to be taken into consideration when creating the tests.

The filename itself is returned in the FileName property (full path). Multiple selections are possible by setting the MultiSelect property to true (in this case the FileNames property returns the selected files).

There’s more…

WPF similarly wraps the Save As common dialog with the SaveFileDialog class (in the Microsoft.Win32 namespace as well). Its use is very similar to OpenFileDialog (in fact, both inherit from the abstract FileDialog class).

What about folder selection (instead of files)? The WPF OpenFileDialog does not support that. One solution is to use Windows Forms’ FolderBrowseDialog class. Another good solution is to use the Windows API Code Pack described shortly.

Another common dialog box WPF wraps is PrintDialog (in System.Windows.Controls). This shows the familiar print dialog, with options to select a printer, orientation, and so on. The most straightforward way to print would be calling PrintVisual (after calling ShowDialog), providing anything that derives from the Visual abstract class (which include all elements). General printing is a complex topic and is beyond the scope of this book.

What about colors and fonts?

Windows also provides common dialogs for selecting colors and fonts. However, these are not wrapped by WPF. There are several alternatives:

  • Use the equivalent Windows Forms classes (FontDialog and ColorDialog, both from System.Windows.Forms)
  • Wrap the native dialogs yourself
  • Look for alternatives on the Web

The first option is possible, but has two drawbacks: first, it requires adding reference to the System.Windows.Forms assembly; this adds a dependency at compile time, and increases memory consumption at run time, for very little gain. The second drawback has to do with the natural mismatch between Windows Forms and WPF. For example, ColorDialog returns a color as a System.Drawing.Color, but WPF uses System.Windows.Media.Color. This requires mapping a GDI+ color (WinForms) to WPF’s color, which is cumbersome at best.

The second option of doing your own wrapping is a non-trivial undertaking and requires good interop knowledge. The other downside is that the default color and font common dialogs are pretty old (especially the color dialog), so there’s much room for improvement.

The third option is probably the best one. There are more than a few good candidates for color and font pickers. For a color dialog, for example, you can use the ColorPicker or ColorCanvas provided with the Extended WPF toolkit library on CodePlex (http://wpftoolkit.codeplex.com/). Here’s how these may look (ColorCanvas on the left-hand side, and one of the possible views of ColorPicker on the right-hand side):

The Windows API Code Pack

The Windows API Code Pack is a Microsoft project on CodePlex (http://archive.msdn.microsoft.com/WindowsAPICodePack) that provides many .NET wrappers to native Windows features, in various areas, such as shell, networking, Windows 7 features (this is less important now as WPF 4 added first class support for Windows 7), power management, and DirectX. One of the Shell features in the library is a wrapper for the Open dialog box that allows selecting a folder instead of a file. This has no dependency on the WinForms assembly.

LEAVE A REPLY

Please enter your comment!
Please enter your name here