I'm just learning WPF, and ultimately what I'm trying to accomplish is a calculated column in a data grid where the number displayed there is the sum of a particular property in a collection.
After a bit of googling, the approach I decided to take was to use a ValueConverter to do the calculation, but it seems that the number is never updated in the UI. The reading I've done suggests that the PropertyChangedEvent should bubble up and this should just work but it doesn't. I'm missing something, but I don't know what.
I wrote up a simple demo app to show what I'm doing below. The number in the second TextBlock should be 10 before clicking the button (it is), but 6 after clicking, but it stays at 10.
How come? Am I barking up the wrong tree? Is there a better way to do this? Any help would be appreciated.
MainWindow.xaml:
<Window x:Class="TestApp.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:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber, Mode=TwoWay}" />
<TextBlock Text="{Binding ObjFoo.Bars, Converter={StaticResource BarSumConverter}, Mode=TwoWay}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
MainWindow.xaml.cs
namespace TestApp
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public Foo ObjFoo { get; set; }
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
ObjFoo = new Foo();
ObjFoo.Bars.Add(new Bar(5));
ObjFoo.Bars.Add(new Bar(5));
}
private void Button_Click(object sender, RoutedEventArgs e)
{
ObjFoo.Bars[0].ANumber = 1;
}
}
}
Foo.cs
public class Foo
{
public Foo()
{
bars = new ObservableCollection<Bar>();
}
ObservableCollection<Bar> bars;
public ObservableCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged("aNumber");
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
BarSumConverter.cs
public class BarSumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var bars = value as ObservableCollection<Bar>;
if (bars == null) return 0;
decimal total = 0;
foreach (var bar in bars)
{
total += bar.ANumber;
}
return total;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
So it turns out the crux of the issue was that I'd assumed that updating an item inside an ObservableList that implements INotifyPropertyChanged would trigger the CollectionChanged event, but that is not the case. So here's updated code including some of Mario's suggestions that fixes the issue:
MainWindow.xaml:
<Window x:Class="TestApp.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:TestApp"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<local:BarSumConverter x:Key="BarSumConverter" />
</Window.Resources>
<StackPanel>
<TextBlock Text="{Binding ObjFoo.Bars[0].ANumber}" />
<TextBlock Text="{Binding ObjFoo.Total}" />
<Button Content="Click me!" Click="Button_Click" />
</StackPanel>
</Window>
Foo.cs
public class Foo : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Foo()
{
bars = new ObservableItemsCollection<Bar>();
bars.CollectionChanged += OnCollectionChanged;
}
private decimal total;
public decimal Total
{
get { return total; }
private set
{
if (total != value)
{
total = value;
OnPropertyChanged();
}
}
}
ObservableItemsCollection<Bar> bars;
void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
decimal t = 0;
foreach (var bar in bars)
{
t += bar.ANumber;
}
this.Total = t;
}
public ObservableItemsCollection<Bar> Bars
{
get
{
return bars;
}
set { bars = value; }
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Bar.cs
public class Bar : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public Bar(int number)
{
this.ANumber = number;
}
private int aNumber;
public int ANumber
{
get { return aNumber; }
set
{
aNumber = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
ObservableItemsCollection.cs
public class ObservableItemsCollection<T> : ObservableCollection<T>
where T: INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (object t in e.NewItems)
{
((T)t).PropertyChanged += Handle;
}
}
if (e.OldItems != null)
{
foreach (object t in e.OldItems)
{
((T)t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
}