I've got an ItemsControl in XAML where I am displaying an expander for each group so I can expand/collapse the group. I want to persist the state of the IsExpanded
property (and potentially other settings relating to the display of the group header). Normally you just have a class with the properties on it and bind to this. However, the data context for the group is CollectionViewGroup
. Now this class isn't very helpful as it only gives you the Name property and the items in the group (which is fine if you just want a heading and maybe display some sort of metric based on the number of items in the group or their contents but not if you just want to store custom data about the state of the group header UI). What I'd like to do is to derive from this class and add other properties to my derived class and bind to that instead. But there doesn't seem to be any easy way to do this. All the details of group generation seem to be hidden away in internal classes which is very frustrating. Has anyone gone down the route of implementing ICollectionView
themselves (and therefore all the other related classes as well presumably)? It seems like a massive job to replicate everything in ListCollectionView
just to be able to create a custom CollectionViewGroup
class and bind to that instead! Thanks.
One approach is to use a MultiBinding
to find or compute the custom data and bind-time.
I made a DataGrid
with Groups that shows in header the sum of items specific value in the group, in order to update this sum when group items changes I made a multivalue binding with a custom multivalue converter, the multivalue binding with ItemCount
property permits to get notified when the group items changes and then to update the sum and display the newsum value.
Here is the code for the multivalue converter class :
Public Class UserBalanceConverter
Implements IMultiValueConverter
Private Function GetSubTotal(ByVal obj As CollectionViewGroup) As String
Dim total As Decimal
For Each objItem As Object In obj.Items
If TypeOf objItem Is Account Then
Dim a As Account = DirectCast(objItem, Account)
Dim rate As Decimal = 1
rate = 1 / ExchangeRatesInfo.GetExchangeRate(a.currencyCode.ToString)
total += a.Balance * rate
Else
total += GetSubTotal(objItem)
End If
Next
Return total.ToString("C")
End Function
Public Function Convert(ByVal value() As Object,
ByVal targetType As System.Type,
ByVal parameter As Object,
ByVal culture As System.Globalization.CultureInfo) _
As Object Implements System.Windows.Data.IMultiValueConverter.Convert
Dim cvg As CollectionViewGroup = CType(value(1), CollectionViewGroup)
Return GetSubTotal(cvg)
End Function
Public Function ConvertBack(ByVal value As Object,
ByVal targetType() As System.Type,
ByVal parameter As Object,
ByVal culture As System.Globalization.CultureInfo) _
As Object() Implements System.Windows.Data.IMultiValueConverter.ConvertBack
Throw New NotImplementedException
End Function
End Class
Then in XAML you use the multivalue converter in a style used for GroupItem :
<Style TargetType ="{x:Type GroupItem}" x:Key="UserGroupHeaderStyle">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander x:Name="exp" IsExpanded="False">
<Expander.Header>
<StackPanel >
<TextBlock Text="{Binding Name}" />
<StackPanel Orientation="Horizontal" >
<TextBlock Text="{Binding ItemCount}">
<TextBlock Text=" "/>
<TextBlock Text="items" />
<TextBlock Text=" "/>
<TextBlock Text="Balance: " />
<TextBlock>
<TextBlock.Text>
<MultiBinding Converter="{StaticResource UserBalanceConverter}">
<Binding Path="ItemCount"/>
<Binding />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Finish with applying the style to your DataGrid :
<DataGrid.GroupStyle>
<GroupStyle ContainerStyle="{StaticResource UserGroupHeaderStyle}">
<GroupStyle.Panel>
<ItemsPanelTemplate>
<DataGridRowsPresenter/>
</ItemsPanelTemplate>
</GroupStyle.Panel>
</GroupStyle>
</DataGrid.GroupStyle>
Also don't forget to declare your convert class in the resource section of your XAML :
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<local:UserBalanceConverter x:Key="UserBalanceConverter"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
Et Voilà ! It works like a charm!
HTH
A simple approach would be to just wrap the CollectionViewGroup into another ViewModel class wich provide the additional needed display properties like IsExpanded. A Lesson I learned the hard way was not to bend the xaml / view to match the business data. Instead bend / wrap or convert the business data to match the requirements of the UI.
I was able to solve exactly this problem (i.e. binding to IsExpanded
) using an approach that's somewhat similar to Cédric's suggestion, but in a seemingly more MVVM way:
<ControlTemplate TargetType="GroupItem">
<TreeViewItem IsExpanded="{Binding Items[0].IsGroupExpanded, Mode=TwoWay}">
<TreeViewItem.Header>
<TextBlock Text="{Binding Name}" />
</TreeViewItem.Header>
<TreeViewItem.Items>
<ItemsPresenter />
</TreeViewItem.Items>
</TreeViewItem>
</ControlTemplate>
Both ItemViewModel.IsGroupExpanded
setter and getter redirect to Group.IsExpanded
.
Note that Mode=TwoWay
must be specified, since IsExpanded
seems to be bound OneWay
by default.
© 2022 - 2024 — McMap. All rights reserved.