WPF : A Strange Layout Issue
The other day I was doing a new View for a WPF app that we are working on, that required a DataTemplate that consisted something like the following:
<DataTemplate DataType="{x:Type local:EmbeddedViewModel}"> <Expander x:Name="exp" Background="Transparent" IsExpanded="True" ExpandDirection="Down" Expanded="Expander_Expanded" Collapsed="Expander_Collapsed"> <ListView AlternationCount="0" Margin="0" Background="Coral" ItemContainerStyle="{DynamicResource ListItemStyle}" BorderBrush="Transparent" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding SubItems}" IsSynchronizedWithCurrentItem="True" SelectionMode="Single"> <ListView.View> <GridView> <GridViewColumn Width="100" Header="FieldA" DisplayMemberBinding="{Binding FieldA}"/> <GridViewColumn Width="100" Header="FieldB" DisplayMemberBinding="{Binding FieldB}"/> </GridView> </ListView.View> </ListView> </Expander> </DataTemplate>
Which is all cool, but when I let the View out there into the wild for testing the users were like, yeah that’s ok when there are lots of items, but what happens when there is only 1 item. We would like that 1 item to take up all the available space of the screen. And it got me thinking into how to do this. So this is what I came up with.
A ItemsControl that has a specialized Grid (GridWithChildChangedNoitfication) control as its ItemsPanelTemplate. The specialized grid simply raises an event to signal that it has had a new VisualChild added.
Here is the code for the full ItemsControl setup
<ItemsControl x:Name="items" ItemsSource="{Binding Path=Items, Mode=OneWay}" Background="Transparent" ScrollViewer.VerticalScrollBarVisibility="Auto"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <local:GridWithChildChangedNoitfication VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="0" Loaded="ItemsGrid_Loaded"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="{x:Type local:EmbeddedViewModel}"> <Expander x:Name="exp" Background="Transparent" IsExpanded="True" ExpandDirection="Down" Expanded="Expander_Expanded" Collapsed="Expander_Collapsed"> <ListView AlternationCount="0" Margin="0" Background="Coral" ItemContainerStyle="{DynamicResource ListItemStyle}" BorderBrush="Transparent" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" ItemsSource="{Binding SubItems}" IsSynchronizedWithCurrentItem="True" SelectionMode="Single"> <ListView.View> <GridView> <GridViewColumn Width="100" Header="FieldA" DisplayMemberBinding="{Binding FieldA}"/> <GridViewColumn Width="100" Header="FieldB" DisplayMemberBinding="{Binding FieldB}"/> </GridView> </ListView.View> </ListView> </Expander> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl>
And here is what the C# code looks like for the specialized Grid (GridWithChildChangedNoitfication).
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Controls; using System.Windows; namespace ShareOfSize { public class GridWithChildChangedNoitfication : Grid { /// <summary> /// Override that allows us to tell when the /// VisualChildren collection has changed /// </summary> protected override void OnVisualChildrenChanged( DependencyObject visualAdded, System.Windows.DependencyObject visualRemoved) { base.OnVisualChildrenChanged(visualAdded, visualRemoved); OnGridVisualChildrenChanged(new EventArgs()); } /// <summary> /// Raise an event to signal that a VisualChild has been /// added/removed /// </summary> public event EventHandler<EventArgs> GridVisualChildrenChanged; protected virtual void OnGridVisualChildrenChanged(EventArgs e) { EventHandler<EventArgs> handlers = GridVisualChildrenChanged; if (handlers != null) { handlers(this, e); } } } }
The last piece in the puzzle is some code behind that knows how to resize all the children in the ItemsControl when the VisualChild count of the specialized grid changes.
NOTE : This more than likely could be abstracted to a Attached Behaviour via an Attached Property, I’ll leave that as an activity for the user should they wish to do that. For me as it was so UI I did not mind this bit of code behind.
public partial class Window1 : Window { private GridWithChildChangedNoitfication itemsGrid; public Window1() { InitializeComponent(); this.DataContext = new Window1ViewModel(); this.Unloaded += ViewUnloaded; } private void ViewUnloaded(object sender, RoutedEventArgs e) { if(itemsGrid != null) { itemsGrid.GridVisualChildrenChanged -= GridChildrenChanged; } } private void GridChildrenChanged(object sender, EventArgs e) { ResizeRows(); } private void ResizeRows() { if (itemsGrid != null) { itemsGrid.RowDefinitions.Clear(); for (int i = 0; i < itemsGrid.Children.Count; i++) { RowDefinition row = new RowDefinition(); row.Height = new GridLength( itemsGrid.ActualHeight/ itemsGrid.Children.Count,GridUnitType.Pixel); itemsGrid.RowDefinitions.Add(row); itemsGrid.Children[i].SetValue( HorizontalAlignmentProperty, HorizontalAlignment.Stretch); itemsGrid.Children[i].SetValue( VerticalAlignmentProperty, VerticalAlignment.Stretch); itemsGrid.Children[i].SetValue(Grid.RowProperty,i); } } } private void ItemsGrid_Loaded(object sender, RoutedEventArgs e) { itemsGrid = sender as GridWithChildChangedNoitfication; if(itemsGrid != null) { itemsGrid.GridVisualChildrenChanged += GridChildrenChanged; } } private void Expander_Expanded(object sender, RoutedEventArgs e) { Expander expander = sender as Expander; var item = ItemsControl.ContainerFromElement(items, (DependencyObject)sender); Int32 row = (Int32)(item).GetValue(Grid.RowProperty); if (expander.Tag != null) { itemsGrid.RowDefinitions[row].Height = new GridLength((Double)expander.Tag,GridUnitType.Pixel); } } private void Expander_Collapsed(object sender, RoutedEventArgs e) { Expander expander = sender as Expander; var item = ItemsControl.ContainerFromElement(items, (DependencyObject)sender); Int32 row = (Int32)(item).GetValue(Grid.RowProperty); if (expander.Tag == null) { expander.Tag = itemsGrid.RowDefinitions[row].Height.Value; } itemsGrid.RowDefinitions[row].Height = new GridLength(40,GridUnitType.Pixel); } }
Here is what it all looks like
Now I am sure there is some bright spark out there that could have done this with Grid.IsSharedSizeScope I just couldn’t see that myself, so this is what I came up with.
As always here is a small demo app:
http://sachabarber.net/wp-content/uploads/2009/11/ShareOfSize.zip


























Oleg Mihailik said
am November 27 2009 @ 5:56 pm
Doesn’t show XAML, does it?
What I meant was UniformGrid with Cols=1
sacha said
am November 28 2009 @ 8:08 am
I’ll give this a shot, but then there is the expander issue. Ill try it though
sacha said
am November 28 2009 @ 8:20 am
Oleg,
The UniformGrid works in so far as everything initially gets a equal share of the available Height. But as my requirement NEEDS Expanders, you can not use the UniformGrid as it does not expose RowDefinitions required to store the current/old Expander heights.
Shame really, thanks for the idea though.