How to collapse all Group Expander in ListView? - wpf

I have a ListView with a GroupStyle on it. And in the style i have an Expander. I want to use a ContextMenu in the ListView to collapse and expand all groups with one click and i want to expand every single group by clicking on the expander. How can i get the Groups and then expand this programmatically?
<Style x:Key="PropertyGroupStyle" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander Header="{Binding Name}" IsExpanded="True">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ListView Name="PropertyChangeList"
IsSynchronizedWithCurrentItem="True" Height="Auto"
ItemsSource="{Binding}"
>
<ListView.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource PropertyGroupStyle}"/>
</ListView.GroupStyle>
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Name="menuItemPropertyExpanderCollapse"
Header="{Binding Path=labelCollapse, FallbackValue='Collapse'}"
Click="menuItemPropertyExpanderCollapse_Click"
/>
<MenuItem Name="menuItemPropertyExpanderExpand"
Header="{Binding Path=labelExpand, FallbackValue='Expand'}"
/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.View>
<GridView AllowsColumnReorder="False" >
<GridViewColumn Header="Date Occured"
Width="20"
DisplayMemberBinding="{Binding DateOccured}" />
<GridViewColumn Header="PropertyName"
Width="Auto"
DisplayMemberBinding="{Binding PropertyName}"/>
</GridView>
</ListView.View>
</ListView>
ICollectionView PropertyListview = CollectionViewSource.GetDefaultView(hPropList);
PropertyListview.GroupDescriptions.Add(new PropertyGroupDescription("PropertyName"));
PropertyListview.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Ascending));
PropertyListview.SortDescriptions.Add(new SortDescription("DateOccurred", ListSortDirection.Ascending));
PropertyChangeList.ItemsSource = PropertyListview;
Has anybody an sample code to do the collapse and expand all groups with an ContextMenu? i dont find anything out there.

