A Configurable Window for WPF

Intro

Any application that people use often should be as convenient as possible.  One of the most expected conveniences of a desktop application is that the size, location, and state of the application’s various windows persist across runs.  If I resize a window, move it to the left side of my screen, and then close it, the next time I open that window it should be the same size and be at the same place on the left side of the screen.  It would be great if WPF provided that functionality for us, but it does not.  This post introduces a base window class that automates this for us, so that every window in our applications gets this functionality free.

What is involved?

The recipe for this dish involves:

  • 1 lbs of an abstract window subclass called ConfigurableWindow
  • 1 cup of a ConfigurableWindowSettings class that knows how to load and save the window settings
  • 1 dash of a ConfigurableWindow subclass which overrides the CreateSettings method and returns a ConfigurableWindowSettings object
  • 4 delicious properties in the Settings.settings file that are used to persist the window settings

This recipe serves one window.  For every additional window that needs to have its settings persisted, add three more properties to the Settings.settings file, and create a new ConfigurableWindowSettings object that knows how to get and set those values.

How to use it

It is easy to use the ConfigurableWindow class.  Here is a step-by-step explanation of how the demo application (available for download at the end of this post) was put together.  This walkthrough assumes that you already have a WPF project set up and you’ve included the source files for the ConfigurableWindow and ConfigurableWindowSettings classes.

First, create a new Window in your WPF project.  In this case, I named it DemoWindow.

Change DemoWindow so that it derives from ConfigurableWindow.  Note: you need to do this both in the code and XAML file.  Here is how you do it in the XAML:

Configurable Window (step 1)

Now that DemoWindow derives from ConfigurableWindow you need to override the CreateSettings method, and return an IConfigurableWindowSettings object.  Here is that method:

Configurable Window (step 2)

At this point, if you compile the project you will receive an error telling you that the DemoWindowConfigSettings class does not exist.  To remedy this situation, add a nested class in DemoWindow as seen below:

Configurable Window (step 3)

There are several things to notice about that class.  First, it derives from ConfigurableWindowSettings, which is a class that knows how to load and save window settings across runs of an application.  We pass into the base constructor five pieces of information: the backing store for all persisted property settings, and four property names.  Those property names correspond to settings that we will soon add to our application’s Settings.settings file.   Also, the DemoWindowConfigSettings class hooks the DemoWindow’s Closed event and sets the IsFirstRun setting to false.  This is necessary because it enables ConfigurableWindow to know if the persisted window location value is a value saved from a previous run or not.

Lastly, we need to add some settings into the project’s Settings.settings file.  We need to add four items, as seen below:

Configurable Window (step 4)

If you add more windows that derive from ConfigurableWindow, you only need to add the last three properties again with new names.  The IsFirstRun setting only needs to exist once since it applies to the entire application, not a specific window in the application.

Word of warning

If you run the application in debug mode (F5) you will be using a different config file than if you run without a debugger attached (Ctrl + F5).  This is the way things work in Visual Studio, so don’t be surprised if you stumble across this.

Get the code

Download the demo project here: Configurable Window Demo  (The source code was updated on 12/28/07).  Be sure to change the file extension from .DOC to .ZIP and then decompress the file.  Note, this project was built in Visual Studio 2008, so you will have to copy the source files into a new project if you are using VS2005.

About these ads

