Monday, September 14, 2009

Application Settings and WPF Applications

This weekend, I managed to spend several hours working on my hobby project, Star Trek: SupremacySupremacy is an open-source, turn-based strategy game based on the “4X” model (eXplore, eXpand, eXploit, eXterminate).  I’ve been working on it for a few years now in my spare time, and it’s become my de facto “testbed” for all of Microsoft’s latest and greatest .NET development technologies.  As such, the game client is a pure WPF application, though I hope to eventually integrate a 3D combat engine based on either XNA or Managed Direct3D.

While there were plenty of “cool” features I could have worked on, I found myself thinking about how the player’s client settings are managed.  Until now, I have been using the standard ApplicatonSettings APIs that we’re all so familiar with.  I had originally gone this route just to get a quick-and-dirty solution in place, fully expecting to replace it later on.  There were certainly some benefits to using ApplicationSettings:

  • Visual Studio designer integration
  • Simple, out-of-the-box support for loading and saving settings
  • Relatively simple WPF data-binding support

The fact that I could simply data-bind WPF controls to properties on a static ‘Settings’ instance was probably the biggest factor in my decision.  There was no need to manually set a binding source at runtime, and the bindings would automatically subscribe to change notifications:

<CheckBox IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=CheckForUpdatesOnStartup, Mode=TwoWay}"
          Content="{s:StringResource Group=ClientSettings, Key=CheckForUpdatesOnStartup}" />

Great, so what’s wrong with this solution?  Well, Supremacy isn’t just a WPF application—it’s a Composite WPF application.  I recently started refactoring the game client to be more modular, using the Composite WPF (“Prism V2”) framework.  So far the refactoring has been a lot of fun, and I’m much happier with the direction in which the client architecture is heading.  Rather than rolling up all the client-related components into a single application project, I can break them down into separate modules.  For instance, the built-in application updater service has been broken down into an optional updater module.  Naturally, this modular architecture has thrown a wrench into how I handle client settings.

The game’s main menu has an “Options” button which opens a settings dialog, which hosts a collection of settings pages, each in its own tab.  The client application itself provides two settings pages, but other modules must be able to provide their own settings pages.  This is accomplished by adapting the TabControl as a Region.  In Composite WPF terminology, a Region is a placeholder for content and a host for visual elements within the shell.  When each module is initialized, it registers its settings page as a View to be automatically injected into the settings dialog Region.  It works great, too:

Client Settings Dialog

Okay, so now that we’ve solved the  problem of how modules hook into the settings dialog, we have to figure out how and where those settings are stored.  Now that the settings are provided by modules in several assemblies, it doesn’t make as much sense to use the ApplicationSettings API.  It is possible to inject new values into a Settings instance dynamically by means of its property bag.  However, this approach has some limitations, not the least of which relates to the data-binding of WPF controls to the Settings instance.  Sure, we can still achieve the desired two-way binding, thanks to PropertyPath’s support for indexed properties:

<CheckBox IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=Properties[CheckForUpdatesOnStartup], Mode=TwoWay}"
          Content="{s:StringResource Group=ClientSettings, Key=CheckForUpdatesOnStartup}" />

The problem, of course, is that our binding does not automatically receive change notifications for the values in the property bag.  Consequently, when the user changes some settings, clicks “Cancel”, and opens the settings dialog again, the settings reflected by the controls might be wrong.  Boo-urns.

So how do we fix this?  Clearly the ApplicationSettings APIs aren’t going to cut it anymore.  It appears we may have to devise a new system, so we may as well engineer one that specifically targets WPF applications.  And what’s at the very heart of WPF?  Two-and-a-half things:

  • Dependency Objects (and Dependency Properties)
  • Xaml!

How can we leverage these concepts?  Let’s start by designing our new ClientSettings class.  We need a set of base properties for the standard client settings, but we must also allow other modules to “attach” additional properties at runtime.  If you read that last sentence and instantly thought “w00t, attached dependency properties!”, give yourself a pat on the back.  If we design our ClientSettings class as a Dependency Object, then we can easily get and set attached properties.  Moreover, we can use XamlReader and XamlWriter to (de)serialize a ClientSettings using Xaml as our storage mechanism.  It doesn’t really matter where you store the Xaml—you can write directly to the user’s profile area, use the Isolated Storage APIs, or whatever medium you like.  So what does our new solution look like?

client_settings_diagram

A few points that may not be obvious from the class diagram:

  • Current is a static instance property.
  • EnableDialogAnimations is an attached dependency property.
  • Save, Reload, Loaded, and Saved are instance members.
  • The actual work for loading the persisted settings (or returning the default settings if no saved copy is available) is done in another method, LoadCore, which is not included in the diagram.
  • ClientSettings is located in a shared "infrastructure" assembly, while UpdaterSettings is located in the updater module's assembly.

We need to provide a mechanism for “cancelling” user changes that effectively reverts the properties to their last saved values.  Since our control bindings will use “ClientSettings.Current” as the binding source, we should not change the “Current” value as a result of reloading the settings from disk.  Instead, we should load the saved settings and copy the property values from the saved instance into the “Current” instance.  Thankfully, this is trivial to accomplish now that ClientSettings is a DependencyObject:

public void Reload()
{
    try
    {
        var savedOrDefaultSettings = LoadCore();
        var localValueEnumerator = savedOrDefaultSettings.GetLocalValueEnumerator();
        while (localValueEnumerator.MoveNext())
        {
            var currentEntry = localValueEnumerator.Current;
            this.SetValue(
                currentEntry.Property,
                currentEntry.Value);
        }
        OnLoaded();
    }
    catch {}
}

So far, I’ve only come up with one significant caveat to this design.  Can you guess what it is?  The saved Xaml file may reference attached properties that are defined in module assemblies.  So what happens if one of those modules is uninstalled, removing the assembly?  The next time the client tries to load the saved settings, an exception will be thrown.  We can counter this reasonably well by establishing some rules as to how modules are (un)installed.  If we explicitly inform a module that it is being uninstalled, then we can give that module an opportunity to clear any of its properties from the client settings.  Sure, it’s not a perfect solution, but given that most (if not all) of the modules will be produced by me, and it’s unlikely that users will be changing which modules are loaded, I’m willing to live with it.

So there you have it.  Article finished.  Why are you still here?  Do I sense that you’re wondering why EnableDialogAnimations is an attached property?  Would it make more sense if I told you that it’s not only an attached property, but an inherited property?  Are your spider senses tingling yet?  You see, by relying on dependency properties for our client settings, we have opened the door for an additional exploit: selective overriding of the settings.  In this example, the client shell binds its own EnableDialogAnimations value to the one stored in ClientSettings.Current.  The Style for my inline game dialogs includes an animation that plays when the dialog opens, but only when the EnableDialogAnimations property is set to “True”.  In most cases, a dialog simply inherits the user-specified value from the client shell, but there are a couple dialogs (such as the “loading” dialog) which are shown during processor- or I/O-intensive operations.  The animations for these dialogs would almost always be choppy, so I disable them manually by setting the local EnableDialogAnimations value to “False”.  Slick and easy.

Okay, so that’s the end.  For real this time.  Go grab a lemonade and write some code :).

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home