Thursday, October 13, 2011

Persisting a GridSplitter

Hi All,

If you are using WPF then you probably use the Grid container and if you are using that then at times you surely need the assistance of the GridSplitter. The GridSplitter is a sub element of the Grid and it allows the user to conveniently resize the grid columns or rows. Those of you who worked with the GridSplitter know that it is quite challenging to work with correctly.

I will start with a small example. In my application I need a vertical splitter between two buttons (conveniently named Left and Right). The initial size of the Left button should be twice the size of the Right button. Here is the XAML:

  1. <Grid>
  2.     <Grid.ColumnDefinitions>
  3.         <ColumnDefinition Width="2*"></ColumnDefinition>
  4.         <ColumnDefinition Width="Auto"></ColumnDefinition>
  5.         <ColumnDefinition Width="*"></ColumnDefinition>
  6.     </Grid.ColumnDefinitions>
  7.     <GridSplitter Width="5" Background="Red" VerticalAlignment="Stretch" HorizontalAlignment="Center" Grid.Column="1"></GridSplitter>
  8.     <Button Grid.Column="0">Left</Button>
  9.     <Button Grid.Column="2">Right</Button>
  10. </Grid>

And this is the result:

p2

I am using a technique where the GridSplitter sits in its own column. I find this approach better since then you don’t need to think how it should be position within the column or within the XAML file.

The problem with the GridSplitter becomes apparent when you try to use fixed sizes in the grid column width (the same applies for grid row height but I will continue the discussion with the grid columns). What if we want the left column to start with a fixed size of 5cm on the screen. The logical thing to do would be to set the width of the first column to 5cm. If you do that and run the application everything would seem normal until you start dragging the GridSplitter. Try dragging it to the right… way right… past the window border right… now let go and try to drag it back. That’s right. The GridSplitter is gone! I read somewhere on the Internet that the grid splitter does exactly what I told it to do. If you believe that then try to do the same with the right column and dragging the GridSplitter left. The phenomenon here is even stranger.

  1. <Grid>
  2.      <Grid>
  3.          <Grid.ColumnDefinitions>
  4.              <ColumnDefinition Width="*" ></ColumnDefinition>
  5.              <ColumnDefinition Width="Auto"></ColumnDefinition>
  6.              <ColumnDefinition Width="5cm" ></ColumnDefinition>
  7.          </Grid.ColumnDefinitions>
  8.  
  9.          <GridSplitter Width="5" Background="Red" VerticalAlignment="Stretch"
  10.     HorizontalAlignment="Center" Grid.Column="1"></GridSplitter>
  11.          <Button Grid.Column="0">Left</Button>
  12.          <Button Grid.Column="2" >Right</Button>
  13.      </Grid>
  14.  </Grid>

Don’t do that… Its crazy!

The point I am trying to make here is that the GridSplitter is best used when the widths of the column are star based. You can have some control using the MinWidth and MaxWidth properties applied on the columns, but sometimes this behavior is not the desired one.

Now for the main challenge. I want my application to restore the state of the GridSplitter position next time it is run. For example if I move the GridSplitter such that only the Right button is visible and close the application. When I start the application I expect only the Right button to be visible. Actually doing this is quite simple. I add x:Name attributes to the grid columns and handle the DragCompleted event of the GridSplitter. In the handler I serialize the width properties of both columns into a file. The problem here is that the Width is not a simple double value but a struct (GridLength). Fortunately, the framework provides us with a small helper class to serialize and deserialize this struct called GridLengthConverter. The serialization code looks like this:

  1. Assembly assembly = Assembly.GetExecutingAssembly();
  2. String directory = System.IO.Path.GetDirectoryName(assembly.Location);
  3. using (StreamWriter writer = new StreamWriter(Path.Combine(directory, "width.txt")))
  4. {
  5.   GridLengthConverter converter = new GridLengthConverter();
  6.   writer.WriteLine(converter.ConvertToString(LeftColumn.Width));
  7.   writer.WriteLine(converter.ConvertToString(RightColumn.Width));
  8. }

and the deserialization code is therefore like this:

  1. Assembly assembly = Assembly.GetExecutingAssembly();
  2. String directory = System.IO.Path.GetDirectoryName(assembly.Location);
  3. if (File.Exists(Path.Combine(directory, "width.txt")))
  4. {
  5.   using (StreamReader reader = new StreamReader(Path.Combine(directory, "width.txt")))
  6.   {
  7.     GridLengthConverter converter = new GridLengthConverter();
  8.     LeftColumn.Width = (GridLength)converter.ConvertFromString(reader.ReadLine());
  9.     RightColumn.Width = (GridLength)converter.ConvertFromString(reader.ReadLine());
  10.   }
  11. }

(it is run after InitializeComponents).

The important thing to notice here is that you must serialize both column width because they are relative to each other.

That’s it for today. Thank you for reading.

Boris.