Introduction
How to easily hide columns and rows in a WPF Grid.
Background
The RowDefinition and ColumnDefinition are missing an obvious Visibility property. Why??? Who knows but it seems a rather silly oversight...
There have been various solutions mentioned (find these using Google etc...):
- Set the
ColumnDefinition/RowDefinitionWidth/Height
to "Auto" and then set the Visibility of each element sitting in that column/row to Visibility.Collapsed.
This method is by far the most heavy and quite frankly a waste of processing time... not to mention having to keep track of all the objects in a particular column/row etc...
- Manually setting the
Width/Height of the respective
ColumnDefinition/RowDefinition to 0...
This method is quick and easy but stumbles when it comes to unhiding - oh no - I forgot to remember what the width was before I set it zero... arghhhh........
now I have to keep track of all the column/row widths/heights or some other method...
Well, to be honest both methods, although they work, they suck...
I want to have my cake and to eat it too... This means I simply want to have a Visible property on my column and row definitions.
Solution
The solution is surprising easy...
For clarity I will now only refer to a ColumnDefinition for explanation. The solution is exactly the same for a
RowDefinition. All you will need to do is swap
the Width to Height and ColumnDefinition to RowDefinition, wave your magic wand - tap the screen and hey presto...
Recipe
(This recipe will only use a true/false for whether the column is visible. The
third state of Visibility.Hidden makes no sense in this context.
- We need a dependency property to add the '
Visible' to the ColumnDefinition.
- Use this new
Visible property to coerce the Width dependency property.
What is coercion??? In this case think of it as a filter that sits on top of the Width property. I will have it filter the Width value
to either report its original value or report zero depending on if the column is visible or not. It is important to note that coercion does not alter the underlying
Width property. This means that we don't need code trying to remember what the width was before we hide the column.
So lets define a subclass which inherits ColumnDefinition as thus:
public class ColumnDefinitionExtended : ColumnDefinition
{
}
Now we want to define our Visible dependency property using some boiler plate code inside our new class:
public class ColumnDefinitionExtended : ColumnDefinition
{
public static DependencyProperty VisibleProperty;
public Boolean Visible
{
get { return (Boolean)GetValue(VisibleProperty); }
set { SetValue(VisibleProperty, value); }
}
static ColumnDefinitionExtended()
{
VisibleProperty = DependencyProperty.Register("Visible",
typeof(Boolean),
typeof(ColumnDefinitionExtended),
new PropertyMetadata(true, new PropertyChangedCallback(OnVisibleChanged)));
}
public static void SetVisible(DependencyObject obj, Boolean nVisible)
{
obj.SetValue(VisibleProperty, nVisible);
}
public static Boolean GetVisible(DependencyObject obj)
{
return (Boolean)obj.GetValue(VisibleProperty);
}
}
OK, so that's taken care of.
We've defined the VisibleProperty as a static DependencyProperty and provided some getter/setters...
We have also provided a static constructor to register the property as required with a default value of true and providing a property changed callback.
The default value true is so that the column starts off by default as visible. The only thing missing now is the OnVisibleChanged callback code.
Now before we get there we need to put in some secret sauce and do a property override to be able to coerce the Width property.
This can be done by adding one more line to the static constructor as thus:
static ColumnDefinitionExtended()
{
VisibleProperty = DependencyProperty.Register("Visible",
typeof(Boolean),
typeof(ColumnDefinitionExtended),
new PropertyMetadata(true, new PropertyChangedCallback(OnVisibleChanged)));
ColumnDefinition.WidthProperty.OverrideMetadata(typeof(ColumnDefinitionExtended),
new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), null,
new CoerceValueCallback(CoerceWidth)));
}
We have now overridden the normal ColumnDefinition.WidthProperty to provide a CoerceValueCallback function.
This will enable the overriding of the Width property based on our requirements. In this case I will override the value if the
visibility is set to false.
So we now have two routines to go... OnVisibleChanged and CoerceWidth.
The OnVisibleChanged is really a one-liner telling the ColumnDefinition.WidthProperty to perform its coercion.
static void OnVisibleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
obj.CoerceValue(ColumnDefinition.WidthProperty);
}
So when we change the Visible property to either true or false this one liner forces the WidthProperty to be coerced.
I.e., the WidthProperty will be filtered against our requirements.
So here is the last piece of our puzzle...
static Object CoerceWidth(DependencyObject obj, Object nValue)
{
return (((ColumnDefinitionExtended)obj).Visible) ? nValue : new GridLength(0);
}
This is a simple test. If our Visible property is set to true then return the width as is... If the visible is set to false then set the width
to zero (thus hiding the column).
You might ask that if I set the width to zero when I 'hide' then when I set the visible back to true 'unhide' how does the width get set back to its original value.
As mentioned above the coercion does not overwrite the width value. The width value remains unchanged. The coercion simple alters the value that you 'see' from the WidthProperty.
Remember it acts like a filter.
So there we have it - The complete solution for column hiding is as follows:
using System;
using System.Windows;
using System.Windows.Controls;
namespace MyProject.MyNamespace.Extended
{
public class ColumnDefinitionExtended : ColumnDefinition
{
public static DependencyProperty VisibleProperty;
public Boolean Visible
{
get { return (Boolean)GetValue(VisibleProperty); }
set { SetValue(VisibleProperty, value); }
}
static ColumnDefinitionExtended()
{
VisibleProperty = DependencyProperty.Register("Visible",
typeof(Boolean),
typeof(ColumnDefinitionExtended),
new PropertyMetadata(true, new PropertyChangedCallback(OnVisibleChanged)));
ColumnDefinition.WidthProperty.OverrideMetadata(typeof(ColumnDefinitionExtended),
new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), null,
new CoerceValueCallback(CoerceWidth)));
}
public static void SetVisible(DependencyObject obj, Boolean nVisible)
{
obj.SetValue(VisibleProperty, nVisible);
}
public static Boolean GetVisible(DependencyObject obj)
{
return (Boolean)obj.GetValue(VisibleProperty);
}
static void OnVisibleChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
obj.CoerceValue(ColumnDefinition.WidthProperty);
}
static Object CoerceWidth(DependencyObject obj, Object nValue)
{
return (((ColumnDefinitionExtended)obj).Visible) ? nValue : new GridLength(0);
}
}
}
I will leave it as an exercise for you to produce the corresponding RowDefinitionExtended class... (should take all of about 10-30 seconds depending on how
fast you can cut/paste and find/replace...)
Examples
So let's look at an example of how to use the code...
At the top of your XAML file, stick in a namespace definition of where you have the classes defined. E.g...
xmlns:extended="clr-namespace:MyProject.MyNamespace.Extended"
Here is an ordinary Grid with three columns and three rows... Nothing really to note here... This is what it looks like before you upgrade it to using the new definitions.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
</Grid>
So here is the same Grid using the new extended column and row definitions.
<Grid>
<Grid.ColumnDefinitions>
<extended:ColumnDefinitionExtended />
<extended:ColumnDefinitionExtended />
<extended:ColumnDefinitionExtended />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<extended:RowDefinitionExtended />
<extended:RowDefinitionExtended />
<extended:RowDefinitionExtended />
</Grid.RowDefinitions>
</Grid>
Not much really happening - but we can do things like:
<Grid>
<Grid.ColumnDefinitions>
<extended:ColumnDefinitionExtended Visible="False" />
Or we could databind the Visible property to a datacontext/viewmodel...
<Grid DataContext="Something">
<Grid.ColumnDefinitions>
<extended:ColumnDefinitionExtended Visible="{Binding ColumnVisible}"/>
Or programmatically:
((ColumnDefinitionExtended)m_MyGrid.ColumnDefinitions[SomeColumnNumber]).Visible = false;
So there we have it - Our cake and we get to eat it too.....