0

I have a main DataGrid (bound to ObservableCollection<MainItem>) with another sub DataGrid defined in the RowDetailsTemplate (bound to ObservableCollection<SubItem>). The MainItem.Total property is just a derived value that returns SubItems.Sum(s => s.Amount). This works great upon displaying the grids.

Yet, when a value in the Amount column is changed and SubItem.Amountupdated, though the MainItem.Total value is correct (it is just a derived value), the main grid doesn't refresh to show the new value. If I force MainDataGrid.Items.Refresh() (for example, in MainDataGrid_SelectedCellsChanged), the main grid then displays the value now in MainItem.Total. So, this works but it smells since it is such a brute force method refreshing all visible rows in the main grid. [NOTE: This situation is similar to the article Update single row in a WPF Datagrid but I'm already using an ObservableCollection.]

I expect this behavior is because the change occurred in ObservableCollection<SubItem> not in ObservableCollection<MainItem> so it doesn't know about it? But I haven't found an event to raise that informs the main grid that the contents of the bound MainItem.Total has been updated.

[NOTE: The code below is a self-contained sample in case someone wants to build this and try it. It is based on the same situation in my real project.]

XAML:

<Window x:Class="WpfAppDataGrid.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfAppDataGrid"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="100" />
        </Grid.ColumnDefinitions>
        <DataGrid x:Name="MainDataGrid" Grid.Column="0"
                  ItemsSource="{Binding MainItems}"
                  RowDetailsVisibilityMode="VisibleWhenSelected"
                  SelectedCellsChanged="MainDataGrid_SelectedCellsChanged"
                  AutoGenerateColumns="False"
                  CanUserSortColumns="True"
                  CanUserAddRows="True"
                  CanUserDeleteRows="True"
                  RowBackground="AliceBlue"
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch"
                  Margin="10,10,10,10">
            <DataGrid.RowDetailsTemplate>
                <DataTemplate>
                    <DataGrid x:Name="SubDataGrid" ItemsSource="{Binding SubItems}"
                              CellEditEnding="SubDataGrid_CellEditEnding"
                              AutoGenerateColumns="False"
                              CanUserAddRows="True"
                              CanUserDeleteRows="True"
                              RowBackground="Bisque">
                        <DataGrid.Columns>
                            <DataGridTextColumn Header="Candidate" Binding="{Binding Candidate}" />
                            <DataGridTextColumn Header="Primary" Binding="{Binding Primary}" />
                            <DataGridTextColumn Header="Amount" Binding="{Binding Amount, StringFormat=\{0:N2\}}" />
                            <DataGridTextColumn Header="Previous" Binding="{Binding Previous}" />
                            <DataGridTextColumn Header="Party" Binding="{Binding Party}" />
                        </DataGrid.Columns>
                    </DataGrid>
                </DataTemplate>
            </DataGrid.RowDetailsTemplate>
            <DataGrid.Columns>
                <DataGridTextColumn Header="Voter" Binding="{Binding Voter}" />
                <DataGridTemplateColumn Header="Date" Width="100" SortMemberPath="Date" >
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <TextBlock Text="{Binding RecordedDate, StringFormat=\{0:MM/dd/yyyy\}}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <DatePicker SelectedDate="{Binding RecordedDate}" />
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
                <DataGridTextColumn Header="Status" Binding="{Binding Status}" />
                <DataGridTextColumn Header="Auto" Binding="{Binding Total, StringFormat=\{0:N2\}}" IsReadOnly="True" >
                    <DataGridTextColumn.CellStyle>
                        <Style>
                            <Setter Property="TextBlock.TextAlignment" Value="Right" />
                        </Style>
                    </DataGridTextColumn.CellStyle>
                </DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <StackPanel Grid.Column="1" Margin="10,10,10,10">
            <Button Content="Refresh" Click="Button_Click" />
        </StackPanel>
    </Grid>
</Window>

View code-behind:

using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace WpfAppDataGrid
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        enum SubDataGridColumn
        {
            Candidate,
            Primary,
            Amount,
            Previous,
            Party
        }

        private static MainViewModel vm = new MainViewModel();
        private static bool subAmountChanged = false;

        public MainWindow()
        {
            DataContext = vm;
            InitializeComponent();
        }

        private void SubVerifyEdit(SubItem sub, int column, string txt)
        {
            switch ((SubDataGridColumn)column)
            {
                case SubDataGridColumn.Candidate:
                    if (sub.Candidate != txt)
                        sub.Candidate = txt;
                    break;
                case SubDataGridColumn.Primary:
                    if (sub.Primary != txt)
                        sub.Primary = txt;
                    break;
                case SubDataGridColumn.Amount:
                    var amount = decimal.Parse(txt);
                    if (sub.Amount != amount)
                    {
                        sub.Amount = amount;
                        subAmountChanged = true;
                    }
                    break;
                case SubDataGridColumn.Party:
                    if (sub.Party != txt)
                        sub.Primary = txt;
                    break;
                case SubDataGridColumn.Previous:
                    if (sub.Previous != txt)
                        sub.Previous = txt;
                    break;
                default:
                    break;
            }
        }

        private void SubDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
        {
            var sub = (SubItem)e.Row.Item;
            var column = e.Column.DisplayIndex;
            var dep = (DependencyObject)e.EditingElement;
            if (dep is TextBox)
            {
                SubVerifyEdit(sub, column, ((TextBox)dep).Text);
            }
            else if (dep is ComboBox)
            {
                SubVerifyEdit(sub, column, ((ComboBox)dep).SelectedItem.ToString());
            }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            MainDataGrid.Items.Refresh();
        }

        private void MainDataGrid_SelectedCellsChanged(object sender, SelectedCellsChangedEventArgs e)
        {
            if (subAmountChanged)
            {
                //var main = (MainItem)MainDataGrid.CurrentItem;  
                //var sum = main.SubItems.Sum(s => s.Amount);
                //var manual = main.ManualTotal;
                //var auto = main.AutoTotal;

                //var dep = (DependencyProperty)MainDataGrid.CurrentItem;
                //MainDataGrid.CoerceValue(dep);
                MainDataGrid.Items.Refresh();
                subAmountChanged = false;
            }
        }
    }
}

