How to compute Group Summary recursively?

Emon Haque 3,176 Reputation points
2021-01-26T12:50:47.387+00:00

I've following data structure in an ICollectionView:

public class GroupedData
{
    public string Level1 { get; set; }
    public string Level2 { get; set; }
    public string Level3 { get; set; }
    public string Level4 { get; set; }
    public int Amount { get; set; }
}

with these Group description:

View = CollectionViewSource.GetDefaultView(data);
View.GroupDescriptions.Add(new PropertyGroupDescription("Level1"));
View.GroupDescriptions.Add(new PropertyGroupDescription("Level2"));
View.GroupDescriptions.Add(new PropertyGroupDescription("Level3"));

With this Converter I get summary for Level2 and Level3:

public class SummaryConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        int sum = 0;
        var group = value as CollectionViewGroup;
        if (group.IsBottomLevel)
            sum = ((IEnumerable<object>)group.Items).OfType<GroupedData>().Sum(x => x.Amount);
        else
            foreach (CollectionViewGroup subGroup in group.Items)
                sum += ((IEnumerable<object>)subGroup.Items).OfType<GroupedData>().Sum(x => x.Amount);
        return sum;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

so for Level1 summary I've replaced the else block with these:

else
    foreach (CollectionViewGroup subGroup in group.Items)
        if (subGroup.IsBottomLevel)
            sum += ((IEnumerable<object>)subGroup.Items).OfType<GroupedData>().Sum(x => x.Amount);
        else foreach (CollectionViewGroup sSubGroup in subGroup.Items)
                sum += ((IEnumerable<object>)sSubGroup.Items).OfType<GroupedData>().Sum(x => x.Amount);

With this approach I'd have to replace the last else block if I'd added one more group description. How to simplify this function?


EDIT

I've added one more level public string Level5 { get; set; } in the data structure and here's the link for the sample app.

Developer technologies Windows Presentation Foundation
Developer technologies C#
{count} votes

Accepted answer
  1. Emon Haque 3,176 Reputation points
    2021-05-26T01:11:53.363+00:00

    This Converter actually works:

    public class SummaryConverter : IValueConverter  
    {  
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)  
        {  
            int sum = 0;  
            var group = value as CollectionViewGroup;  
            if (group.IsBottomLevel)  
                sum = ((IEnumerable<object>)group.Items).OfType<GroupedData>().Sum(x => x.Amount);  
            else foreach (CollectionViewGroup subGroup in group.Items)   
                    sum += (int)Convert(subGroup, null, null, null);  
            return sum;  
        }  
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException();  
    }  
    

    in the else block, I've recursive call to the Convert with subGroup and the result is:

    99661-1.png

    great!


2 additional answers

Sort by: Most helpful
  1. DaisyTian-1203 11,646 Reputation points
    2021-01-29T09:22:13.337+00:00

    After many attempts, I can use switch to replace if else to implement header styles. Below is my updated codes:

     public class HeaderConverter : IValueConverter  
        {  
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)  
            {  
                var group = value as CollectionViewGroup;  
                var header = new TextBlock() { Text = group.Name.ToString(), FontWeight = FontWeights.Bold };  
                Recurisive(group, header,0);  
                return header;  
            }  
            public void Recurisive(CollectionViewGroup group, TextBlock header, int i)  
            {  
                if (group.IsBottomLevel)  
                {  
                    switch (i)  
                    {  
                        case 1:  
                            header.Foreground = Brushes.Blue;  
                            header.FontSize = 12;  
                            i = 0;  
                            break;  
                        case 2:  
                            header.Foreground = Brushes.Green;  
                            header.FontSize = 14;  
                            i = 0;  
                            break;  
                        case 3:  
                            header.Foreground = Brushes.Red;  
                            header.FontSize = 16;  
                            header.Margin = new Thickness(0, 10, 0, 0);  
                            i = 0;  
                            break;  
                    }  
                }  
                else  
                {  
                    i++;  
                    foreach (CollectionViewGroup subgroup in group.Items)  
                    {  
                         
                        Recurisive(subgroup,header,i);  
                          
                    }  
                }  
            }  
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)  
            {  
                throw new NotImplementedException();  
            }  
        }  
    

    If the response is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.

    1 person found this answer helpful.
    0 comments No comments

  2. Emon Haque 3,176 Reputation points
    2021-01-29T14:27:28.573+00:00

    @DaisyTian-MSFT, altogether it's 18 headers so Convert is called 18 times and these 18 Convert calls call your Recursive 33 times. So, just like my original if else in foreach, yours Recursive also sets Foreground and FontSize more times than necessary! One way to reduce is to use break in my original Convert like this:

    public class HeaderConverter : IValueConverter
    {
        int convert = 0;
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var group = value as CollectionViewGroup;
            var header = new TextBlock() { Text = group.Name.ToString(), FontWeight = FontWeights.Bold };
            if ((!group.IsBottomLevel))
            {
                foreach (CollectionViewGroup subGroup in group.Items)
                {
                    if (subGroup.IsBottomLevel)
                    {
                        Debug.WriteLine(++convert);
                        header.Foreground = Brushes.Blue;
                        header.FontSize = 12;
                    }
                    else
                    {
                        foreach (CollectionViewGroup sSubGroup in subGroup.Items)
                        {
                            if (sSubGroup.IsBottomLevel)
                            {
                                Debug.WriteLine(++convert);
                                header.Foreground = Brushes.Green;
                                header.FontSize = 14;
                            }
                            else
                            {
                                Debug.WriteLine(++convert);
                                header.Foreground = Brushes.Red;
                                header.FontSize = 16;
                                header.Margin = new Thickness(0, 10, 0, 0);
                            }
                            break;
                        }
                    }
                    break;
                }
            }
            return header;
        }
    

    BUT still I've to use foreach and if else! It'd be nice If I'd access to the indexes of group levels and could set those styles based on the leve like this:

    public class HeaderConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var group = value as CollectionViewGroup;
            var header = new TextBlock() { Text = group.Name.ToString(), FontWeight = FontWeights.Bold };
            switch (group.Level) // non existent property group.Level
            {
                case 0:
                    header.Foreground = Brushes.Red;
                    header.FontSize = 16;
                    header.Margin = new Thickness(0, 10, 0, 0);
                    break;
                case 1:
                    header.Foreground = Brushes.Green;
                    header.FontSize = 14;
                    break;
                case 2:
                    header.Foreground = Brushes.Blue;
                    header.FontSize = 12;
                    break;
            }
            return header;
        }
    

    or in xaml with a DataTrigger like this:

    <ControlTemplate TargetType="GroupItem">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock x:Name="header" Text="{Binding Name}"/>
            <ItemsPresenter x:Name="items" Grid.Row="1" Margin="15 0 0 0"/>
            <ContentControl x:Name="footer" Grid.Row="2" Content="{Binding Converter={StaticResource FC}}"/>
        </Grid>
        <ControlTemplate.Triggers>
            <DataTrigger Binding="{Binding GroupLevel}" Value="0">
                <Setter Property="TextBlock.FontSize" Value="16" TargetName="header"/>
                <Setter Property="TextBlock.Foreground" Value="Red" TargetName="header"/>
            </DataTrigger>
            <DataTrigger Binding="{Binding GroupLevel}" Value="1"> ... </DataTrigger>
            <DataTrigger Binding="{Binding GroupLevel}" Value="2"> ... </DataTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    
    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.