29 Responses to A Configurable Window for WPF

  1. marlongrech says:

    Good one… I like it :)

    Great job :)

  2. Josh Smith says:

    Thanks Marlon! I’m happy with it too. Wait until you see the application I’m building which needed it…much cooler! ;)

    Thanks,
    Josh

  3. TJ says:

    Wouldnt it be easier to just have those properties in the settings file handle multiple windows. Couldn’t you just csv the data? I could see having to add properties for each new window a little tedious. Otherwise nice job.

  4. MarcoB says:

    Found minor bug,

    If I drag the window on my secondary monitor and maximize it. It will reappear maximized on my primary monitor.

    Thanks and keep up the great work.

    If it wasn’t for you and Charles Peroltz. We would still be in WinForms.

  5. Josh Smith says:

    TJ,

    Using CSV to store data for multiple windows is an interesting idea. I’ll think about that!

    MarcoB,

    Thanks for the bug report. Unfortunately I don’t have a second monitor, so testing a fix is impossible for me right now. :( If anyone comes up with a fix for the bug MarcoB reported, please drop a comment with the code. Thanks!

    Josh

  6. agsmith says:

    I could be wrong but it looks like the WPF framework is only adjusting the location of the window when its window state is normal so the os is using the primary monitor for determining the monitor in which to maximize the window. I think you’re going to have to wait for the SourceInitialized event to fire (or override OnSourceInitialized) to initialize the window state.

  7. Josh Smith says:

    Thanks agsmith! Good catch. So, in the ConfigurableWindow.ApplySettings() method, you need to replace the line of code which sets the WindowState with this:

    this.SourceInitialized += delegate
    {
    base.WindowState = _settings.WindowState;
    };

    Granted, I have not tested that yet since I don’t have two monitors, but if you’re correct, then that code should fix the problem.

    Thanks!
    Josh

  8. Josh Smith says:

    NOTE: I updated the source code download on 12/28/07 to fix the bug agsmith pointed out. Thanks to MarcoB for testing the fix, too.

  9. Josh,

    For settings you could consider just serializing a class that holds a collection of window positions and possibly other settings as well. We are doing this in Mole. Then you can add or remove settings without having to do anything except edit the class.

    I did test this solution on my two and three monitor systems and it worked fine. However, I didn’t maximize and close them like Marlon did. I’ll update the code and test for you too.

    Cheers,

    Karl

  10. Josh Smith says:

    Karl,

    The backing store used is arbitrary from ConfigurableWindow’s perspective. Notice how its CreateSettings method returns an IConfigurableWindowSettings instance, which can be any type of object. I specifically set it up so that the using the Settings infrastructure is merely an option, not a requirement.

    If you decide that your application should not use the Settings support, but should save user settings in a database, or pass them to a Web service, or use a serialized object graph, then you can create your own class which implements IConfigurableWindowSettings and return it from CreateSettings.

    Thanks,
    Josh

  11. Anthony says:

    Hi Josh,

    Have you seen the GetWindowPlacement() and SetWindowPlacement() functions (in user32.dll). I know it’s P/Invoke, but they handle all the problems with multiple screens/resolutions, and if these change between runs (including making sure the window is still visible — many applications don’t do this). It has worked great in our WinForms apps.

    Thanks,

    Anthony.

  12. Josh Smith says:

    Anthony,

    Thanks for the tip about those Win32 functions. Those sound pretty useful!

    Josh

  13. James Hurst says:

    Josh, thank you for this article.

    Q: I can run your demo program just fine, but when I create a new one that is structured identically (except for the name) and try to run it, I get an InvalidCastException in class ConfigurableWindowSettings.GetValue, where propName is “MainWindowSize” (I named my main window that and renamed your properties accordingly). The stack trace indicates it’s happening in the first line of ConfigurableWindow.ApplySettings: “Size sz = _settings.WindowSize;”

    Does this look familiar?

    (I’m using VS2008, under Vista x64 Enterprise)

  14. Josh Smith says:

    James,

    In the Settings.settings file, be sure to set the MainWindowSize setting to be of type System.Windows.Size.

    Josh

  15. James Hurst says:

    That was it – my bad. Thanks Josh.

  16. James Hurst says:

    and thanks for the help and the quick response!

    BTW, is it not possible to put ConfigurableWindow into a separate library project (I’m trying to build several WPF apps and share as much as possible betweenst them)? When I move it into a separate class-library project, I can’t get it to compile. VS2008 says “‘Window’ does not exist in the namespace ‘Sysem.Windows’ (are you missing..” This is somewhat confusing: I have a “using Sysem.Windows” at the top. And ConfigurableWindow derives from System.Windows.Window. Are we not able to reference the Window class within a class library?

  17. Josh Smith says:

    James,

    It sounds like you need to need to add references in your class library to the PresentationCore, PresentationFramework, and WindowsBase assemblies. The Window class is in PresentationFramework, but it can’t hurt to add references to the other common WPF DLLs too.

    Josh

  18. Terry Hamaluk says:

    Josh

    Can you please explain why “DemoWindow.xaml” causes a problem loading in the designer ?

    The error that I get is “Type ‘ConfigurableWindow’ is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter.”

    Is there someway to correct this ?

  19. Josh Smith says:

    Terry,

    Add a public constructor with no parameters.

    Josh

  20. Jules says:

    Hi Josh,

    Great classes, but I don’t follow what you said to Terry – do you mean replace the protected ConfigurationWindow() constructor by making it public? If so then I get an error (when attempting to make the designer reload the window, but not when building) saying

    “Could not create an instance of type ‘ConfigurableWindow'”

    Any idea?

    Thanks,
    Jules

  21. Josh Smith says:

    Jules,

    Is ConfigurableWindow abstract? If so, that’s the problem. I don’t remember off the top of my head.

    Josh

  22. Jules says:

    Hi Josh – ridiculously fast response!

    Yes it is. The way you have it set up is like this;

    public abstract class ConfigurableWindow : Window

    protected ConfigurableWindow()
    {

    Everything compiles like that, but the designer will not render my xaml code that has a root element of ConfigurableWindow.

    In between crashing (VS2008), it gives me the same message Terry was getting and will not render anything.

  23. Josh Smith says:

    Try making it non-abstract. That might help. Visual inheritance is flaky in cider.

  24. Jules says:

    Thanks Josh. I might give that a go, but it would involve a fair amount of reworking your code as it all hinges around the abstract ConfigurableWindow class calling CreateSettings() which is overriden by the child window. Making it non abstract would involve changing the method by which the settings were created.

    Did all this work in VS2008 for you originally?

  25. Jules says:

    Just been reading some more about this and it turns out you’re right – Cider can only display concrete base classes.

    Thanks for your prompt replies – much appreciated.

    Cheers,
    Jules

  26. Eric says:

    Two slight nits on this excellent (as always, Josh!) class:

    1. Nit: it is slightly odd to have ConfigurableWindow call a virtual (abstract) function from its ctor (and it will generate an FxCop warning, I believe). The problem with the current pattern is that the overridden implementation of CreateSettings runs in a half-baked state (during the base class ctor, not after). Better might be to trade away the abstract CreateSettings() class and instead pass the IConfigurableWindowSettings instance to the ctor as a parameter, to wit:

    protected ConfigurableWindow(IConfigurableWindowSettings settings)
    {
    _settings = settings;

    2. Nit: the ConfigurableWindowSettings class you provide (which implements IConfigurableWindowSettings) might be less brittle if instead of passing string resource keys from the main window and doing type conversion in the generic Get/Set functions, you simply had an implementation which referenced the Settings properities using the .Net 2.0 wrappers. For example:

    public Point WindowLocation
    {
    get { return Settings.Default.WindowLocation; }
    set { Settings.Default.WindowLocation = value; }
    }

    Just random observations – certainly not worth losing any sleep over!!

    Great stuff as always, Josh. Cheers,
    -Eric

  27. Josh Smith says:

    Eric,

    Those are both great points. Thanks for sharing them! Have a great day.

    Josh

  28. JonS. says:

    Is there a way to name the Window object so you can later reference it in code behind. Adding an x:Name attribute yeilds the following error:

    The type CLASSNAME cannot have a Name attribute. Value types and types without a default constructor can be used as items within a ResourceDictionary

    Thanks!
    Jon

  29. Josh Smith says:

    JonS,

    What you’re trying to do doesn’t make sense. You can access the Window from its code-behind via the “Me” keyword, or “this” in C#.

    Josh

Follow

Get every new post delivered to your Inbox.

Join 287 other followers

%d bloggers like this: