Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

1012 CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

Window mainWindow = new Window(); mainWindow.Title = "My First WPF App!"; mainWindow.Height = 200; mainWindow.Width = 300;

mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen; mainWindow.Show();

}

}

}

Note The Main() method of a WPF application must be attributed with the [STAThread] attribute, which ensures any legacy COM objects used by your application are thread-safe. If you do not annotate Main() in this way, you will trigger a runtime exception!

Note that the MyWPFApp class extends the System.Windows.Application type. Within this type’s Main() method, we create an instance of our application object and handle the Startup and Exit events using method group conversion syntax. Recall from Chapter 11 that this shorthand notation removes the need to manually specify the underlying delegates used by a particular event.

However, if you wish, you can specify the underlying delegates directly by name. In the following modified Main() method, notice that the Startup event works in conjunction with the StartupEventHandler delegate, which can only point to methods taking an Object as the first parameter and a StartupEventArgs as the second. The Exit event, on the other hand, works with the ExitEventHandler delegate, which demands that the method pointed to take an ExitEventArgs type as the second parameter:

[STAThread] static void Main()

{

// This time, specify the underlying delegates.

MyWPFApp app = new MyWPFApp();

app.Startup += new StartupEventHandler(AppStartUp); app.Exit += new ExitEventHandler(AppExit); app.Run(); // Fires the Startup event.

}

The AppStartUp() method has been configured to create a Window type, establish some very basic property settings, and call Show() to display the window on the screen in a modal-less fashion (like Windows Forms, the ShowDialog() method can be used to launch a modal dialog). The AppExit() method simply makes use of the WPF MessageBox type to display a diagnostic message when the application is being terminated.

To compile this C# code into an executable WPF application, assume that you have created a C# response file named build.rsp that references each of the WPF assemblies. Note that the path to each assembly should be defined on a single line (see Chapter 2 for more information on response files and working with the command-line compiler):

# build.rsp

#

/target:winexe

/out:SimpleWPFApp.exe

/r:"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll" /r:"C:\Program Files\Reference Assemblies\Microsoft\Framework

\v3.0\PresentationCore.dll"

CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1013

/r:"C:\Program Files\Reference Assemblies\Microsoft\Framework \v3.0\PresentationFramework.dll"

*.cs

You can now compile this WPF program at the command prompt as follows:

csc @build.rsp

Once you run the program, you will find a very simple main window that can be minimized, maximized, and closed. To spice things up a bit, we need to add some user interface elements. Before we do, however, let’s refactor our code base to account for a strongly typed and wellencapsulated Window-derived class.

Extending the Window Class Type

Currently, our Application-derived class directly creates an instance of the Window type upon application startup. Ideally, we would create a class deriving from Window in order to encapsulate its functionality. Assume we have created the following class definition within our current

SimpleWPFApp namespace:

class MainWindow : Window

{

public MainWindow(string windowTitle, int height, int width)

{

this.Title = windowTitle;

this.WindowStartupLocation = WindowStartupLocation.CenterScreen; this.Height = height;

this.Width = width; this.Show();

}

}

We can now update our Startup event handler to simply directly create an instance of

MainWindow:

static void AppStartUp(object sender, StartupEventArgs e)

{

// Create a MainWindow object.

MainWindow wnd = new MainWindow("My better WPF App!", 200, 300);

}

Once the program is recompiled and executed, the output is identical. The obvious benefit is that we now have a strongly typed class to build upon.

Note When you create a Window (or Window-derived) object, it will automatically be added to the internal windows collection of the Application type (via some constructor logic found in the Window class itself). Given this fact, a window will be alive in memory until it is terminated or is explicitly removed from the collection via the

Application.Windows property.

Creating a Simple User Interface

Adding UI elements into a Window-derived type is similar (but not identical) to adding UI elements into a System.Windows.Forms.Form-derived type:

1014 CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1.Define a member variable to represent the required widget.

2.Configure the variable’s look and feel upon the creation of your Window type.

3.Add the widget to the Window’s client area via a call to AddChild().

Although the process might feel familiar to Windows Forms development, one obvious difference is that the UI controls used by WPF are defined within the System.Windows.Controls namespace rather than System.Windows.Forms (thankfully, in many cases, they are identically named and feel quite similar to their Windows Forms counterparts).

A more drastic change from Windows Forms is the fact that a Window-derived type can contain only a single child element (due to the WPF content model). When a window needs to contain multiple UI elements (which will be the case for practically any window), you will need to make use of a layout manager such as DockPanel, Grid, Canvas, or StackPanel to control their positioning.

For this example, we will add single Button type to the Window-derived type. When we click this button, we terminate the application by gaining access to the global application object (via the Application.Current property) in order to call the Shutdown() method. Ponder the following update to the MainWindow class:

class MainWindow : Window