You could bind the IsExpanded property to the Tag property of the ListView:
<Style x:Key="PropertyGroupStyle" TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander Header="{Binding Name}"
IsExpanded="{Binding Tag, RelativeSource={RelativeSource AncestorType=ListView}, TargetNullValue=true, FallbackValue=true}">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
...and set the Tag property in the event handlers:
private void menuItemPropertyExpanderCollapse_Click(object sender, RoutedEventArgs e)
{
PropertyChangeList.Tag = false;
}
you answered the question right, but i forget to write more details. Yes now i can expand and collapse all groups but i cant expand anymore a single group. it is an all or nothing thing. My question missed some important details :-( I updated my question text.
Change the AncestorType of the binding to GroupItem and set the Tag property of each GroupItem by iterating through them in the visual tree:
private void menuItemPropertyExpanderCollapse_Click(object sender, RoutedEventArgs e)
{
foreach (GroupItem gi in FindVisualChildren<GroupItem>(PropertyChangeList))
gi.Tag = false;
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in FindVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
XAML:
<Expander Header="{Binding Name}"
IsExpanded="{Binding Tag, RelativeSource={RelativeSource AncestorType=GroupItem}, TargetNullValue=true, FallbackValue=true}">
<ItemsPresenter />
</Expander>

Related

Horizontal accordion control?

I want a control whose behavior is as follows:
Act like a Grid
Each child control is embedded in a horizontal Expander (whose header is binded to the control's Tag property)
Each of these Expander has its own ColumnDefinition
Only one of these expanders can be expanded at a time
Non-expanded Expanders' ColumnDefinition have a width set to Auto
The expanded Expander's one is * (Star)
It has to use these exact controls (Grid/Expander), and not some custom ones, so my application's style can automatically apply to them.
I can't seem to find something already made, no built-in solution seems to exist (if only there was a "filling" StackPanel...) and the only solution I can come up with is to make my own Grid implementation, which seems... daunting.
Is there a solution to find or implement such a control?
Here's what I have for now. It doesn't handle the "single-expanded" nor the filling. I don't really know if StackPanel or Expander is to blame for this.
<ItemsControl>
<ItemsControl.Resources>
<DataTemplate x:Key="verticalHeader">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
</DataTemplate>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="HeaderTemplate"
Value="{StaticResource verticalHeader}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="ExpandDirection"
Value="Right" />
</Style>
</ItemsControl.Resources>
<ItemsControl.Template>
<ControlTemplate>
<!-- Damn you, StackPanel! -->
<StackPanel Orientation="Horizontal" IsItemsHost="True"/>
</ControlTemplate>
</ItemsControl.Template>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</ItemsControl>
My first thought is to perform this kind of action with a Behavior. This is some functionality that you can add to existing XAML controls that give you some additional customization.
I've only looked at it for something that's not using an ItemsSource as I used a Grid with Columns etc. But in just a plain grid, you can add a behavior that listens for it's childrens Expanded and Collapsed events like this:
public class ExpanderBehavior : Behavior<Grid>
{
private List<Expander> childExpanders = new List<Expander>();
protected override void OnAttached()
{
//since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
AssociatedObject.Initialized += (gridOvject, e) =>
{
foreach (Expander expander in AssociatedObject.Children)
{
//store this so we can quickly contract other expanders (though we could just access Children again)
childExpanders.Add(expander);
//track expanded events
expander.Expanded += (expanderObject, e2) =>
{
//contract all other expanders
foreach (Expander otherExpander in childExpanders)
{
if (expander != otherExpander && otherExpander.IsExpanded)
{
otherExpander.IsExpanded = false;
}
}
//set width to star for the correct column
int index = Grid.GetColum(expanderObject as Expander);
AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
};
//track Collapsed events
expander.Collapsed += (o2, e2) =>
{
//reset all to auto
foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
{
colDef.Width = GridLength.Auto;
}
};
}
};
}
}
Use it like this, note you have to add System.Windows.Interactivity as a reference to your project:
<Window ...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:local="...">
<Window.Resources>
<DataTemplate x:Key="verticalHeader">
<ItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type Expander}}, Path=Header}" />
</DataTemplate>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="HeaderTemplate"
Value="{StaticResource verticalHeader}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="ExpandDirection"
Value="Right" />
</Style>
<local:ExpanderBehavior x:Key="ExpanderBehavor"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<i:Interaction.Behaviors>
<local:ExpanderBehavior/>
</i:Interaction.Behaviors>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2" Grid.Column="1">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3" Grid.Column="2">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</Grid>
</Window>
The final result:
Edit: Working with ItemsControl - add it to the grid that hosts the items, and add a little to manage the column mapping
public class ItemsSourceExpanderBehavior : Behavior<Grid>
{
private List<Expander> childExpanders = new List<Expander>();
protected override void OnAttached()
{
AssociatedObject.Initialized += (gridOvject, e) =>
{
//since we are accessing it's children, we have to wait until initialise is complete for it's children to be added
for (int i = 0; i < AssociatedObject.Children.Count; i++)
{
Expander expander = AssociatedObject.Children[i] as Expander;
//sort out the grid columns
AssociatedObject.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Grid.SetColumn(expander, i);
childExpanders.Add(expander);
//track expanded events
expander.Expanded += (expanderObject, e2) =>
{
foreach (Expander otherExpander in childExpanders)
{
if (expander != otherExpander && otherExpander.IsExpanded)
{
otherExpander.IsExpanded = false;
}
}
//set width to auto
int index = AssociatedObject.Children.IndexOf(expanderObject as Expander);
AssociatedObject.ColumnDefinitions[index].Width = new GridLength(1, GridUnitType.Star);
};
//track Collapsed events
expander.Collapsed += (o2, e2) =>
{
foreach (ColumnDefinition colDef in AssociatedObject.ColumnDefinitions)
{
colDef.Width = GridLength.Auto;
}
};
}
};
}
}
Used:
<ItemsControl>
<ItemsControl.Template>
<ControlTemplate>
<Grid IsItemsHost="True">
<i:Interaction.Behaviors>
<local:ItemsSourceExpanderBehavior/>
</i:Interaction.Behaviors>
</Grid>
</ControlTemplate>
</ItemsControl.Template>
<Expander Header="Exp1">
<TextBlock Text="111111111" Background="Red"/>
</Expander>
<Expander Header="Exp2">
<TextBlock Text="222222222" Background="Blue"/>
</Expander>
<Expander Header="Exp3">
<TextBlock Text="333333333" Background="Green"/>
</Expander>
</ItemsControl>
Note, that you'll have to add some logic to manage new/removed children if you have any changes to your ItemsSource!

Horizontally scroll all rows within column at the same time using one scrollbar