ViewModel:

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;

namespace WpfAppDataGrid
{
    public class MainViewModel : INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;

        public MainViewModel()
        {
            MainItems = new ObservableCollection<MainItem>();

            MainItems.Add(new MainItem("Thomas", DateTime.Now, "Unsure"));
            MainItems[0].SubItems.Add(new SubItem("Roberts", "NH", 27.46m, "Senator", "Republican"));

            MainItems.Add(new MainItem("Arthur", DateTime.Now, "No worries"));
            MainItems[1].SubItems.Add(new SubItem("Johnson", "IA", 47.5m, "Representative", "Republican"));
            MainItems[1].SubItems.Add(new SubItem("Butegieg", "CA", 76.42m, "Senator", "Democrat"));
            MainItems[1].SubItems.Add(new SubItem("Warren", "SC", 14.5m, "Governor", "Democrat"));

            MainItems.Add(new MainItem("Cathy", DateTime.Now, "What now"));
            MainItems[2].SubItems.Add(new SubItem("Biden", "WI", 1456.98m, "Mayor", "Democrat"));

            MainItems.Add(new MainItem("Jonathan", DateTime.Now, "Got this"));
            MainItems[3].SubItems.Add(new SubItem("Foobar", "MI", 5672.3m, "None", "Republican"));
            MainItems[3].SubItems.Add(new SubItem("Sanders", "ME", 1.45m, "Senator", "Democrat"));

            MainItems.Add(new MainItem("Roger", DateTime.Now, "Still undecided"));
            MainItems[4].SubItems.Add(new SubItem("Wakemeyer", "AK", 56m, "Police Chief", "Democrat"));
            MainItems[4].SubItems.Add(new SubItem("Trump", "FL", 982.34m, "Businessman", "Republican"));
        }

        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        private ObservableCollection<MainItem> mainItems;
        public ObservableCollection<MainItem> MainItems
        {
            get { return mainItems; }
            set
            {
                mainItems = value;
                NotifyPropertyChanged();
            }
        }
    }
}

MainItem class:

using System;
using System.Collections.ObjectModel;
using System.Linq;

namespace WpfAppDataGrid
{
    public class MainItem
    {
        public MainItem() { }

        public MainItem(string voter, DateTime date, string status)
        {
            Voter = voter;
            RecordedDate = date;
            Status = status;
        }

        public string Voter { get; set; }
        public DateTime RecordedDate { get; set; }
        public string Status { get; set; }
        public decimal Total { get { return SubItems.Sum(s => s.Amount); } }
        public ObservableCollection<SubItem> SubItems { get; set; } = new ObservableCollection<SubItem>();
    }
}

SubItem class:

namespace WpfAppDataGrid
{
    public class SubItem
    {
        public SubItem() { }

        public SubItem(string candidate, string primary, decimal amount, string previous, string party)
        {
            Candidate = candidate;
            Primary = primary;
            Amount = amount;
            Previous = previous;
            Party = party;
        }

        public string Candidate { get; set; }
        public string Primary { get; set; }
        public decimal Amount { get; set; }
        public string Previous { get; set; }
        public string Party { get; set; }
    }
}
AdvApp
  • 1,094
  • 1
  • 14
  • 27

1 Answers1

1

Both MainItem and SubItem should implement INotifyPropertyChanged.

The latter should raise the PropertyChanged event whenever the Amount property is changed and the MainItem class then needs to subscribe to the PropertyChanged event for all SubItem objects in SubItems and raise the PropertyChanged event for the Total property whenever any of them is changed:

public class MainItem : INotifyPropertyChanged
{
    public MainItem()
    {
        SubItems.CollectionChanged += SubItems_CollectionChanged;
    }

    public MainItem(string voter, DateTime date, string status)
    {
        Voter = voter;
        RecordedDate = date;
        Status = status;
        SubItems.CollectionChanged += SubItems_CollectionChanged;
    }

    private void SubItems_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.NewItems != null)
        {
            foreach (object subItem in e.NewItems)
            {
                (subItem as INotifyPropertyChanged).PropertyChanged
                    += new PropertyChangedEventHandler(item_PropertyChanged);
                item_PropertyChanged(sub, new PropertyChangedEventArgs(nameof(Total)));
            }
        }

        if (e.OldItems != null)
        {
            foreach (object country in e.OldItems)
            {
                item_PropertyChanged(sub, new PropertyChangedEventArgs(nameof(Total)));
                (subItem as INotifyPropertyChanged).PropertyChanged
                    -= new PropertyChangedEventHandler(item_PropertyChanged);
            }
        }
    }

    private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        NotifyPropertyChanged(e.PropertyName);
    }

    public string Voter { get; set; }
    public DateTime RecordedDate { get; set; }
    public string Status { get; set; }
    public decimal Total { get { return SubItems.Sum(s => s.Amount); } }
    public ObservableCollection<SubItem> SubItems { get; set; } = new ObservableCollection<SubItem>();


    public event PropertyChangedEventHandler PropertyChanged;
    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
AdvApp
  • 1,094
  • 1
  • 14
  • 27
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Great answer; thank you. I also updated the code in the answer to include the pieces I found missing. – AdvApp Feb 26 '20 at 17:30