I'm looking for a better way to make a programmatically generated window with the following behavior.
That is: List of expanders that can be initialized programmatically, each of which contains scrollable content larger than can be displayed in the window (in this case a datagrid). When an expander is expanded it's contents are limited to the available size of the window, while allowing all the rest of the expanders to be seen and manipulated. Additionally only one expander can be open at any given time.
This functionality seems that it could be useful in a lot of application menus, so I was surprised how difficult it was to implement. Is there a better way than what I did?
XAML (surprisingly simple):
<Window x:Class="ExpanderScrollExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="200">
<Grid>
<ItemsControl ItemsSource="{Binding Dict}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid Loaded="GridLoaded" ScrollViewer.CanContentScroll="False"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header ="{Binding Key, Mode=OneWay}" Expanded="Expander_Expanded" Collapsed="Expander_Collapsed">
<DataGrid ItemsSource="{Binding Path=Value}"/>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Code Behind (with most of the magic):
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace ExpanderScrollExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel();
}
private void GridLoaded(object sender, RoutedEventArgs e)
{
Grid grid = sender as Grid;
grid.LayoutUpdated += (s, e2) =>
{
var childCount = grid.Children.Count;
int rowsToAdd = (childCount - grid.RowDefinitions.Count);
for (int row = 0; row < rowsToAdd; row++)
{
RowDefinition rowDefinition = new RowDefinition();
rowDefinition.Height = new GridLength(0, GridUnitType.Auto);
grid.RowDefinitions.Add(rowDefinition);
}
for (int i = 0; i < childCount; i++)
{
var child = grid.Children[i] as FrameworkElement;
Grid.SetRow(child, i);
}
};
}
private void Expander_Expanded(object sender, RoutedEventArgs e)
{
ContentPresenter parentDataContext = VisualTreeHelper.GetParent(sender as DependencyObject) as ContentPresenter;
Grid grid = VisualTreeHelper.GetParent(parentDataContext as DependencyObject) as Grid;
grid.RowDefinitions[Grid.GetRow(parentDataContext)].Height = new GridLength(1, GridUnitType.Star);
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(grid); i++)
{
DependencyObject neighborDataContext = VisualTreeHelper.GetChild(grid, i);
for (int j = 0; j < VisualTreeHelper.GetChildrenCount(neighborDataContext); j++)
{
DependencyObject neighborExpander = VisualTreeHelper.GetChild(neighborDataContext, j);
if (neighborExpander is Expander && neighborExpander != sender)
{
((Expander)neighborExpander).IsExpanded = false;
this.Collapse(neighborExpander as Expander);
}
}
}
}
private void Expander_Collapsed(object sender, RoutedEventArgs e)
{
this.Collapse(sender as Expander);
}
private void Collapse(Expander expander)
{
ContentPresenter parent = VisualTreeHelper.GetParent(expander as DependencyObject) as ContentPresenter;
Grid grandparent = VisualTreeHelper.GetParent(parent as DependencyObject) as Grid;
grandparent.RowDefinitions[Grid.GetRow(parent)].Height = new GridLength(0, GridUnitType.Auto);
}
}
}
ViewModel (only for data generation)
using System.Collections.Generic;
namespace ExpanderScrollExample
{
class MainWindowViewModel
{
public Dictionary<string, List<MyClass>> Dict { get; }
public MainWindowViewModel()
{
Dict = new Dictionary<string, List<MyClass>>();
for ( int i = 0; i < 5; i++ )
{
string key = "Header " + i.ToString();
Dict[key] = new List<MyClass>();
for ( int j = 0; j < 100; j++)
{
Dict[key].Add(new MyClass(j, i*100 + j));
}
}
}
public class MyClass
{
public int Column1 {get; set;}
public int Column2 { get; set; }
public MyClass( int column1, int column2)
{
Column1 = column1;
Column2 = column2;
}
}
}
}
I also typically try to confirm with MVVM pattern, but was unable to do so in this case.
Generating items controls in grid is taken from here. I also considered using style triggers to expand/collapse and set size as described in this answer but I couldn't think of a good way how to bind the dynamically generated rows with expanders.
Is there a better way to do this?
1. Only one Expander
expanded at a given time
To make sure that only one Expander
is expanded at a given time, you can create an Attached Behavior
. I implemented one for reference.
ExpanderGroupBehavior (inspired by RadioButton
)
<Expander wpf:ExpanderGroupBehavior.GroupName="ExpanderGroup01" />
This makes sure that only one Expander
within the same group is expanded.
2. Expanded Expander
fill available space
To achieve that, you may create your own Panel
which handles that for you.
See How to get controls in WPF to fill available space? and https://learn.microsoft.com/en-us/dotnet/framework/wpf/controls/how-to-create-a-custom-panel-element