Monday, January 30, 2017

Vertical Separator in ListBox Group

Leave a Comment

I have a ListBox where I did the grouping based on a property like this :

CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(listbox.ItemsSource); PropertyGroupDescription groupDescription = new PropertyGroupDescription("CurrentDate"); view.GroupDescriptions.Add(groupDescription); 

And after grouping I want to add a vertical separator between the groups and I wrote a code like this:

<ListBox.GroupStyle>     <GroupStyle>         <GroupStyle.HeaderTemplate>             <DataTemplate>                 <StackPanel Orientation="Horizontal">                     <Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" />                     <TextBlock Text="{Binding Path=Name}"                                FontWeight="Bold"/>                 </StackPanel>             </DataTemplate>         </GroupStyle.HeaderTemplate>         <GroupStyle.Panel>             <ItemsPanelTemplate>                 <VirtualizingStackPanel Orientation="Horizontal"/>             </ItemsPanelTemplate>         </GroupStyle.Panel>     </GroupStyle> </ListBox.GroupStyle> 

But it's appearing like this:

enter image description here

Whereas I want a separator to go down totally but when I am trying to increase the height of the separator the items goes down along with that.

2 Answers

Answers 1

Diagnosis

When you group items in a ListBox using CollectionView + GroupStyle what happens is the ListBox displays a list of GroupItem controls, each representing a group of items. A GroupItem basically consists of a ContentPresenter (for presenting the header) and an ItemsPresenter (for presenting grouped items) put in a StackPanel.

When you specify GroupStyle.HeaderTemplate it will be used as ContentTemplate for the mentioned ContentPresenter. So if you increase the height of the Separator it will still be contained in the ContentPresenter causing it to grow vertically, and the items will still be stacked below it - hence your result.

Solution

What you need to do to achieve your goal is to re-template the GroupItem so that the Separator is displayed alongside the ContentPresenter and ItemsPresenter, and then wire it using GroupStyle.ContainerStyle. For convenience, let's put it in ListBox.Resources dictionary:

<ListBox (...)>     <ListBox.Resources>          <ControlTemplate x:Key="GroupItemTemplate" TargetType="{x:Type GroupItem}">             <DockPanel>                 <Separator DockPanel.Dock="Left"                            Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" />                 <StackPanel>                     <ContentPresenter /><!-- This will be automatically wired -->                     <ItemsPresenter Margin="5,0,0,0" /><!-- So will this -->                 </StackPanel>             </DockPanel>         </ControlTemplate>     </ListBox.Resource>     <ListBox.GroupStyle>         <GroupStyle>              <GroupStyle.HeaderTemplate>                 <DataTemplate>                     <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" />                 </DataTemplate>             </GroupStyle.HeaderTemplate>             <GroupStyle.ContainerStyle>                 <Style TargetType="{x:Type GroupItem}">                     <Setter Property="Template"                             Value="{StaticResource GroupItemTemplate}" />                 </Style>             </GroupStyle.ContainerStyle>             (...)         </GroupStyle>     </ListBox.GroupStyle>     (...) </ListBox> 

Notice that I removed the separator from the header template.

Here are possible outcomes you may want to get (I put a blue border around the ListBox to distinguish #3 and #4):

enter image description here

The code excerpt I provided will by default give you #1 (all separators are stretched vertically across the whole ListBox).

To achieve #2 (separators stretch down only to the last item of the corresponding group) you should add <Setter Property="VerticalAlignment" Value="Top" /> to the GroupStyle.ContainerStyle. Alternatively you could put it on the DockPanel inside the GroupItem template instead.

To get #3 (separators stretch to the height of the largest group) you should add VerticalAlignment="Top" to the panel inside the GroupStyle.Panel (the VirtualizingStackPanel in your case).

And finally #4 (the ListBox itself is restricted to the size of the largest group) can be achievied by putting VerticalAlignment="Top" on the ListBox itself.

Answers 2

I cannot imagine an out-of-the-box solution to this, since you are trying to pivot groups. I've made an example, yet it cant resize the columns' width within the itemsarea but at header without using Seperators:

Code-Behind

public partial class Window1  {      public Window1() {       InitializeComponent();        this._items.Add(new Item { Name = "one", DateTime = DateTime.Today });       this._items.Add(new Item { Name = "two", DateTime = DateTime.Today.Subtract(new TimeSpan(1, 0, 0, 0)) });       this._items.Add(new Item { Name = "three", DateTime = DateTime.Today.Subtract(new TimeSpan(1, 0, 0, 0)) });       this._items.Add(new Item { Name = "four", DateTime = DateTime.Today.Add(new TimeSpan(1, 0, 0, 0)) });       this._items.Add(new Item { Name = "five", DateTime = DateTime.Today.Add(new TimeSpan(1, 0, 0, 0)) });       this.DataContext = this;     }      private ObservableCollection<Item> _items = new ObservableCollection<Item>();      public ObservableCollection<Item> Items => _items;    }     public abstract class ViewModelBase : INotifyPropertyChanged {     public event PropertyChangedEventHandler PropertyChanged;      [NotifyPropertyChangedInvocator]     protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {       this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));     }   }    public class Item : ViewModelBase {     private string _name;     private DateTime _dateTime;      public string Name {       get {         return this._name;       }       set {         if (value == this._name)           return;         this._name = value;         this.OnPropertyChanged();       }     }      public DateTime DateTime {       get {         return this._dateTime;       }       set {         if (value.Equals(this._dateTime))           return;         this._dateTime = value;         this.OnPropertyChanged();       }     }    } 

Grouping with resources

 <Window.Resources>         <CollectionViewSource x:Key="CollectionViewSource" Source="{Binding Items}">             <CollectionViewSource.GroupDescriptions>                 <PropertyGroupDescription PropertyName="DateTime" />             </CollectionViewSource.GroupDescriptions>         </CollectionViewSource>     </Window.Resources> 

ListBox

<ListBox ItemsSource="{Binding Source={StaticResource CollectionViewSource}}" Width="400" Height="200">             <ListBox.GroupStyle>                 <GroupStyle>                     <GroupStyle.HeaderTemplate>                         <DataTemplate>                             <GridViewColumnHeader Content="{Binding Name}"/>                         </DataTemplate>                     </GroupStyle.HeaderTemplate>                     <GroupStyle.Panel >                         <ItemsPanelTemplate>                             <VirtualizingStackPanel Orientation="Horizontal"/>                         </ItemsPanelTemplate>                     </GroupStyle.Panel>                 </GroupStyle>             </ListBox.GroupStyle>             <ListBox.ItemContainerStyle>                 <Style TargetType="ListBoxItem">                     <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter>                     <Setter Property="VerticalContentAlignment" Value="Stretch"></Setter>                 </Style>             </ListBox.ItemContainerStyle>             <ListBox.ItemTemplate>                 <DataTemplate>                     <Border BorderBrush="DarkGray" BorderThickness="0,0,1,0" Margin="-6,-2,-6,-2">                         <StackPanel Margin="6,2,6,2">                             <TextBlock Text="{Binding Name}"/>                         </StackPanel>                     </Border>                  </DataTemplate>             </ListBox.ItemTemplate>         </ListBox> 

The keystone to this Workaround is to use a GridViewColumnHeader as HeaderTemplate for the GroupStyle.

Probably a better solution might be to change the ListBox to a ListView and set the ListView's View-Property to GridView. This requires to change your datastructure though.

Note

The ListBox's grouping was never meant to perform the Task you are trying to do. The default way with listbox and grouping is to have expanders in the ListBox's content area as described here

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment