One of my customers asked me if WPF provides an option for storing or serializing controls state. For example, having a ListView, is it possible to store the width of its columns after closing and opening the Window, or maybe after restarting the application?
I was thinking to myself, sure, you should use Data Binding. All you have to do is to bind the width or height, of any element to a back storage. For example you can create a State class for storing the data, and then you should bind it to your properties, using the Binding markup extension.
Thinking twice, I realized that it is much more complicated than it looks. Data Binding is a great tool but it should be customized to support this feature.
So the answer was no! but...
Then I developed a smart Markup Extension, backed up with Attached Properties and a smart Back Storage to provide an easy way to save controls properties state.
For example, the code snippet bellow stores the Window Size and Position, and the ListView, GridViewColumn's width.
<Window x:Class="Tomers.WPF.State.Demo.DemoWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data="clr-namespace:Tomers.WPF.State.Demo.Data"
ElementState.Mode="Persist"
ElementState.UId="DemoWindow"
Height="{ElementState Default=300}" Width="{ElementState Default=300}" Left="{ElementState Default=100}" Top="{ElementState Default=100}"
Title="Test">
<Window.Resources>
<ObjectDataProvider x:Key="cConsultants" ObjectType="{x:Type data:Consultants}" /> </Window.Resources>
<StackPanel>
<ListView ItemsSource="{Binding Source={StaticResource cConsultants}}"> <ListView.View>
<GridView ColumnHeaderToolTip="Sela Consultants">
<GridViewColumn
ElementState.Mode="Persist"
ElementState.UId="DemoWindow_GridViewColumn1"
DisplayMemberBinding="{Binding Path=FirstName}" Header="First Name"
Width="{ElementState Default=100}" /> <GridViewColumn
ElementState.Mode="Persist"
ElementState.UId="DemoWindow_GridViewColumn2"
DisplayMemberBinding="{Binding Path=LastName}" Header="Last Name"
Width="{ElementState Default=100}" /> <GridViewColumn
ElementState.Mode="Persist"
ElementState.UId="DemoWindow_GridViewColumn3"
DisplayMemberBinding="{Binding Path=Blog}" Header="Blog"
Width="{ElementState Default=Auto}" /> </GridView>
</ListView.View>
</ListView>
</StackPanel>
</Window>
As you can see, I'm using my ElementState.Mode and ElementState.UId attached properties to tell the back storage to save the state for these element's dependency properties. Then I'm using my ElementState Markup Extension to set each property and its default value.
The ElementState.Mode attached property can be one of: Persist or Memory values.
- Persist is used to serialize the element state into an XML stream. Restarting the application will restore this state.
- Memory is used to hold the state only in memory. Restarting the application will not restore this state.
The ElementState.UId attached property is used to uniquely identify the element (this must be a unique name among all elements of the application).
To Load state and Save state you should call ElementStateOperations.Load and ElementStateOperations.Save respectively. For example:
public partial class App : System.Windows.Application
{ protected override void OnStartup(StartupEventArgs e)
{ using (Stream stream = File.Open("ElementStateDemo.xml", FileMode.OpenOrCreate, FileAccess.Read, FileShare.Read))
{ ElementStateOperations.Load(stream);
}
base.OnStartup(e);
}
protected override void OnExit(ExitEventArgs e)
{ using (Stream stream = File.Open("ElementStateDemo.xml", FileMode.Create, FileAccess.Write, FileShare.None))
{ ElementStateOperations.Save(stream);
}
base.OnExit(e);
}
}
You can download ver 1.0 from here.
It is a first prototype, it took me several hours to write it down, so use it on your own risk.
You are free to write comments for any suggestion, bug or other.
Update on 12-Aug-2008
Since I have two classes ElementState and ElementStateExtension, when using ElementState from XAML it maps to the first class which is not a markup extension. I changed the ElementStateExtension to PropertyStateExtension.
You can download ver 1.1 from here.
Update on 16-Aug-2008
Thanks to Brandon comment, a weird bug was found after installing .NET 3.5 SP1. Somehow the ValueConverter.ConvertBack stop working after a success binding. I tried to figure out what happened in SP1 and got this:
Creating a property path with the "." syntax: new ProeprtyPath(".") which means that the path is the source object itself works up until .NET 3.5 SP1. I think that changes for supporting formatting arguments in Data Binding are responsible for this.
Anyway, I changed the property path as follows: binding.Path = new PropertyPath(string.Format("[{0}]", uid));
Where string.Format("[{0}]", uid) means: Get the value from the source indexer using uid as the index.
Also I made a little refactoring to the code, especially in the markup extension and data binding. Now it is two-way.
You can download ver 1.2 from here.