Click here to Skip to main content
Click here to Skip to main content

WPF Persistency

By , 3 Jan 2013
 

Introduction

While looking for a way to persist state for a WPF application, I found the article WPF Control State Persistency from Tomer Shamam. Although the article was great and easy to use, I was inspired by a comment from Robert Cannon to try an alternate implementation.

The main differences are:

  • Automatic key generation
  • No need to explicitly load/save persisted data
  • No mode support (Memory/Persist)
  • Only one (static) dictionary for the whole project
  • Direct binding to back storage

Using the code

The following steps have to be done to use the code:

  • Copy attached file UserSettings.cs into your project
  • Add a namespace declaration to the XAML file xmlns:app="clr-namespace:WpfPersist".
  • Use the UserSettings markup extension (and provide default value) where appropriate.

The markup snippet below shows how to use the UserSettings markup extension to store Window Size and Position:

<Window x:Class="WpfPersist.Demo.Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:app="clr-namespace:WpfPersist"
   Title="WpfPersist.Demo"
   Height="{app:UserSettings Default=300}" 
   Width="{app:UserSettings Default=400}"
   Top="{app:UserSettings}" Left="{app:UserSettings}"
   >
   <Grid>
   ...
   </Grid>
</Window>

How it Works?

The main difference between this implementation and the one from Tomer is that I try to automatically derive a key for persistent storage. The snippet below shows how I do that for objects that derive from UIElement:

IUriContext uriContext = (IUriContext)serviceProvider.GetService
                            (typeof(IUriContext));
key = string.Format("{0}.{1}[{2}].{3}",
   uriContext.BaseUri.PathAndQuery,
   targetObject.GetType().Name, ((UIElement)targetObject).PersistId,
   targetProperty.Name);

Objects that have a parent in the logical tree (like ColumnDefinition) also can have the key automatically generated:

IUriContext uriContext = (IUriContext)serviceProvider.GetService
                            (typeof(IUriContext));
UIElement parent = (UIElement)LogicalTreeHelper.GetParent(targetObject);
int i = 0;
foreach (object c in LogicalTreeHelper.GetChildren(parent))
{
   if (c == targetObject)
   {
      key = string.Format("{0}.{1}[{2}].{3}[{4}].{5}",
         uriContext.BaseUri.PathAndQuery,
         parent.GetType().Name, parent.PersistId,
         targetObject.GetType().Name, i,
         targetProperty.Name);
      break;
   }
   i++;
}

Unfortunately I found no way to derive a key for GridViewColumn objects.
For debug builds, the code issues an assert on properties where no key can be generated. In release builds, the code silently ceases functioning.
To work around this, there is a Key property on the markup extension. Note that the key should be unique for the whole project and not only the XAML file. The snippet below shows how to apply the Key property:

<GridViewColumn
   DisplayMemberBinding="{Binding Mode=OneTime,Path=ProcessName}"
   Header="ProcessName"
   Width="{app:UserSettings Default=100, 
                Key=Window1.ListView0.Col0.Width }" />

Another difference between the two implementations is that I use an ApplicationSettingsBase derived internal class for persistent storage. The current implementation saves the data automatically when the main Window is closing. Meaning that there is no need to provide additional code to save/load the data.

Points of Interest

The biggest shortcoming of this implementation is that I found no way to persist ordering for GridViewColumn's.
For Winforms, the designers provided the DisplayIndex property on the DataGridViewColumn object that could be used for that, but in WPF there is no such thing. The WPF-Designers obviously felt that a modified Columns collection should be enough.

Another issue that nearly drove me nuts was the XmlnsDefinitionAttribute that Tomer is using. It took me quite a while to figure out that this attribute works only for code that is implemented in a different assembly.

If anyone finds a good solution to persist column ordering or finds a way to generate a key for the GridViewColumn property, I'm more than happy to hear about it.

Change Log

  • 7 Aug 2007: Initial Version
  • 3 Jan 2013: Update to VS2010 (recommended fix for VS-Designer)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Reto Ravasio