{

// Our UI element.

private Button btnExitApp = new Button();

public MainWindow(string windowTitle, int height, int width)

{

//Configure button and set the child control. btnExitApp.Click += new RoutedEventHandler(btnExitApp_Clicked); btnExitApp.Content = "Exit Application";

btnExitApp.Height = 25; btnExitApp.Width = 100;

//Set the content of this window to a single button. this.AddChild(btnExitApp);

//Configure the window.

this.Title = windowTitle;

this.WindowStartupLocation = WindowStartupLocation.CenterScreen; this.Height = height;

this.Width = width; this.Show();

}

private void btnExitApp_Clicked(object sender, RoutedEventArgs e)

{

// Get a handle to the current application and shut it down.

Application.Current.Shutdown();

}

}

Given your work with Windows Forms in Chapter 27, the code within the window’s constructor should not look too threatening. Do notice, however, that the Click event of the WPF button works in conjunction with a delegate named RoutedEventHandler, which obviously begs the question, what is a routed event? You’ll examine the details of the WPF event model in the next chapter; for the time being, simply understand that targets of the RoutedEventHandler delegate must supply an object as the first parameter and a RoutedEventArgs as the second.

CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1015

In any case, once you recompile and run this application, you will find the customized window shown in Figure 28-4. Notice that our button is automatically placed in the dead center of the window’s client area, which is the default behavior when content is not placed within a WPF panel type.

Figure 28-4. A somewhat interesting WPF application

Source Code The SimpleWPFApp project is included under the Chapter 28 subdirectory.

Additional Details of the Application Type

Now that you have created a simple WPF program using a 100-percent pure code approach, let’s illustrate some additional details of the Application type, beginning with the construction of application-wide data. To do so, we will extend the previous SimpleWPFApp application with new functionality.

Application-wide Data and Processing Command-Line

Arguments

Recall that the Application type defines a property named Properties, which allows you to define a collection of name/value pairs via a type indexer. Because this indexer has been defined to operate on type System.Object, you are able to store any sort of item within this collection (including your custom classes), to be retrieved at a later time using a friendly moniker. Using this approach, it is simple to share data across all windows in a WPF application.

To illustrate, we will update the current Startup event handler to check the incoming commandline arguments for a value named /GODMODE (a common cheat code for many PC video games). If we find such a token, we will establish a bool value set to true within the properties collection of the same name (otherwise we will set the value to false).

Sounds simple enough, but one question you may have is, how are we going to pass the incoming command-line arguments (typically obtained from the Main() method) to our Startup event handler? One approach is to call the static Environment.GetCommandLineArgs() method. However, these same arguments are automatically added to the incoming StartupEventArgs parameter and can be accessed via the Args property. This being said, here is our first update:

static void AppStartUp(object sender, StartupEventArgs e)

{

//Check the incoming command-line arguments and see if they

//specified a flag for /GODMODE.

Application.Current.Properties["GodMode"] = false;

1016 CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

foreach(string arg in e.Args)

{

if (arg.ToLower() == "/godmode")

{

Application.Current.Properties["GodMode"] = true; break;

}

}

// Create a MainWindow object.

MainWindow wnd = new MainWindow("My better WPF App!", 200, 300);

}

Now recall that this new name/value pair can be accessed from anywhere within the WPF application. All we are required to do is obtain an access point to the global application object (via Application.Current) and investigate the collection. For example, we could update the Click event handler of the Button type of the main window like so:

private void btnExitApp_Clicked(object sender, RoutedEventArgs e)

{

//Did user enable /godmode? if((bool)Application.Current.Properties["GodMode"])

MessageBox.Show("Cheater!");

//Get a handle to the current application and shut it down. Application.Current.Shutdown();

}

With this, if the end user launches our program as follows:

SimpleWPFApp.exe /godmode

he or she will see our shameful message box displayed when terminating the application.

Iterating over the Application’s Windows Collection

Another interesting property exposed by Application is Windows, which provides access to a collection representing each window loaded into memory for the current WPF application. Recall that as you create new Window types, they are automatically added into the global application object’s Windows collection. We have no need to update our current example to illustrate this; however, here is an example method that will minimize each window of the application (perhaps in response to a given keyboard gesture or menu option triggered by the end user):

static void MinimizeAllWindows()

{

foreach (Window wnd in Application.Current.Windows)

{

wnd.WindowState = WindowState.Minimized;

}

}

Additional Events of the Application Type

Like many types within the .NET base class libraries, Application also defines a set of events that you can intercept. You have already seen the Startup and Exit events in action. You should also be aware of Activated and Deactivated. At first glance these events can seem a bit confusing, given

CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1017

that the Window type supplied identically named methods. Unlike their UI counterparts, however, the Activated and Deactivated events fire whenever any window maintained by the application object received or lost focus (in contrast to the same events of the Window type, which are unique to that Window object).