How can i horizontally scroll a listview within single column in a DataGrid, using a scrollbar that is external to the DataGrid, ensuring that each row scrolls at the same time?
(I would also like the starting position of the scrollbar to be all the way to the right, so that the last item in the listview is visible when the data is loaded)
I cannot use and third-party libraries or dlls.
Any help would be greatly appreciated.
Many thanks
EDIT
First column sample code
<DataGridTemplateColumn Width="325" Header="Header Text" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ListBox ItemsSource="{Binding TrendList}"
BorderThickness="0"
Background="Transparent"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
HorizontalAlignment="Center"
HorizontalContentAlignment="Center"
>
<ListBox.Resources>
<Style TargetType="ScrollViewer">
<Setter Property="resex:ScrollSynchronizer.ScrollGroup" Value="Group1" />
<Setter Property="resex:ScrollSynchronizer.AlwaysScrollToEnd" Value="True" />
</Style>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<Label Margin="2,2,2,2" Width="25" Height="25"
HorizontalAlignment="Center" VerticalAlignment="Center"
HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
Content="{Binding ArisingList.Count}"
FontWeight="Bold
>
<interact:Interaction.Triggers>
<interact:EventTrigger EventName="MouseDoubleClick">
<interact:InvokeCommandAction Command="{Binding OpenPCICommand}"
CommandParameter="{Binding ArisingList}" />
</interact:EventTrigger>
</interact:Interaction.Triggers>
<Label.Style>
<Style TargetType="Label">
<Setter Property="Background" Value="Green"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ArisingList.Count}" Value="0">
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
<Label.ToolTip>
<TextBlock x:Name="pciID" Text="{Binding PciID, StringFormat='PCI Number : {0}'}" />
</Label.ToolTip>
</Label>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</DataTemplate>
This is what i have so far. The scrollbars are all linked internally but they arent linked to any external scrollbar
I managed to create a synchronous scroll behavior using Attached properties
may this help you achieve your goal in some way
for this I have made some changes to your template as well
ListBox to ItemsControl
Wrapped ItemsControl in a ScrollViewer
Added ScrollHelper.IsSyncScrollEnabled & ScrollHelper.ScrollValue to ScrollViewer
added a Name to main DataGrid
sample xaml
<Grid xmlns:l="clr-namespace:CSharpWPF">
<DataGrid x:Name="dGrid" ItemsSource="{Binding MyDataSource}">
<DataGrid.Columns>
<DataGridTemplateColumn Width="325"
Header="Header Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Hidden"
l:ScrollHelper.IsSyncScrollEnabled="true"
l:ScrollHelper.ScrollValue="{Binding (l:ScrollHelper.ScrollValue),ElementName=dGrid,Mode=TwoWay}">
<ItemsControl ItemsSource="{Binding TrendList}">
<ItemsControl.ItemTemplate>
... your item template here
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
... other columns etc
</DataGrid.Columns>
</DataGrid>
</Grid>
ScrollHelper class
namespace CSharpWPF
{
class ScrollHelper : DependencyObject
{
public static bool GetIsSyncScrollEnabled(DependencyObject obj)
{
return (bool)obj.GetValue(IsSyncScrollEnabledProperty);
}
public static void SetIsSyncScrollEnabled(DependencyObject obj, bool value)
{
obj.SetValue(IsSyncScrollEnabledProperty, value);
}
// Using a DependencyProperty as the backing store for IsSyncScrollEnabled. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsSyncScrollEnabledProperty =
DependencyProperty.RegisterAttached("IsSyncScrollEnabled", typeof(bool), typeof(ScrollHelper), new PropertyMetadata(false, OnIsSyncScrollEnabledChanged));
private static void OnIsSyncScrollEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer sv = d as ScrollViewer;
if ((bool)e.NewValue)
sv.ScrollChanged += sv_ScrollChanged;
else
sv.ScrollChanged -= sv_ScrollChanged;
}
static void sv_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.HorizontalChange != 0)
{
ScrollViewer sv = sender as ScrollViewer;
SetScrollValue(sv, sv.HorizontalOffset);
}
}
public static double GetScrollValue(DependencyObject obj)
{
return (double)obj.GetValue(ScrollValueProperty);
}
public static void SetScrollValue(DependencyObject obj, double value)
{
obj.SetValue(ScrollValueProperty, value);
}
// Using a DependencyProperty as the backing store for ScrollValue. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ScrollValueProperty =
DependencyProperty.RegisterAttached("ScrollValue", typeof(double), typeof(ScrollHelper), new PropertyMetadata(0.0, OnScrollValueChange));
private static void OnScrollValueChange(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer sv = d as ScrollViewer;
if (sv != null)
{
double offset = (double)e.NewValue;
if (sv.HorizontalOffset != offset)
sv.ScrollToHorizontalOffset(offset);
}
}
}
}
result is synchronous scrolling between all of the scrollviewers
Extra
if you do not want scroll bar to appear on all rows you can make use of triggers
eg
<Grid xmlns:l="clr-namespace:CSharpWPF">
<DataGrid x:Name="dGrid">
<DataGrid.Columns>
<DataGridTemplateColumn Width="325"
Header="Header Text">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
x:Name="scroll"
l:ScrollHelper.IsSyncScrollEnabled="true"
l:ScrollHelper.ScrollValue="{Binding (l:ScrollHelper.ScrollValue),ElementName=dGrid,Mode=TwoWay}">
<ItemsControl ItemsSource="{Binding TrendList}">
<ItemsControl.ItemTemplate>
... your template here
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<DataTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter TargetName="scroll"
Property="HorizontalScrollBarVisibility"
Value="auto" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
result is a single scroll bar scrolls all the rows data
EDIT
to get the default scroll position to show the last item in the itemscontrol, change the method sv_ScrollChanged as follows
static void sv_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
ScrollViewer sv = sender as ScrollViewer;
if (e.HorizontalChange != 0)
{
SetScrollValue(sv, sv.HorizontalOffset);
}
if (e.ExtentWidthChange != 0)
{
SetScrollValue(sv, e.ExtentWidth);
}
}
If I understand you correctly, you want to be able to scroll across the Width of your DataGrid, but not using the DataGrid ScrollBars. As long as there is nothing restricting the Width of the DataGrid, then you can do this easily with a ScrollViewer. Try this:
<ScrollViewer>
<DataGrid ItemsSource="{Binding YourCollectionProperty}" />
</ScrollViewer>

WPF combobox in a datagrid group header

Is it possible to use a combobox in a datagrid group header to set a selected value for each cells of the appropriate column (this one contains comboboxes)?
Or is there any better solution to set multiple combobox values in a column at the same time?
You can put a ComboBox in the group header like this:
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander>
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" VerticalAlignment="Center" />
<TextBlock Text="{Binding Path=ItemCount}" Margin="5,0,2,0" VerticalAlignment="Center" />
<TextBlock Text="Items" VerticalAlignment="Center" />
<ComboBox Margin="10,0" SelectionChanged="ComboBox_SelectionChanged">
<ComboBoxItem Content="1" />
<ComboBoxItem Content="2" />
<ComboBoxItem Content="3" />
</ComboBox>
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
The SelectionChanged event handler looks something like this:
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
DependencyObject dependencyObject = sender as DependencyObject;
// Walk up the visual tree from the ComboBox to find the GroupItem.
while (dependencyObject != null && dependencyObject.GetType() != typeof(GroupItem))
{
dependencyObject = VisualTreeHelper.GetParent(dependencyObject);
}
GroupItem groupItem = dependencyObject as GroupItem;
CollectionViewGroup collectionViewGroup = groupItem.Content as CollectionViewGroup;
// Iterate the items of the CollectionViewGroup for the GroupItem.
foreach (object item in collectionViewGroup.Items)
{
// Your logic for changing values here...
}
}

wpf datagrid automatically expand first group

I have a datagrid with the itemsource bound to a ListCollectionView with one group.
When i fill the collection, i want the first group autmatically viewed as expanded, how to code that in wpf (codebehind or mvvm)?
<DataGrid
ItemsSource="{Binding ResultColl}"
SelectedItem="{Binding Path=SelectedResultItem, Mode=TwoWay}"
SelectionMode="Single" IsReadOnly="True" >
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander>
<Expander.Header>
<StackPanel>
<TextBox Text="{Binding Items[0].ID}" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=ID}"/>
<DataGridTextColumn Binding="{Binding Path=Typ}"/>
<DataGridTextColumn Binding="{Binding Path=Info}"/>
<DataGridTextColumn Binding="{Binding Path=orderDate, StringFormat={}{0:dd-MM-yyyy}}"/>
</DataGrid.Columns>
</DataGrid>
In the mvvm controller:
ListCollectionView tmp = new ListCollectionView(myList);
tmp.GroupDescriptions.Add(new PropertyGroupDescription("ID"));
ResultColl = tmp;
...
ListCollectionView _resultColl;
public ListCollectionView ResultColl
{
get { return _resultColl; }
set { _resultColl = value;
RaisePropertyChanged("ResultColl");
if (value != null && _resultColl.Count > 0)
SelectedResultItem = _resultColl.GetItemAt(0) as ItemResult;
}
}
When executing the code, the datagrid is filled the 1st item is selected but group is collapsed.
Add IsExpanded property to your class and add binding to Expander:
<Expander IsExpanded="{Binding Items[0].IsExpanded}">
Set IsExpanded for first to true
you can try add another bool property to your View Model defaulted to true but switching to false when first time used. And bind IsExpanded property of Expander to this with OneTime mode.
public bool IsExpanded
{
get
{
if (_isExpanded)
{
_isExpanded = false;
return true;
}
return false;
}
}
Xaml would be like that:
<Expander IsExpanded="{Binding DataContext.IsExpanded, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGrid}}, Mode=OneTime}">

