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.
An 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
{
protected override void OnVisualChildrenChanged(
DependencyObject visualAdded,
System.Windows.DependencyObject visualRemoved)
{
base.OnVisualChildrenChanged(visualAdded,
visualRemoved);
OnGridVisualChildrenChanged(new EventArgs());
}
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 an 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.