Our current example has no need to handle these two events, but if you need to do so, be aware that each event works in conjunction with the System.EventHandler delegate, and therefore the event handler will take an Object as the first parameter and System.EventArgs as the second (see Chapter 27 for a refresher on the EventHandler delegate).

Note A majority of the remaining events of the Application type are specific to a navigation-based WPF application. Using these events, you are able to intercept the process of moving between Page objects of your program.

Additional Details of the Window Type

The Window type, as you saw earlier in this chapter, gains a ton of functionality from its set of parent classes and implemented interfaces. Over the chapters to come, you’ll glean more and more information about what these base classes bring to the table; however, it is important to revisit the Window type itself and come to understand some core services you’ll need to use on a day-to-day basis, beginning with the set of events that are fired over its lifetime.

The Lifetime of a Window Object

Like the System.Windows.Forms.Form type, System.Windows.Window has a set of events that will fire over the course of its lifetime. When you handle some (or all) of these events, you will have a convenient manner in which you can perform custom logic as your Window goes about its business. First of all, because a window is a class type, the very first step in its initialization entails a call to a specified constructor. After that point, the first WPF-centric event that fires is SourceInitialized, which is only useful if your Window is making use of various interoperability services (e.g., using legacy ActiveX controls in a WPF application). Even then, the need to intercept this event is limited, so consult the .NET Framework 3.5 documentation if you require more information.

The first immediately useful event that fires after the Window’s constructor is Activate, which works in conjunction with the System.EventHandler delegate. This event is fired when a window receives focus and thus becomes the foreground window. The counterpart to this event is Deactivate (which also works with the System.EventHandler delegate), which fires when a window loses focus.

Here is an update to our existing Window-derived type that adds an informative message to a private string variable (you’ll see the usefulness of this string variable in just a bit) when the

Activate and Deactivate events occur:

class MainWindow : Window

{

private Button btnExitApp = new Button();

// This string will document which events fire, and when. private string lifeTimeData = String.Empty;

protected void MainWindow_Activated(object sender, EventArgs e)

{

1018 CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

lifeTimeData += "Activate Event Fired!\n";

}

protected void MainWindow_Deactivated(object sender, EventArgs e)

{

lifeTimeData += "Deactivated Event Fired!\n";

}

public MainWindow(string windowTitle, int height, int width)

{

// Rig up events.

this.Activated += MainWindow_Activated; this.Deactivated += MainWindow_Deactivated;

...

}

}

Note Recall that you can handle application-level activation/deactivation for all windows with the

Application.Activated and Application.Deactivated events.

Once the Activated event fires, the next event to do so is Loaded (which works with the RoutedEventHandler delegate), which signifies that the window has been for all practical purposes fully laid out and rendered, and is ready to respond to user input.

Note While the Activated event can fire many times as a window gains or loses focus, the Loaded event will fire only one time during the window’s lifetime.

Assume that our MainWindow type has handled this event and defines the following event handler:

protected void MainWindow_Loaded(object sender, RoutedEventArgs e)

{

lifeTimeData += "Loaded Event Fired!\n";

}

Note Should the need arise (which can be the case with custom WPF controls), you can capture the exact moment when a window’s content has been loaded by handling the ContentRendered event.

Handling the Closing of a Window Object

End users can shut down a window using numerous built-in system-level techniques (e.g., clicking the “X” close button on the window’s frame) or by indirectly calling the Close() method in response to some user interaction element (e.g., File Exit). In either case, WPF provides two events that you can intercept to determine if the user is truly ready to shut down the window and remove it from memory. The first event to fire is Closing, which works in conjunction with the CancelEventHandler delegate.

CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1019

This delegate expects target methods to take System.ComponentModel.CancelEventArgs as the second parameter. CancelEventArgs provides the Cancel property, which when set to false will prevent the window from actually closing (this is handy when you have asked the user if he really wants to close the window, or perhaps needs to save his work).

If the user did indeed wish to close the window, CancelEventArgs.Cancel can be set to true, which will then cause the Closed event to fire (which works with the System.EventHandler delegate), which is the point at which the window is about to be closed for good. Assuming the MainWindow type has handled these two events, consider the final event handlers:

protected void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)

{

lifeTimeData += "Closing Event Fired!\n";

// See if the user really wants to shut down this window. string msg = "Do you want to close without saving?"; MessageBoxResult result = MessageBox.Show(msg,

"My App", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.No)

{

// If user doesn't want to close, cancel closure. e.Cancel = true;

}

}

protected void MainWindow_Closed(object sender, EventArgs e)

{

lifeTimeData += "Closing Event Fired!\n"; MessageBox.Show(lifeTimeData);

}

When you compile and run your application, shift the window into and out of focus a few times. Also attempt to close the window once or twice. When you do indeed close the window for good, you will see a message box pop up that displays the events that fired during the window’s lifetime (Figure 28-5 shows one possible test run).

