1

I have a datagrid with grouped items created via CollectionViewGroup class. For each group of items I calculate the sum of the items of this group and then the total sum of all groups. The user can change value of a sum of one of groups. The total sum has to be changed as well, but it doesn't. I suppose that the problem is that the qualifier of the event OnPropertyChanged is protected, but I'm not sure.

My code is:

public MainWindow()
    {
        InitializeComponent();
        var lv = new ObservableCollection<LPosition>();
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "1", OZ2 = "1" });
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "1", OZ2 = "2" });
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "1", OZ2 = "2" });
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "2", OZ2 = "1" });
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "2", OZ2 = "1" });
        lv.positions.Add(new LPosition() { GB = 5, OZ1 = "2", OZ2 = "2" });
        myGrid.DataContext = lv;

in XAML:

<Window.Resources>
    <local:GroupSumConverter x:Key="groupSumConverter"/>
    <CollectionViewSource x:Key="groupedItems" Source="{Binding ElementName=myGrid, Path=DataContext.positions}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="OZ1"/>
            <PropertyGroupDescription PropertyName="OZ2"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>

    <Style x:Key="FirstLevelGroupStyle" TargetType="{x:Type GroupItem}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type GroupItem}">
                    <Border BorderThickness="2" BorderBrush="DarkGray" Margin="0,1">
                        <StackPanel>
                            <Expander>
                                <Expander.Header>
                                    <StackPanel Orientation="Horizontal">
                                        <TextBlock FontWeight="Bold" x:Name="TextBlock_SecondLvlSum">
                                            <TextBlock.Text>
                                                <MultiBinding StringFormat="{}{0} - {1}">
                                                    <Binding Path="Name"/>
                                                    <Binding Path="myGrid.DataContext"/>
                                                </MultiBinding>
                                            </TextBlock.Text>
                                        </TextBlock>
                                    </StackPanel>
                                </Expander.Header>
                                <ItemsPresenter/>
                            </Expander>
                            <TextBlock TextAlignment="Right" FontWeight="Bold">
                                <TextBlock.Text>
                                    <MultiBinding x:Name="totalSumConverter" Converter="{StaticResource groupSumConverter}" StringFormat="Summe: {0:N2}" ConverterCulture='de-DE'>
                                        <Binding Path="Items"/>
                                        <Binding />
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>
                        </StackPanel>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="SecondLevelGroupStyle" TargetType="{x:Type GroupItem}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type GroupItem}">
                    <StackPanel>
                        <ItemsPresenter>
                            <ItemsPresenter.Style>
                                <Style TargetType="ItemsPresenter">
                                    <Setter Property="Visibility" Value="Collapsed"/>
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Name}" Value="">
                                            <Setter Property="Visibility" Value="Visible"></Setter>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </ItemsPresenter.Style>
                        </ItemsPresenter>
                        <Border BorderThickness="2" BorderBrush="DarkGray" Margin="0,1" Background="Aquamarine">
                            <Border.Style>
                                <Style TargetType="Border">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Name}" Value="">
                                            <Setter Property="Visibility" Value="Collapsed"></Setter>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Border.Style>
                            <StackPanel>
                                <Expander>
                                    <Expander.Header>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock FontWeight="Bold">
                                                <TextBlock.Text>
                                                    <MultiBinding StringFormat="{}{0} - {1}">
                                                        <Binding Path="Name"/>
                                                        <Binding Path="Items[0].kurztext"/>
                                                    </MultiBinding>
                                                </TextBlock.Text>
                                            </TextBlock>
                                        </StackPanel>
                                    </Expander.Header>
                                    <ItemsPresenter/>
                                </Expander>
                                <TextBlock TextAlignment="Right" FontWeight="Bold">
                                    <TextBlock.Text>
                                        <MultiBinding Converter="{StaticResource groupSumConverter}" StringFormat="Summe: {0:N2}" ConverterCulture='de-DE'>
                                            <Binding Path="Items"/>
                                            <Binding Path="Items[0].GB"/>
                                        </MultiBinding>
                                    </TextBlock.Text>
                                </TextBlock>
                            </StackPanel>
                        </Border>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

    <DataGrid AutoGenerateColumns="False"
          x:Name="myGrid"
          ItemsSource="{Binding Source={StaticResource groupedItems}}">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding OZ1}" Header="OZ1"/>
            <DataGridTextColumn Binding="{Binding OZ2}" Header="OZ2"/>
            <DataGridTextColumn Binding="{Binding GB}" Header="GB"/>
        </DataGrid.Columns>

        <DataGrid.GroupStyle>
            <GroupStyle ContainerStyle="{StaticResource FirstLevelGroupStyle}">
                <GroupStyle.Panel>
                    <ItemsPanelTemplate>
                        <DataGridRowsPresenter/>
                    </ItemsPanelTemplate>
                </GroupStyle.Panel>
            </GroupStyle>
            <GroupStyle ContainerStyle="{StaticResource SecondLevelGroupStyle}">
                <GroupStyle.Panel>
                    <ItemsPanelTemplate>
                        <DataGridRowsPresenter/>
                    </ItemsPanelTemplate>
                </GroupStyle.Panel>
            </GroupStyle>
        </DataGrid.GroupStyle>
    </DataGrid>

The corresponding converter:

