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.Amount
updated, 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; }
}
}