I am using C# and WPF and I have a ListView that holds items with a CheckBox in the first column. The ListView's ItemsSource is set in code (not via binding) and contains instances of a class 'Item' with properties 'Name', 'Type' and 'Selected'.
public class Item : INotifyPropertyChanged
{
private string _name;
private bool _selected;
private string _type;
public string Name
{
get { return _name; }
set
{
_name = value;
this.OnPropertyChanged();
}
}
public bool Selected
{
get { return _selected; }
set
{
_selected = value;
this.OnPropertyChanged();
}
}
public string Type
{
get { return _type; }
set
{
_type = value;
this.OnPropertyChanged();
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string property = "")
{
if (this.PropertyChanged != null) this.PropertyChanged(this, new PropertyChangedEventArgs(property));
}
public event PropertyChangedEventHandler PropertyChanged;
}
The ListView's View is set to a GridView with the first column a checkbox that is bound to the Selected property - eg the checkbox signifies being 'selected'.
I am adding grouping to this ListView (grouped by 'Type'), and the GroupStyle contains a CheckBox as well.
var lst = new List<Item>();
lst.Add(new Item { Name = "A", Type = "1" });
lst.Add(new Item { Name = "B", Type = "1" });
lst.Add(new Item { Name = "C", Type = "1" });
lst.Add(new Item { Name = "A", Type = "2" });
lst.Add(new Item { Name = "B", Type = "2" });
lst.Add(new Item { Name = "C", Type = "2" });
listview.ItemsSource = lst;
var view = CollectionViewSource.GetDefaultView(lst);
view.GroupDescriptions.Add(new PropertyGroupDescription("Type"));
The XAML for the ListView contains the GridView and the GroupStyle:
<ListView x:Name="listview">
<!-- View -->
<ListView.View>
<GridView>
<GridViewColumn Width="50">
<GridViewColumn.CellTemplate>
<DataTemplate DataType="cls:Item">
<CheckBox IsChecked="{Binding Selected, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="300" Header="Name" DisplayMemberBinding="{Binding Name, UpdateSourceTrigger=PropertyChanged}"></GridViewColumn>
<GridViewColumn Width="100" Header="Type" DisplayMemberBinding="{Binding Type}"></GridViewColumn>
</GridView>
</ListView.View>
<!-- Group style -->
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="True">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<CheckBox></CheckBox>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding ItemCount, StringFormat='- {0} item(s)'}" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
Finally on to my question: what I would like to happen is to be able to use the CheckBox in the group header to select all or none of the items in that particular group. For example:
Clicking the group header checkbox should select all items in that particular group if they are not selected yet. Clicking it again should then de-select (uncheck) all items in that group. If the user manually selects or deselects some items in a group, it would be nice for the group header checkbox to show an indeterminate state, but just unchecked would be good too.
I have no idea where to begin with this. I assume I'll need to bind the IsChecked property of the group header checkbox, but I don't know what to bind it to as the datacontext will be some kind of GroupDescriptor thing that doesn't contain any information about the group, nor what items are in that group (right??).
I am not strictly following MVVM so I don't worry about doing it all in bindings and in my viewmodel, I would be fine with something like listening to the Checked event of the checkbox and somehow figuring out in code which items should be checked. For example; if I could listen to the Checked event and somehow extract the Type of the group I would be mostly set (I could go through the whole list and select all those with the matching group). But I don't even see a way of doing that; I can get the CheckBox in the Checked event (the sender), and I can loop up to all the parent controls but nowhere do I see a way to extract information about the property I'm grouping on...
Any help would be great!
I figured it out, all I needed was the DataContext of the CheckBox. It's not the pretiest solution (not MVVM for sure) but it seems to work ok.
Just add Checked and Unchecked event handlers to the checkbox in the group style, cast the DataContext to a CollectionViewGroup which contains the items.
In case of nested grouping the Items collection contains another instance of CollectionViewGroup, so you need to recursively loop through those items when you find another (nested) group:
private void OnGroupChecked(object sender, RoutedEventArgs e)
{
this.HandleGroupCheck((CheckBox)sender, true);
}
private void OnGroupUnchecked(object sender, RoutedEventArgs e)
{
this.HandleGroupCheck((CheckBox)sender, false);
}
private void HandleGroupCheck(CheckBox sender, bool check)
{
var group = (CollectionViewGroup) sender.DataContext;
this.HandleGroupCheckRecursive(group, check);
}
private void HandleGroupCheckRecursive(CollectionViewGroup group, bool check)
{
foreach (var itemOrGroup in group.Items)
{
if (itemOrGroup is CollectionViewGroup)
{
// Found a nested group - recursively run this method again
this.HandleGroupCheckRecursive(itemOrGroup as CollectionViewGroup, check);
}
else if (itemOrGroup is Item)
{
var item = (Item)itemOrGroup;
item.Selected = check;
}
}
}
Now I still need to figure out how to respond to checking the items and changing the checkbox in the corresponding group.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With