Introduction
Someone asked me if WPF provides an option for storing or serializing control state. For an instance, 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.
Using the code
To work with the attached code, you should:
- Add a reference to the Tomers.WPF.State.dll assembly (unless you have compiled it with a different name)
- Mark each persist-element with a unique ID (among the whole application) by using the
ElementState.UId
attached property
- Select the state mode by using the
ElementState.Mode
attached property
- Provide a value to any element property by using the
ElementState
Markup Extension
NOTE: I'm using the XmlnsDefinitionAttribute
attribute for mapping the CLR namespace into the WPF XML root namespace, so you don't need to use any prefix for using these custom types from XAML.
The markup snippet below demonstrates how to use my Markup Extension and Attached Properties to store a Window Size and Position, and a 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>
Using the Demo
Download the attached file, extract it into your local directory, and open the WPFElementState.sln solution file with Visual Studio 2005. Build and Run!
Now, resize the main window, move it to any place on the desktop, resize any grid column. Close the application, run it again! (The Window position and size, and columns width should be restored).
How it Works?
As you can see, I'm using the 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 the 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 and save state, you should call the ElementStateOperations.Load
and ElementStateOperations.Save
methods respectively. These methods accept any valid Stream
instance, which should be readable or writeable stream respectively. This stream is used for serializing the state by using the XmlSerializer
class. 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);
}
}
Points of Interest
It is a first prototype, it took me several hours to write it down, so use it at your own risk.