Switzerland Switzerland
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionErrors and Warningsmemberyolki201219 Nov '12 - 0:52 
Error:
Error 3 Object reference not set to an instance of an object.
 

Warning 2   'System.Windows.UIElement.PersistId' is obsolete: 'PersistId is an obsolete property and may be removed in a future release.  The value of this property is not defined.'   
 

though program runs file
 
what is cause of error and warning and is there any way to get rid of them ?
I am using VS2012 .net4.5
AnswerRe: Errors and WarningsmemberFernando E. Braz22 Mar '13 - 7:02 
works fine. i have vs2010 and i am not having that warning.
BugBug fix : avoid exception in design modememberchprogmer6 Jun '12 - 3:12 
As you can see in the demo app, you cannot edit the XAML file in the VS' designer because it causes exceptions in UserSettings.cs
 
Fortunately this problem is easy to be solved.
Just insert these lines:
 
if (System.Reflection.Assembly.GetEntryAssembly()==null) // Design mode (the XAML designer called it).
  return ConvertFromString(targetObject, targetProperty, defaultValue);
 
just before the first occurrence of this line:
 
if (key == null)
 
Now you can play with the "default" values and watch the result at real time.
 
I hope that helps.
 
Tested with VS 2010, but not in Blend.
GeneralRe: Bug fix : avoid exception in design modememberyolki201219 Nov '12 - 0:49 
why error
Error 3 Object reference not set to an instance of an object.
comes prograM RUNS Fine but this error comes in warning ang errors
 
and warning
 
Warning 2 'System.Windows.UIElement.PersistId' is obsolete: 'PersistId is an obsolete property and may be removed in a future release. The value of this property is not defined.'
 
is ther nay way to get rid of it.
GeneralRe: Bug fix : avoid exception in design modememberchprogmer29 Nov '12 - 13:44 
1. For "PersistId is an obsolete property.." warning, it cannot be avoided since this property is necessary to identify the element but it is officially obsolete (although it still exists in dotnet 4).
2. For your error "Object reference not set", please give more information.
NewsRe: Bug fix : avoid exception in design modememberReto Ravasio3 Jan '13 - 14:07 
Thanks for the input. I finally found the time to update the article.
Instead of your solution which I think fails when hosted by native code, I've put in the following:
if (DesignerProperties.GetIsInDesignMode(targetObject))
   return ConvertFromString(targetObject, targetProperty, Default);
Works perfect!
SuggestionLicensememberchprogmer6 Jun '12 - 0:31 
Hello
 
Thank you for this nice work.
 
Could you consider to add a license to your work ?
I suggest the Ms-PL for its guarantees against patents, or the CPOL.
 
Other suggestion: update the source code using the previous questions/suggestions.
 
Thanks
GeneralBe carful with PersistId property because it is obsoletememberido.ran24 Jun '10 - 6:02 
Thank you very much for the article.
I just want to make sure people using this code pay attention to the user of PersistId property which is marked as Obsolete and will not exist in future releases of WPF.
 
Maybe you can change the impl. to use x:Uid instead.
 