class GroupSumConverter:IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var lPosContainer = (ReadOnlyObservableCollection<Object>)values[0];
        double sum = 0.0;

        if (lPosContainer[0] != null && lPosContainer[0] is CollectionViewGroup)
        {
            foreach (CollectionViewGroup group in lPosContainer)
            {
                sum += sumPositions((ReadOnlyObservableCollection<Object>)group.Items);
            }
        }
        else
        {
            sum += sumPositions(lPosContainer);
        }

        return sum;
    }

    private double sumPositions(ReadOnlyObservableCollection<Object> items)
    {
        double sum = 0.0;
        foreach (LPosition lPos in items)
        {
            sum += lPos.GB;
        }
        return sum;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And the LPosition class is:

class LPosition:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private void OnPropertyChanged(string propName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }
    private double _GB;

    public double GB
    {
        get { return _GB; }
        set { _GB = value;
        OnPropertyChanged("GB");
        }
    }

    public string OZ1 { get; set; }
    public string OZ2 { get; set; }
}

Any suggestions how to solve this problem and help would be appreciated!

EDIT 1:
I have tried to solve the problem according to this question. I've used a class TrulyObservableCollection. The problem now is, that collection notices when item in it changes, but the Converter is not called after the item has been changed.
EDIT 2: I have added screenshots so it's clear what the problem is:

The sum is correct
The sum is not correct

EDIT 3: I have tried the solution of @Estacado7706, but there is still one problem remaining.

It works for all rows, except the ones that are the last of a group. The problem seems to be, that the change of the index is handeled in another way in this case. The SelectedIndex is set to -1 because there is no next following item to select and therefore the converter is called (works as expected so far).

The problem is, that the last value of the collection is not passed in this case. @Estacado7706 has mentioned this and posted a workaround. But this workaround does not work for me: The index is changed to 0 and so the converter is called again, but the edited item is still not passed.

@Estacado7706 has mentioned in a workaround to add a selectedItemchanged event to the DataGrid. There is no such event (at least I didn't find one), so I tried SelectionChanged event, as well as SelecedCellsChanged event, but both seem to be called before the commit.

I have also tried to avoid this by commiting the item manually by using

myGrid.CommitEdit();
myGrid.Items.Refresh();

This works and refreshes the sum, but the disadvantage is, that all expanders are closed (because of the Items.Refresh()), which is not very user-friendly.

Community
  • 1
  • 1
java
  • 47
  • 7

2 Answers2

1

Your main problem should be, that, by binding to the collection you only update the values, if the itemset is changed. Eg: new items are added. That's the reason for correct values on startup and no change at all after that. The funny thing: It works, as long as you only have one item within a second level group, because then editing the item is the same as editing the collection.

Here is a little hack that should do the trick.

In your first level group:

<TextBlock>
                                <TextBlock.Text>
                                    <MultiBinding Converter="{StaticResource mySumConverter}" StringFormat="Summe: {0:N2}" ConverterCulture='de-DE'>
                                        <Binding ElementName="myGrid" Path="SelectedItem"/>
                                        <Binding Path="Items"/>
                                    <!--The second value is only used that the Text is updated (as Items itself has no INotifyPropertyChanged).
                                                TODO: This is no really a nice way of doing this, but it works. Has to be improved if someone finds a better solution-->
                                    </MultiBinding>
                                </TextBlock.Text>
                            </TextBlock>

the "new" converter (pretty much the same):

class MySumConverter:IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        double sum = 0.0;

        var lPosContainer = (ReadOnlyObservableCollection<Object>)values[1];

        if (lPosContainer[0] != null && lPosContainer[0] is CollectionViewGroup)
        {
            foreach (CollectionViewGroup group in lPosContainer)
            {
                sum += sumPositions((ReadOnlyObservableCollection<Object>)group.Items);
            }
        }
        else
        {
            sum += sumPositions(lPosContainer);
        }

        return sum;
    }

    private double sumPositions(ReadOnlyObservableCollection<Object> items)
    {
        double sum = 0.0;
        foreach (LPosition lPos in items)
        {
            sum += lPos.GB;
        }
        return sum;
    }

After that you will have the situation, that your sum is only correct, when you klick any item, but after editing your sum will represent the real sum minus the recently edited value. The reason: the collection is passed without the value...(Don't ask me why) A workaround for this:

Add a "selectedItemChanged" event to your grid:

if (myGrid.SelectedIndex < 1 && myGrid.Items.Count > 0)
            myGrid.SelectedIndex = 0;

When hitting enter your last cell is not selected anymore (in your case, even the first level group closes, when only one item is present). Therefore this method is called. It will change the selectedIndex, thereby causing the sums to be updated and that's it.

Not really nice (rather ugly as hell), but working.

0

That should be fairly easy to fix. After pressing enter you travel to the next row. If you are at the end of a group you'll select the first element of the next group, if it is expanded. If that is not the case there is nothing to select and you end up with -1.

Just create a custom grid, that inherits from DataGrid, override OnKEyDown and OnPreviewKeyDown to work as intended with everything except Key.Return. If this is presses, just leave the editmode, but don't go any further.

Second possibility: change the way the validation template does the work to stay in the same row after validating. I've seen a few of those template, but I couldn't find it within the last minute to link it here.