Figure 28-5. The life and times of a System.Windows.Window

1020 CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

Handling Window-Level Mouse Events

Much like Windows Forms, the WPF API provides a number of events you can capture in order to interact with the mouse. Specifically, the UIElement base class defines a number of mouse-centric events such as MouseMove, MouseUp, MouseDown, MouseEnter, MouseLeave, and so forth.

Consider, for example, the act of handling the MouseMove event. This event works in conjunction with the System.Windows.Input.MouseEventHandler delegate, which expects its target to take a

System.Windows.Input.MouseEventArgs type as the second parameter. Using MouseEventArgs (like a Windows Forms application) you are able to extract out the (x, y) position of the mouse and other relevant details. Consider the following partial definition:

public class MouseEventArgs : InputEventArgs

{

...

public Point GetPosition(IInputElement relativeTo); public MouseButtonState LeftButton { get; }

public MouseButtonState MiddleButton { get; } public MouseDevice MouseDevice { get; } public MouseButtonState RightButton { get; } public StylusDevice StylusDevice { get; } public MouseButtonState XButton1 { get; } public MouseButtonState XButton2 { get; }

}

The GetPosition() method allows you to get the (x, y) value relative to a UI element on the window. If you are interested in capturing the position relative to the activated window, simply pass in this. Here is an event handler for MouseMove that will display the location of the mouse in the window’s title area (notice we are translating the returned Point type into a string value via

ToString()):

protected void MainWindow_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)

{

// Set the title of the window to the current X,Y of the mouse. this.Title = e.GetPosition(this).ToString();

}

Handling Window-Level Keyboard Events

Processing keyboard input is also very straightforward. UIElement defines a number of events that you can capture to intercept keypresses from the keyboard on the active element (e.g., KeyUp, KeyDown, etc.). The KeyUp and KeyDown events both work with the System.Windows.Input.KeyEventHandler delegate, which expects the target’s second event handler to be of type KeyEventArgs, which defines several public properties of interest:

public class KeyEventArgs : KeyboardEventArgs

{

...

public bool IsDown { get; } public bool IsRepeat { get; } public bool IsToggled { get; } public bool IsUp { get; } public Key Key { get; }

public KeyStates KeyStates { get; } public Key SystemKey { get; }

}

CHAPTER 28 INTRODUCING WINDOWS PRESENTATION FOUNDATION AND XAML

1021

To illustrate handling the KeyUp event, the following event handler will display the previously pressed key on the window’s title:

protected void MainWindow_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)

{

// Display keypress. this.Title = e.Key.ToString();

}

At this point in the chapter, WPF might look like nothing more than a new GUI model that is providing (more or less) the same services as System.Windows.Forms.dll. If this were in fact the case, you might question the usefulness of yet another UI toolkit. To truly see what makes WPF so unique requires an understanding of a new XML-based grammar, XAML.

Source Code The SimpleWPFAppRevisited project is included under the Chapter 28 subdirectory.

Building a (XAML-Centric) WPF Application

Extensible Application Markup Language, or XAML, is an XML-based grammar that allows you to define the state (and, to some extent, the functionality) of a tree of .NET objects through markup. While XAML is frequently used when building UIs with WPF, in reality it can be used to describe any tree of nonabstract .NET types (including your own custom types defined in a custom .NET assembly), provided each supports a default constructor. As you will see, the markup found within a *.xaml file is transformed into a full-blown object model that maps directly to the types within a related .NET namespace.

Because XAML is an XML-based grammar, we gain all the benefits and drawbacks XML affords us. On the plus side, XAML files are very self-describing (as any XML document should be). By and large, each element in an XAML file represents a type name (such as Button, Window, or Application) within a given .NET namespace. Attributes within the scope of an opening element map to properties (Height, Width, etc.) and events (Startup, Click, etc.) of the specified type.

Given the fact that XAML is simply a declarative way to define the state of an object, it is possible to define a WPF widget via markup or procedural code. For example, the following XAML:

<!-- Defining a WPF Button in XAML -->

<Button Name = "btnClickMe" Height = "40" Width = "100" Content = "Click Me" />

can be represented programmatically as follows:

// Defining the same WPF Button in C# code.

Button btnClickMe = new Button(); btnClickMe.Height = 40; btnClickMe.Width = 100; btnClickMe.Content = "Click Me";

On the downside, XAML can be verbose and is (like any XML document) case sensitive, thus complex XAML definitions can result in a good deal of markup. Most developers will not need to manually author a complete XAML description of their WPF applications. Rather, the majority of this task will (thankfully) be relegated to development tools such as Visual Studio 2008, Microsoft Expression Blend, or any number of third-party products. Once the tools generate the basic markup, you can go in and fine-tune the XAML definitions by hand if necessary.