Ido
GeneralRe: Be carful with PersistId property because it is obsoletememberReto Ravasio24 Jun '10 - 11:55 
I'm aware of this. I left the warning in place on purpose so that users compiling the application are notified about this potential pitfall. I didn't use x:Uid originally because they are not set automatically. but today as I now have a number of localizable applications I think your idea of using x:Uid is looking rather promising and I'l definitely have a closer look when MS makes PersistId inoperable.
Thanks a lot for the input.
reto
AnswerRe: Be carful with PersistId property because it is obsoletememberReto Ravasio3 Jan '13 - 5:02 
While updating the article I had a go at using the x:Uid property. It looks like this is leading nowhere Frown | :-(
I've also found your blog post[^] about this issue. I probably would have spent even more time without finding it Smile | :)
GeneralAbout the "Settings" classmemberAvi Bueno19 Mar '10 - 14:09 
Hi, this is a bit off-topic but it looks like you're in control with this material so I hope you could shed some light:
 
First, can you please explain why you use the line this["Dictionary"] in your code?
 
Second, I would like to save the settings file to a location of my choosing but I'm having a real hard time accomplishing that. I tried writing my own SettingsProvider class and feeding it to ApplicationSettingsBase, but I keep getting a null pointer exception when trying to address this["Dictionary"] and I have no idea why..
 
Can you please help, or at least point me to articles that could explains the above issues?
 
Thanks!
GeneralRe: About the "Settings" classmemberReto Ravasio21 Mar '10 - 14:13 
this["Dictionary"] accesses a named property provided by the base class. The name used ("Dictionary") is not important as long as it's unique within the settings group ("AppPersist"). You can replace "Dictionary" with any name you like because it's the only property in the group.

I don't think you can influence where the settings are stored when you use the classes from System.Configuration. I think the framework team did that on purpose so that application settings aren't spread allover the filesystem.
GeneralRe: About the "Settings" classmemberAvi Bueno9 Apr '10 - 11:20 
Thanks. I asked about "Dictionary" because it caused me exceptions when trying to work with my own SettingsProvider (but, of course, it happened with any other identifier I came up with).
 
After extensive searching I decided to adhere to a solution that copies a user.config file from my own directory to where it is expected to be (e.g. under "C:\Documents and Settings\...") and copy it back to my directory when the application is terminating.
GeneralMaking sure owned windows are also persistedmemberAvi Bueno12 Aug '09 - 7:00 
The demo app contains a single window, which traps the Window.Closing event.
But if you have several windows, and one or more are owned by others (e.g. this.Owner = that), the Window.Closing will be triggered for the owning window, but not for the owned window (See Window.Closing documentation - this behavior is by design).
 
The solution is to register on Window.Closed instead.
This event will happen for all windows - owned or not.
 
Good day!
AnswerRe: Making sure owned windows are also persistedmemberReto Ravasio12 Aug '09 - 9:37 
I don't see the problem. As I hook the main window only (by using Application.Current.MainWindow.Closing
I don't see how it can become an 'owned' window. I did read the documentation[^] however and got curious about the behavior on SessionEnding. They write:
If a session ends, because a user logs off or shuts down, Closing is not raised; handle SessionEnding to implement code that cancels application closure.
As I understand it, the handler ist not called on logout and should therefore not persist the data. I tested the code and I'm pretty sure that the handler still gets called Confused | :confused:
 
Reading up on the difference of Closed and Closing I think that you have a point here and that it's probably safer to use Closed as you proposed.

Thanks...
GeneralRe: Making sure owned windows are also persistedmemberAvi Bueno16 Aug '09 - 21:20 
Oops.. I wrote my prev. comment after pushing your solution further.. I'll roll back a bit..
 
My environment has multiple windows, so I had to make a few changes.
One of them was to count the number of opened windows, and to perform the settings.Save() operation only after the last window was closed. This solution was good as long as the windows didn't have an owner.
 
I then made a test where the main application window was the owner of other windows. With these settings, when I closed the main application window, all owned windows were closed as well, BUT - since they were listening to the Window.Closing event, this event didn't fire for the owned windows (in accordance with the manual), and thus my opened-windows-counter didn't decrement to zero and I had a bit of a mess.
 
From what I've learned, events that end with "ing" (e.g. Closing) may or may not fire, while the equivalent "ed" events (e.g. Closed) will always fire. That is why I replaced Window.Closing with Window.Closed.
 
Actually, this wasn't my main reason for the Window.Closed change.
I actually created a nice solution for saving complex types (e.g. DataGrid, TreeView, etc) into the UserSettings' dictionary. For this to happen I had to have a way to intercept every closed window, and that was the real main reason for the Window.Closed change.
 
Anyway, I'm only about 2 weeks into WPF so I learned a lot from your code.
 
Thanks!
 
-- Avi.
GeneralKudos + UserSettings DesignTime bug fixmemberAvi Bueno11 Aug '09 - 5:41 
Great work Reto - 5 thumbs up! Smile | :)
It's slick and elegant, and although it still needs some work (e.g. RadioButtons) - I love it!
 