How apply MinWidth for ListView columns in WPF in control template?

Following the answer to a similar question here, I was able to set the MinWidth on the XAML page.
What I would like to do is accomplish this in the control template for all GridViewColumn's in all ListView's.
Is this possible?
Update:
I tried a simple bit of sample code below, but it does not work:
<Window x:Class="WpfApplication4.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<Style TargetType="{x:Type GridViewColumnHeader}" >
<Setter Property="MinWidth" Value="200" />
</Style>
</Window.Resources>
<Grid Width="500">
<Border BorderBrush="Black" BorderThickness="2" Margin="20">
<ListView SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Header="Header 1" Width="Auto">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="Hello There"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Header 2" Width="Auto" />
</GridView>
</ListView.View>
</ListView>
</Border>
</Grid>
</Window>
If you use a GridViewColumnHeader you can handle size changes:
<GridView>
<GridViewColumn>
<GridViewColumnHeader Content="HeaderContent" SizeChanged="HandleColumnHeaderSizeChanged"/>
...
in Code:
private void HandleColumnHeaderSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
if (sizeChangedEventArgs.NewSize.Width <= 60) {
sizeChangedEventArgs.Handled = true;
((GridViewColumnHeader) sender).Column.Width = 60;
}
}
<ListView>
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock MinWidth="100"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
...more columns...
</GridView>
</ListView.View>
</ListView>
<Window.Resources>
<Style TargetType="{x:Type GridViewColumnHeader}" >
<Setter Property="MinWidth" Value="400" />
</Style>
</Window.Resources>
I stumbled into this one also. To solved it I had to do two things :
Modify the ControlTemplate of ListView's header.
Handle the DragDelta event of the Thumb inside the ControlTemplate.
ListView's header is GridViewColumnHeader.
Shown below is a simplified version of GridViewColumnHeader's ControlTemplate. As we can see, it uses a Thumb in a Canvas to create the drag/resize effect.
PS: To obtain the complete GridViewColumnHeader ControlTemplate please refer to How to grab WPF 4.0 control default templates?
<ControlTemplate TargetType="GridViewColumnHeader">
<Grid SnapsToDevicePixels="True">
<Border BorderThickness="0,1,0,1" Name="HeaderBorder" ...>
<!-- omitted -->
</Border>
<Border BorderThickness="1,0,1,1" Name="HeaderHoverBorder" Margin="1,1,0,0" />
<Border BorderThickness="1,1,1,0" Name="HeaderPressBorder" Margin="1,0,0,1" />
<Canvas>
<Thumb Name="PART_HeaderGripper">
<!-- omitted -->
</Thumb>
</Canvas>
</Grid>
<ControlTemplate.Triggers>
<!-- omitted -->
</ControlTemplate.Triggers>
So In order to limit the size of GridViewColumnHeader, we need to hook Thumb's drag events(DragStarted, DragDelta, DragCompleted...etc).
Turned out all we need is the DragDelta event as long we can know the MinSize within the DragDeltaEventHandler.
Shown below is modified XAML with comment.
<Grid Width="500">
<Border BorderBrush="Black" BorderThickness="2" Margin="20">
<ListView SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Header="Header 1" Width="Auto">
<!-- Apply a style targeting GridViewColumnHeader with MinWidth = 80 and a ControlTemplate -->
<GridViewColumn.HeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="MinWidth" Value="80" />
<Setter Property="Control.Template" Value="{DynamicResource myGridViewColumnHeaderControlTemplate}" />
</Style>
</GridViewColumn.HeaderContainerStyle>**
</GridViewColumn>
<GridViewColumn Header="Header 2" Width="Auto" />
</GridView>
</ListView.View>
</ListView>
</Border>
</Grid>
In the myGridViewColumnHeaderControlTemplate add some XAML to:
Bind GridViewColumnHeader's MinWidth to Canvas's MinWidth.
Hook up Thumb's DragDelta event.
<ControlTemplate x:Key="TemplateGridViewColumnHeader" TargetType="GridViewColumnHeader">
<!-- omitted -->
<Canvas MinWidth="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MinWidth, Mode=OneTime}">
<Thumb x:Name="PART_HeaderGripper" DragDelta="myGridViewColumnHeader_DragDelta">
Finally the myGridViewColumnHeader_DragDelta function:
private void myGridViewColumnHeader_DragDelta(object sender, DragDeltaEventArgs e)
{
DependencyObject parent = sender as DependencyObject;
try
{
do
{
parent = VisualTreeHelper.GetParent(parent as DependencyObject);
} while (parent.GetType() != typeof(Canvas));
Canvas canvas = parent as Canvas;
if (canvas.ActualWidth + e.HorizontalChange < canvas.MinWidth)
{
e.Handled = true;
}
}
catch
{
}
}
This is the only way i find working. Do hope there is a simpler way.
I wanted to apply a minwidth to all columns, so I wrote this:
public static class GridViewConstraints
{
public static readonly DependencyProperty MinColumnWidthProperty =
DependencyProperty.RegisterAttached("MinColumnWidth", typeof(double), typeof(GridViewConstraints), new PropertyMetadata(75d, (s,e) =>
{
if(s is ListView listView)
{
listView.Loaded += (lvs, lve) =>
{
if(listView.View is GridView view)
{
foreach (var column in view.Columns)
{
SetMinWidth(listView, column);
((System.ComponentModel.INotifyPropertyChanged)column).PropertyChanged += (cs, ce) =>
{
if (ce.PropertyName == nameof(GridViewColumn.ActualWidth))
SetMinWidth(listView, column);
};
}
}
};
}
}));
private static void SetMinWidth(ListView listView, GridViewColumn column)
{
double minWidth = (double)listView.GetValue(MinColumnWidthProperty);
if (column.Width < minWidth)
column.Width = minWidth;
}
public static double GetMinColumnWidth(DependencyObject obj) => (double)obj.GetValue(MinColumnWidthProperty);
public static void SetMinColumnWidth(DependencyObject obj, double value) => obj.SetValue(MinColumnWidthProperty, value);
}
Just drop it on your listview:
<ListView b:GridViewConstraints.MinColumnWidth="255" />
Update to the solution of Billy Jake O'Connor who gave the most simple, easy to implement and WORKING CORRECTLY solution of them all.
For the people who don't want all columns to share the same minimum width, with the next code update you can set specific minimum width for each column separately specifying the min width directly in the column properties.
public static class GridColumn {
public static readonly DependencyProperty MinWidthProperty =
DependencyProperty.RegisterAttached("MinWidth", typeof(double), typeof(GridColumn), new PropertyMetadata(75d, (s, e) => {
if(s is GridViewColumn gridColumn ) {
SetMinWidth(gridColumn);
((System.ComponentModel.INotifyPropertyChanged)gridColumn).PropertyChanged += (cs, ce) => {
if(ce.PropertyName == nameof(GridViewColumn.ActualWidth)) {
SetMinWidth(gridColumn);
}
};
}
}));
private static void SetMinWidth(GridViewColumn column) {
double minWidth = (double)column.GetValue(MinWidthProperty);
if(column.Width < minWidth)
column.Width = minWidth;
}
public static double GetMinWidth(DependencyObject obj) => (double)obj.GetValue(MinWidthProperty);
public static void SetMinWidth(DependencyObject obj, double value) => obj.SetValue(MinWidthProperty, value);
}
And the XAML could be something like this ("local" is your using namespace name, modify accordingly)
<ListView>
<ListView.View>
<GridView>
<GridViewColumn local:GridColumn.MinWidth="25" />
<GridViewColumn local:GridColumn.MinWidth="100" />
<GridViewColumn local:GridColumn.MinWidth="200" />
</GridView>
</ListView.View>
</ListView>
You can try this, for each column, if you want to set different minimum width for all columns and maximum to auto
<ListView.View>
<GridView >
<GridViewColumn Header="FILE NAME" DisplayMemberBinding="{Binding fileName}" Width="auto" >
<GridViewColumn.HeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="MinWidth" Value="200" />
</Style>
</GridViewColumn.HeaderContainerStyle>
</GridViewColumn>
<GridViewColumn Header="ERROR DETAILS" DisplayMemberBinding="{Binding errorMessage}" Width="auto">
<GridViewColumn.HeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="MinWidth" Value="396" />
</Style>
</GridViewColumn.HeaderContainerStyle>
</GridViewColumn>
</GridView>
</ListView.View>

Resources