A WPF application is a Windows process in which you have an instance
of the WPF Application
object. The
Application
object provides lifetime
services and integration with ClickOnce deployment. Between application
sessions, you’ll want to be able to keep application and user settings in
a way that integrates well with WPF applications. All of these topics are
the focus of this chapter.
On the other hand, if you’re interested in XML Browser Applications (XBAPs)—applications hosted in the browser and deployed over the Web—read Chapter 11.
In the Windows sense, an “application” is an address space and at
least one thread of execution (a.k.a. a “process”). In the WPF sense, an
application is a singleton object
that provides services for UI components and UI programmers in the
creation and execution of a WPF program. More specifically, in WPF, an
application is an instance of the Application
class from the System.Windows
namespace.
Example 2-1shows code
for creating an instance of the Application
class.
using System; System.Windows; // the home of the Application class class Program { [STAThread] static void Main( ) { Application app = new System.Windows.Application( ); Window1 window = new Window1( ); window.Show( ); app.Run( ); } }
Here, we’re creating an application inside an STA
thread,[5] creating a window and showing it, and then running the
application. While the application is running, WPF processes Windows
messages and routes events to WPF UI objects as necessary. When the
Run
method returns, messages have
stopped being routed and generally don’t start again (unless you show
a modal window after the Run
method
returns, but that’s not something you’ll usually do). During its
lifetime, the application provides various services.
One of the services the Application
class provides is access to the
current instance. Once an instance of the Application
class is created, [6] it’s available via the Current
static property of the Application
class. For example, the code in
Example 2-1 is equivalent to
the code in Example 2-2.
using System; using System.Windows; class Program { [STAThread] static void Main( ) { // Fills in Application.Current Application app = new System.Windows.Application( ); Window1 window = new Window1( ); window.Show( ); Application.Current.Run(); // same as app.Run( ) } }
Here, in the process’s entry point, we’re creating an
application, creating and showing the main window, and then running
the application. Creation of the Application
object fills the static Application.Current
property. Access to the
current application is very handy in other parts of your program where
you don’t create the application or when you let WPF create the
application for you itself.
Because a Main
method that
creates and runs an application is pretty darn common, WPF can provide
the process’s entry point for you. WPF projects generally designate
one XAML file that defines the application. For example, if we had
defined our application in a XAML file with code behind, it would look
like Example 2-3.
<!-- App.xaml -->
<Application
x:Class="ImplicitAppSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" />
// App.xaml.cs
using System;
using System.Windows;
namespace ImplicitAppSample {
public partial class App : System.Windows.Application {
protected override void OnStartup(StartupEventArgs e) {
// let the base class have a crack
base.OnStartup(e);
// WPF itself is providing the Main that creates an
// Application and calls the Run method; all we have
// to do is create a window and show it
Window1 window = new Window1( );
window.Show( );
}
}
}
Notice that Example 1-11
is defining a custom application class in this code (ImplicitAppSample.App
) that derives from the
Application
class. In the OnStartup
override, we’re only creating a
window and showing it, assuming WPF is going to create the Main
for us that creates the instance of the
App
class and calls the Run
method (which calls the OnStartup
method). The way that WPF knows
which XAML file contains the definition of the Application
class is that the Build Action
is set to ApplicationDefinition
, as
shown in Figure 2-1.
The ApplicationDefinition
Build Action lets WPF know which class is our application and hooks it
up appropriately in a Main
method
it generates for us, which saves us from writing several lines of
boilerplate code.
For msbuild aficionados, the standard XAML Build Action
setting of Page
looks like this
in the .csproj file:
<Project ...>
...
<ItemGroup>
<Page Include="App.xaml" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
...
</ItemGroup>
...
</Project>
When we switch the Build Action to ApplicationDefinition
, it looks like
this:
<Project ...>
...
<ItemGroup>
<ApplicationDefinition Include="App.xaml" />
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
...
</ItemGroup>
...
</Project>
This setting causes the WPF build tasks to generate the following code:
namespace ImplicitAppSample { public partial class App : Application { [System.STAThreadAttribute( )] [DebuggerNonUserCodeAttribute( )]public static void Main( ) {
ImplicitAppSample.App app =
new ImplicitAppSample.App( );
app.Run( );
}
} }
Except for the debugger attribute (which stops Visual Studio from stepping into this method when debugging), this is equivalent to what we were writing by hand a few code samples ago.
If our window class is defined in a XAML file itself (as most
likely it will be), we can save ourselves from overriding the OnStartup
method by setting the StartupUri
property in the application’s
XAML file (see Example 2-4).
<!-- App.xaml --> <Application x:Class="ImplicitAppSample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Window1.xaml" />
The combination of setting the Build Action of the application’s
XAML file to ApplicationDefinition
and the StartupUri
property
provides the following features:
Creating an instance of the Application
object and setting it as the
value of the Application.Current
property
Creating and showing an instance of the UI defined in the
XAML designated in the StartupUri
property
Setting the Application
object’s MainWindow
property
Calling the Application
object’s Run
method, keeping
the application running until the main window is closed
This set of features makes more sense when we get a handle on what the “main window” is.
A top-level window is a
window that is not contained within or owned by another window (window
ownership is discussed in more detail later). A WPF application’s
main window is the top-level
window that is set in the MainWindow
property of the Application
object. This property is set by
default when the first instance of the Window
class is created and the Application.Current
property is set. In
other words, by default, the main window is the top-level window
that’s created first after the application itself has been created. If
you like, you can override this default by setting the MainWindow
property manually.
In addition to the main window, the Application
class provides a list of
top-level windows from the Windows
property. This is useful if you’d like to implement a Window menu,
like the one in Figure 2-2.
To implement the Window menu, we first start with a MenuItem
element:
<!-- Window1.xaml --> <Window ...> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Menu> <MenuItem Header="Window" x:Name="windowMenu"> <MenuItem Header="dummy item" /> </MenuItem> </Menu> </Grid> </Window>
MenuItem
is a HeaderedItemControl
(as described in Chapter 5), which means that it has header content that
we’ll use to hold the name of the menu item (“Window”), and subcontent
that we’ll use to hold the menu items for each top-level window.
Notice the use of a dummy subitem to start with. Without this dummy
item, you won’t be able to get notification that the user has asked to
show the menu items (whether via mouse or via keyboard).
To populate the Window menu, we’ll handle the menu item’s
SubmenuOpened
event:
public partial class Window1 : Window { ... public Window1( ) { InitializeComponent( ); windowMenu.SubmenuOpened += windowMenu_SubmenuOpened; } void windowMenu_SubmenuOpened(object sender, RoutedEventArgs e) { windowMenu.Items.Clear( ); foreach (Window window in Application.Current.Windows) { MenuItem item = new MenuItem( ); item.Header = window.Title; item.Click += windowMenuItem_Click; item.Tag = window; item.IsChecked = window.IsActive; windowMenu.Items.Add(item); } } void windowMenuItem_Click(object sender, RoutedEventArgs e) { Window window = (Window)((MenuItem)sender).Tag; window.Activate( ); } }
When the SubmenuOpened
event
is triggered, we use the Application
object’s Windows
property to get a list of each
top-level Window
, creating a
corresponding MenuItem
for each
Window
.
For those of you already steeped in data binding and data
templates who are wondering why we’re populating the Window menu
manually, it’s because the WindowCollection
class that the Windows
property returns doesn’t provide
notifications when it changes, so once the Window menu is populated
initially, there’s no way to keep it up-to-date. Maybe next version
. . .
Some applications work naturally with the idea of a single main
window. For example, many applications (drawing programs, IDEs,
Notepad, etc.) have a single top-level window that controls the
lifetime of the application itself (i.e., when the main window goes
away, the application shuts down). On the other hand, some
applications have multiple top-level windows or some other kind of
lifetime control that’s independent of a single main window.[7] You can specify when your application shuts down by
setting the application’s ShutdownMode
property to one of the values
of the ShutdownMode
enumeration:
namespace System.Windows { public enum ShutdownMode { OnLastWindowClose = 0, // default OnMainWindowClose = 1, OnExplicitShutdown = 2, } }
The OnMainWindowClose
value
is useful when you’ve got a single top-level window, and the OnLastWindowClose
value is useful for
multiple top-level windows (and is the default). In either of these
cases, in addition to the automatic application shutdown the ShutdownMode
policy describes, an
application can also be shut down manually by calling the Application
object’s Shutdown
method. However, in the case of
OnExplicitShutdown
, the only way to
stop a WPF application is by calling Shutdown
:
public partial class Window1 : System.Windows.Window {
...
void shutdownButton_Click(object sender, RoutedEventArgs e) {
Application.Current.Shutdown( );
}
}
You can change the shutdown mode in code whenever you like, or you can set it in the application definition XAML:
<Application
x:Class="AppWindowsSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml"
ShutdownMode="OnExplicitShutdown" />
Of course, there are a number of ways to shut down a Windows
process. The Application.Shutdown
method is a nice way of doing it by closing the
top-level windows and returning from the Run
method. This lets the windows involved
get their Closing and Closed notifications, although canceling the
shutdown in the Closing
event
doesn’t actually stop the application shutdown process.
You can best see the life cycle of a standard application in the set of events that it exposes:[8]
Startup
Activated
Deactivated
DispatcherUnhandledException
SessionEnding
Exit
The Application
object’s
Startup
event is fired when the
application’s Run
method is
called, and it is a useful place to do application-wide
initialization, including the handling of command-line arguments,
which are passed in the StartupEventArgs
:
void App_Startup(object sender, StartupEventArgs e) { for (int i = 0; i !=e.Args.Length
; ++i) { // do something useful with eache.Args[i]
... } }
The Activated
event is
called when one of the application’s top-level windows is activated
(e.g., via a mouse click or Alt-Tab). Deactivated
is called when your
application is active and another application’s top-level window is
activated. These events are handy when you want to stop or start
some interactive part of your application:
void App_Activated(object sender, EventArgs e) { ResumeGame( ); } void App_Deactivated(object sender, EventArgs e) { PauseGame( ); }
The application’s dispatcher is an object that routes
events to the correct place, including unhandled exceptions. In the
event that you’d like to handle an exception otherwise unhandled in
your application—maybe to give the user a chance to save his current
document and exit—you can handle the DispatcherUnhandledException
event:
void App_DispatcherUnhandledException(
object sender, DispatcherUnhandledExceptionEventArgs e) {
string err = "Oops: " + e.Exception.Message
);
MessageBox.Show(err, "Exception", MessageBoxButton.OK);
// Only useful if you've got some way of guaranteeing that
// your app can continue reliably in the face of an exception
// without leaving this AppDomain in an unreliable state...
//e.Handled = true; // stop exception from bringing down the app
}
The Exception
property of
the DispatcherUnhandledExceptionEventArgs
event argument is useful to communicate to your users what happened,
whereas the Handled
property is
useful to stop the exception from actually bringing down the
application (although this is a dangerous thing to do and can easily
result in data loss).
The SessionEnding
event is
called when the Windows session itself is ending (e.g., in the event
of a shutdown, logoff, or restart):
void App_SessionEnding(object sender, SessionEndingCancelEventArgs e) {
if (MessageBox.Show(
e.ReasonSessionEnding
.ToString( ),
"Session Ending",
MessageBoxButton.OKCancel) == MessageBoxResult.Cancel) {
e.Cancel = true; // stop the session from ending
}
}
The ReasonSessionEnding
property of the SessionEndingCancelEventArgs
event
argument is one value in the ReasonSessionEnding
enumeration:
namespace System.Windows { public enum ReasonSessionEnding { Logoff = 0, Shutdown = 1, } }
The Cancel
property is
useful if you’d like to stop the session from ending, although this
is considered rude, and more progressive versions of Windows (like
Vista) may not let you change its decision to end a session at
all.
The Exit
event is called
when the application is actually exiting, whether the last window
has gone away, the Application.Shutdown
method is called, or
the session is ending. One of the overloads of the Shutdown
method allows the programmer to
pass an integer exit code, which is ultimately exposed by the
process for use by your favorite Win32 process examination APIs. By
default, this value is zero, but you can observe or override it in
the handlers for this event:
void App_Exit(object sender, ExitEventArgs e) { e.ApplicationExitCode = 452; // keep 'em guessing... }
While we’ve just been talking about the lifetime of an application, things get a bit more interesting when you take into account that multiple instances of a single application can be running at any one time simply because the user can double-click on the same EXE multiple times. In fact, the default behavior in Windows and WPF does nothing to hamper or support multiple instances of the same application. For example, if we double-click on the AppWindowsSample.exe from Figure 2-2 more than once, we get more than one instance, as Figure 2-3 shows.
In Figure 2-3, we’ve got several top-level windows, some associated with each of three instances of the application. Sometimes more than one instance of a single application is a good thing. However, sometimes it just confuses users. For example, in Figure 2-3, even though we’ve got a Window menu, only the windows associated with each instance of the application are shown, which can be confusing as heck to the poor user faced with such an application.
If you’d like your application to be single instance, it’s easy to detect an existing instance and shut down any subsequent instances (see Example 2-5).
public partial class App : System.Windows.Application { Mutex mutex; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); // Check for existing instance string mutexName = "MyCompanyName.MyAppName"; bool createdNew; mutex = new Mutex(true, mutexName, out createdNew); // If there is an existing instance, shut down this one if( !createdNew ) { Shutdown( ); } } }
In Example 2-5,
the key is to access a Windows mutex with a session-wide unique name
so that we can tell whether it was already created by an initial
instance or whether we’re the initial instance. The mutex name we’re
using is one we pick to be sufficiently unique for our needs. Once
the mutex has been created, it’ll live for the life of the WPF
Application
object itself, which
will live for the life of the process, so if we’re not the first one
to create the mutex, we shut down our application, causing our
process to exit.
However, it’s at this point that we realize that single instance detection isn’t the only feature we want; we also want the following:
Passing command-line arguments to the initial instance (e.g., in case a subsequent instance was passed a filename that the user would like opened)
Activating the main window of the initial instance
Dealing properly with multiple users logging into a single computer (even the same user logged in multiple times), giving each login an instance of the application
These are services that are not trivial to implement and we’d
really love it if .NET provided this functionality for us. The good
news is that it does. The bad news is that it’s provided only as
part of the .NET 2.0 Visual Basic support for Windows Forms. If
you’d like to take advantage of robust single instance management,
you have to load the Microsoft.VisualBasic
assembly, and you
have an interesting integration challenge ahead of you, as both
Visual Basic’s support for single instance management and WPF want
to be “the” application. However, it is
possible and you get to leverage Other People’s Code (OPC), of which
I’m a big fan, especially when the “other people” are a
multibillion-dollar corporation with a record of framework
maintenance and upgrades.[9] For an example of how to integrate single instance
detection from Visual Basic into WPF, check out the “Single Instance
Detection” sample in the Windows Platform SDK.[10]
In addition to what we’ve already discussed, the Application
class provides access to
app-level resources and navigation services. Chapter 12 discusses resources and Chapter 11 discusses navigation. The other major service
that WPF applications support is ClickOnce deployment, which we’ll
discuss right now.
For the purposes of demonstration, let’s build something vital for procrastinators the world over: an application to generate excuses. The application was started with the “Windows Application (WPF)” project template in Visual Studio 2005 and was implemented with some very simple code. When you run it, it gives you an excuse from its vast database, as shown in Figure 2-4.
For anyone to use this wonderful application, it must be published. The simplest way to publish your WPF application is by right-clicking on the project in the Solution Explorer and choosing the Publish option, which will bring up the first page of the Publish Wizard (shown in Figure 2-5).
Figure 2-5 asks you to choose where you’d like to deploy your application, including to the disk, to a network share, to an FTP server, or to a web site. By default, the Publish Wizard will assume you want to publish to the Publish subdirectory of your project directory. Clicking the Next button yields Figure 2-6.
Because we’ve chosen to publish to something besides a web site, the Publish Wizard wants to know how users will access your published application—in other words, from a URL, from a UNC path, or from some optical media. (If you choose to publish to a web site, the only way to access the application is from a URL, so it won’t bother to ask.) We’d like to test web deployment, so we pick that option and leave the default URL alone. Clicking Next yields Figure 2-7.
For WPF applications, Figure 2-7 lets us choose whether we’d like this application to be made available online (when the computer is able to connect to the application’s URL) as well as offline (when the computer can’t connect to the URL), or whether you’d like the application to be only available online. These two options correspond to the ClickOnce terms locally installed and online only, respectively.
The job of the Publish Wizard is to bundle up the files needed to deploy an application using ClickOnce, including the manifest files that ClickOnce needs to deploy the application to a client machine after it’s been published.
This example assumes a standalone application, which provides its own host window. WPF also supports the XBAP application type, which is an application composed of one or more pages and hosted in Internet Explorer 6+. You can also publish an XBAP via ClickOnce from within Visual Studio, but the options are different. Chapter 11 discusses XBAP creation, publication, and deployment details.
Leaving the default “online or offline” option and clicking the Finish button yields Figure 2-8.
Figure 2-8 reminds us what we get with a locally installed ClickOnce application (i.e., the application will appear in the Start menu and in the Add or Remove Programs Control Panel). Clicking Finish causes Visual Studio to publish the application to the filesystem, including a publish.htm file that you can use to test deployment. If you happen to have an IIS application set up in the same folder to which Visual Studio publishes, it will launch the publish.htm file for you, as shown in Figure 2-9.
For simple needs, this is the complete experience for publishing a WPF ClickOnce locally installed application.
The user experience for running a ClickOnce locally installed application begins with a web page, such as the one shown in Figure 2-9, that includes a link to install the ClickOnce application. Clicking the link for the first time shows a download progress dialog similar to Figure 2-10.
Once the metadata file describing the application deployment settings has been downloaded (this file is called the application manifest), it will be checked for a certificate, which is extra information attached to the application that identifies a validated publisher name. ClickOnce requires all published applications to be signed, so Visual Studio will generate a certificate file for you as part of the initial publication process if you haven’t already provided one.
If the certificate used to sign the application manifest identifies a publisher that is already approved to install the application on the user’s machine (such as from a previous version or a IT-administered group policy), the application will be run without further ado, as shown at the beginning of this chapter in Figure 2-4.
If, on the other hand, the publisher’s certificate cannot be verified or is not yet trusted to run the application in question, a dialog similar to Figure 2-11 will be presented.
Figure 2-11 displays the name of the application, the source of the application, and the publisher of the application according to the certificate (or “Unknown Publisher” if the certificate could not be verified). It also lists a summary of the reasons this dialog is being shown, along with a link to more detailed warning information. However, such information will likely be ignored by the user choosing between the Install and Don’t Install buttons, from which the user will choose depending on the level of trust she has for the publisher she sees in the Security Warning dialog.
If the user chooses Don’t Install, no application code will be downloaded or executed. If she chooses Install, the application is downloaded, added to the Start menu, and added to the Add or Remove Programs Control Panel, all under the umbrella of the progress dialog shown in Figure 2-12, after which the application is executed.
Subsequent runs of the same version of the application, as launched from either a web site or the Start menu, will not ask for any additional user input (although they may show a dialog if checking for updates), but will launch the installed application directly.
There are a great number of additional details to ClickOnce application deployment, including security considerations, command-line handling, updating and rollback, prerequisite installation, access to external information sources, and certificate management, just to name a few. All of these details are beyond the scope of this book and are covered in great detail by other sources.[11] However, following are some specifics to standalone and XBAP ClickOnce deployment you might like to see all in one place.
Standalone WPF applications deployed using ClickOnce:
Can implement the main window with Window
or NavigationWindow
(although only the
former has a project template in Visual Studio—the “Windows
Application [WPF]” template)
Can be online-only or online/offline
If installed online/offline, can integrate with the Start menu, and can be rolled back and uninstalled
Must set “full trust” in the project’s Security settings
(the Window
class demands
this)
XBAP applications deployed using ClickOnce:
Provide their content with one or more Page
objects to be hosted in the
browser
Must be online-only to deploy with ClickOnce
There can be no “Security Warning” dialog, so must not attempt to elevate permissions beyond what is provided already on the client’s machine
No custom pop-up windows are allowed (e.g., no dialogs); can use standard page navigation, page functions, and message boxes instead
Designated as XBAP by setting the HostInBrowser
property to True
in the project file (will be set by
the “XAML Browser Application (WPF)” project template in Visual
Studio)
For the details of navigation-based applications and XBAP browser hosting and deployment, read Chapter 11.
WPF applications gain access to all the same application and user setting options that any other .NET application can use (e.g., the Registry, .config files, special folders, isolated storage, etc.).
The preferred settings mechanism for WPF applications is the one
provided by .NET 2.0 and Visual Studio 2005: the ApplicationSettingsBase
class from the
System.Configuration
namespace with
the built-in designer. To access the settings for your application,
click on the Settings tab in your project settings. This will bring up
the Settings Designer shown in Figure 2-13.
Here we’ve defined two settings: a user setting of type System.String
, called LastExcuse
; and an application setting of
type System.Boolean
, called
ExcludeAnimalExcuses
with a default
value of True
. These two settings
will be loaded automatically when I run my application, pulled from
the application’s configuration file (named MyApplication.exe.config) and the user
settings file saved from the application’s last session.
The Settings Designer manages a settings file and generates a class that allows you to program against the settings. For instance, our settings example will result in the class in Example 2-6 being generated (roughly).
using namespace System.Configuration;
namespace excusegen.Properties {
sealed partial class Settings
: ApplicationSettingsBase {
static Settings defaultInstance =
((Settings)(ApplicationSettingsBase.Synchronized(new Settings( ))));
public static Settings Default {
get { return defaultInstance; }
}
[UserScopedSettingAttribute( )]
[DefaultSettingValueAttribute("")]
public string LastExcuse {
get { return ((string)(this["LastExcuse"])); }
set { this["LastExcuse"] = value; }
}
[ApplicationScopedSettingAttribute( )]
[DefaultSettingValueAttribute("True")]
public bool ExcludeAnimalExcuses {
get { return ((bool)(this["ExcludeAnimalExcuses"])); }
}
}
}
There are several interesting things to notice about Example 2-6. The first is the
defaultInstance
member, which is
initialized with an instance of the generated Settings
class that’s been synchronized to
allow for safe multithreaded access. Second, notice that this defaultInstance
member is static and exposed
from the Default
static property,
which makes it very easy to get to our settings, as we’ll soon see.
Finally, notice the two properties exposed from the Settings
class, one property for each of our
settings in the Settings Designer. You can see that the mode of each
property, user versus application, the default value, and the type all
match. Further, although a user setting is read-write (it has a getter
and a setter), because it can change during an application session,
the application setting is read-only (it has only a getter). The
implementations of the properties are just type-safe wrappers around
calls to the ApplicationSettingsBase
base class, which
does the work of reading and writing your settings to the associated
settings storage.
With these typed properties in place and the Default
static property to expose an
instance of our generated Settings
class, usage is no different from any other CLR object, as you can see
in Example 2-7.
public partial class Window1 : Window { string[] excuses = {...}; public Window1( ) { InitializeComponent( ); this.newExcuseButton.Click += newExcuseButton_Click; // If there is no "last excuse," show a random excuse if( string.IsNullOrEmpty(Properties.Settings.Default.LastExcuse
) ) { ShowNextExcuse( ); } // Show the excuse from the last session else { excuseTextBlock.Text = Properties.Settings.Default.LastExcuse; } } void newExcuseButton_Click(object sender, RoutedEventArgs e) { ShowNextExcuse( ); } Random rnd = new Random( ); void ShowNextExcuse( ) { // Pick a random excuse, saving it for the next session // and checking for animals do { Properties.Settings.Default.LastExcuse = excuses[rnd.Next(excuses.Length - 1)]; } while(Properties.Settings.Default.ExcludeAnimalExcuses
&& HasAnimal(Properties.Settings.Default.LastExcuse
) ); // Show the current excuse excuseTextBlock.Text = Properties.Settings.Default.LastExcuse; } bool HasAnimal(string excuse) {...} protected override void OnClosed(EventArgs e) { base.OnClosed(e); // Save user settings between sessions Properties.Settings.Default.Save( ); } }
In Example 2-7,
we’re using the LastExcuse
user
setting to restore the last excuse the user saw when running the
application previously, changing it each time a new excuse is
generated. The ExcludeAnimalExcuses
application setting is checked to exclude animal-based excuses, but it
is never set.[12] To store user settings that change during an
application’s session, we’re calling the Save
method on the Settings
object from the ApplicationSettingsBase
base class. This
class does the magic of not only keeping the settings in memory and
notifying you when a setting changes (if you choose to care), but also
automatically loading the settings when the application is loaded,
saving on demand.
To help with the loading and saving, the ApplicationSettingsBase
uses a settings provider, which is a pluggable
class that knows how to read/write application settings (e.g., from
the local filesystem, from the Registry, from a network server, etc.).
The only settings provider that comes out of the box in .NET 2.0 is
the one that writes to disk in a way that’s safe to use from even
partial trust applications (like an XBAP), but it’s not hard to plug
in your own settings provider if you need other behavior.[13]
None of the basics of the ApplicationSettingsBase
-inspired support for
settings, or any of the other mechanisms for doing settings in .NET,
is specific to WPF. However, because the ApplicationSettingsBase
class supports data
change notifications (specifically, it implements INotifyPropertyChanged
), we can bind to
settings data just like any other data (for the details of data
binding, see Chapter 6 and Chapter 7). For example, instead of manually
keeping the TextBlock
that shows
the excuse up-to-date, we can just bind the Text
property to the LastExcuse
property, as shown in Example 2-8.
<Window ... xmlns:local="clr-namespace:excusegen"
>
...
<TextBlock ...
Text="{Binding
Path=LastExcuse,
Source={x:Static local:Properties.Settings.Default}}" />
...
</Window>
Example 2-8shows a bit
of an advanced use of the binding syntax, but basically it says that
we’re binding the Text
property of
the TextBlock
to the LastExcuse
property of the excusegen.Properties.Settings.Default
object. As the LastExcuse
property
changes, so does the Text
property,
so we no longer need to keep the Text
property manually up-to-date; all we
need to do is manage the LastExcuse
property and the Text
property will
follow. For example:
Random rnd = new Random( ); void ShowNextExcuse( ) { // Pick a random excuse, saving it for the next session // and checking for animals do { // This updates the Text property on the TextBlock, too Properties.Settings.Default.LastExcuse = excuses[rnd.Next(excuses.Length - 1)]; } while( Properties.Settings.Default.ExcludeAnimalExcuses && HasAnimal(Properties.Settings.Default.LastExcuse) ); // No longer any need to manually update the TextBlock //excuseTextBlock.Text = Properties.Settings.Default.LastExcuse; }
The ability to use settings to drive a WPF UI makes the new .NET
2.0 ApplicationSettingsBase
and
Settings Designer the preferred means for managing settings in a WPF
application.
In WPF, the application contains an instance of the Application
object. This object provides
management services that let you control the lifetime of your
application, as well as resource management and navigation, covered in
Chapter 12, Appendix C, and Chapter 11, respectively. In this chapter, we also
discussed deploying standalone applications using ClickOnce. (XBAP
deployment can be found in Chapter 11.) Finally, to
manage user and application settings between application sessions, we
briefly discussed the ApplicationSettingsBase
-related settings
services provided by .NET 2.0 and Visual Studio 2005.
[5] * The “Single Threaded Apartment” (STA) was invented as part of the native Component Object Model (COM) to govern the serialization of incoming COM calls. All Microsoft presentation frameworks, native or managed, require that they be run on a thread initialized as an STA thread so that they can integrate with one another and with COM services (e.g., drag-and-drop).
[6] †WPF makes sure that, at most, one Application object is created per application domain. For a discussion of .NET application domains, I recommend Essential .NET, by Don Box with Chris Sells (Addison-Wesley Professional).
[7] * For example, if an Office application is serving OLE objects, closing the windows will not cause the process to stop until those OLE objects are no longer needed.
[8] * Navigation events aren’t listed here, but are discussed in Chapter 11.
[9] * Whether Microsoft has a “good” record of framework maintenance and updates, I’ll leave to you to decide....
[10] †Available online at http://msdn2.microsoft.com/en-us/library/ms771662.aspx (http://tinysells.com/85).
[11] * The SDK does a pretty good job, as does Smart Client Deployment with ClickOnce: Deploying Windows Forms Applications with ClickOnce, by Brian Noyes (Addison-Wesley Professional).
[12] * There is no configuration API to set an application setting.
[13] †The SDK comes with custom settings provider samples that use a web service and the Registry. I didn’t like the one based on the Registry, so I updated it and wrote a little article about the experience of writing and using a custom settings provider. It’s available at http://www.sellsbrothers.com/writing/default.aspx?content=dotnet2customsettingsprovider.htm (http://tinysells.com/86).