One thing that took me a while to track was a problem viewing my windows in the designer.
I got non-stop exceptions and the designer refused to load.
 
I finally managed to nail it down to UserSettings.ConvertFromString()
 
The fix was to change stringValue == null ?
to (stringValue == null || descriptor.Converter == null) ?
 
Here's the fixed version:
private static object ConvertFromString(DependencyObject targetObject, DependencyProperty targetProperty, string stringValue)
{
    DependencyPropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(targetProperty, targetObject.GetType());
    return (stringValue == null || descriptor.Converter == null) ? descriptor.GetValue(targetObject) : descriptor.Converter.ConvertFromInvariantString(stringValue);
}
Thanks!
GeneralNice code! Help needed with radio btn persistencemembermcvf4 Jun '09 - 11:40 
Just want to say this was an excellent article!! I used the updated UserSettings.cs to convert to VS2008 and it works pretty nicely. One issue I've noticed (and can repro in the demo app) is that both radio button's "IsChecked" properties are persisted as "True" leading to problems when you read the data back in. Any thoughts on this? If you can't seem to repro this do you have an idea where I could place a breakpoint to detect when a property is changed?
Thanks in advance!
-Mick
AnswerRe: Nice code! Help needed with radio btn persistencememberReto Ravasio4 Jun '09 - 12:45 
I've also played around with radiobuttons but got nowhere. Sorry that I didn't update the article with the latest findings. Another issue I couldn't find a solution for is that the VS-Designer is hardly usable when Window.Height and Window.Width are persisted.
Sorry for not beeing able to help Frown | :-(
Reto
GeneralBugmemberFly17 Aug '08 - 1:17 
For some property Converter is null, for example CheckBox.IsChecked
GeneralRe: BugmemberReto Ravasio19 Aug '08 - 13:15 
you need to be a bit more specific, without a code sample I can't reproduce anything. The sample project has some checkboxes and last time I tried they worked as expected.
GeneralBug fix for hosting in VS2008 WPF designermemberErwyn7414 Aug '07 - 5:05 
This is a useful markup extension for WPF. However, the code as published with the article caused problems when opening a XAML file in the Visual Studio 2008 WPF Designer (beta 2). The markup extension is throwing exceptions when hosted in the WPF designer.
 
This prevents the WPF designer from showing the XAML file visually.
 
I solved this issue by adding some extra checks in the code and by renaming the UserSettings class to UserSettingsStorage.
 
Check out the adapted code: UserSettings.cs[^]
GeneralRe: Bug fix for hosting in VS2008 WPF designermemberReto Ravasio15 Aug '07 - 7:38 
Thanks a lot. I don't think that I will have time to start using VS2008 und update the code in the near future. I just hope that users see your post and don't get frustrated too much.
GeneralRe: Bug fix for hosting in VS2008 WPF designermemberDmitriy Sinyagin17 Jul '08 - 18:40 
Brilliant! Thanks! Both to you and to Reto!!
Generalnice.memberMichael Sync9 Aug '07 - 22:03 
nice one. you got 5 from me... thanks..
 
Thanks and Regards,
Michael Sync ( Blog: http://michaelsync.net)
 
If you want to thank me for my help, please vote my message by clicking one of numbers beside "Rate this message". Why vote? Plz Read it here. Thank you. Smile | :)

GeneralVery good!memberSuper Lloyd7 Aug '07 - 15:58 
I learn about a new usefull interface available from the IServiceProvider hey! Big Grin | :-D
 
I will study your code later and try to answer your questions Wink | ;)

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 3 Jan 2013
Article Copyright 2007 by Reto Ravasio
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid