Update ItemsControl when an item in an ObservableCollection is updated
I found the answer using Snoop to debug XAML.
The issue is that you are trying to bind to the ToString() method and that does not raise the PropertyChanged event. If you look at the XAML bindings you will notice that the ObservableCollection is actually changing.
Now look at each item control and it's texts binding in the "Text" property. There are none, it's just text.
To fix this simply add an ItemsControl ItemTemplate with a DataTemplate that contains the elements you'd like to be displayed.
<ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Employees, UpdateSourceTrigger=PropertyChanged}" BorderBrush="Red" BorderThickness="1" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock.Text>
<MultiBinding StringFormat=" {0} is {1} years old">
<Binding Path="Name"/>
<Binding Path="Age"/>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
We now have a green light on binding. RaisePropertyChanged is being called.
Ta-da!
Related videos on Youtube
Frank Liu
dotNet (WPF, WCF) development, C++, ARM Embedded Development
Updated on September 26, 2022Comments
-
Frank Liu over 1 year
The Problem:
- You declare an
ItemsControl
( or a control derived fromItemsControl
) in the view. - You bind the
ItemsControl.ItemsSource
property to anObservableCollection
in your ViewModel. - Your view updates as expected when an item is added to /removed from the
ObservableCollection
. - BUT, the view does not update when you change a property of an item in the
ObservableCollection
.
Background:
It seems that this is a common problem many WPF developers have encountered. It has been asked a few times:
Notify ObservableCollection when Item changes
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
ObservableCollection and Item PropertyChanged
My Implementation:
I tried to implement the accepted solution in Notify ObservableCollection when Item changes. The basic idea is to hook up a
PropertyChanged
handler in your MainWindowViewModel for each item in theObservableCollection
. When an item's property is changed, the event handler will be invoked and somehow the View is updated.I could not get the implementation to work. Here is my implementation.
ViewModels:
class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string propertyName = "") { var handler = PropertyChanged; if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName)); } }
Item ViewModel:
class EmployeeViewModel : ViewModelBase { private int _age; private string _name; public int Age { get { return _age; } set { _age = value; RaisePropertyChanged("Age"); } } public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } } public override string ToString() { return string.Format("{0} is {1} years old", Name, Age); } }
Main Window ViewModel:
class MainWindowViewModel : ViewModelBase { private ObservableCollection<EmployeeViewModel> _collection; public MainWindowViewModel() { _collection = new ObservableCollection<EmployeeViewModel>(); _collection.CollectionChanged += MyItemsSource_CollectionChanged; AddEmployeeCommand = new DelegateCommand(() => AddEmployee()); IncrementEmployeeAgeCommand = new DelegateCommand(() => IncrementEmployeeAge()); } public ObservableCollection<EmployeeViewModel> Employees { get { return _collection; } } public ICommand AddEmployeeCommand { get; set; } public ICommand IncrementEmployeeAgeCommand { get; set; } public void AddEmployee() { _collection.Add(new EmployeeViewModel() { Age = 1, Name = "Random Joe", }); } public void IncrementEmployeeAge() { foreach (var item in _collection) { item.Age++; } } private void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) foreach (EmployeeViewModel item in e.NewItems) item.PropertyChanged += ItemPropertyChanged; if (e.OldItems != null) foreach (EmployeeViewModel item in e.OldItems) item.PropertyChanged -= ItemPropertyChanged; } private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e) { RaisePropertyChanged("Employees"); } }
View:
<Window x:Class="WpfApplication2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero" xmlns:d="clr-namespace:Iress.IosPlus.DynamicOE.Controls" Title="MainWindow" Height="350" Width="350"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.3*"></ColumnDefinition> <ColumnDefinition Width="0.7*"></ColumnDefinition> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0"> <Button Command="{Binding AddEmployeeCommand}">Add Employee</Button> <Button Command="{Binding IncrementEmployeeAgeCommand}">Increment Employee Age</Button> </StackPanel> <Grid Grid.Column="1"> <Grid.RowDefinitions> <RowDefinition Height="0.1*"></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="{Binding Path=Employees[0]}"></TextBlock> <ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Employees}" BorderBrush="Red" BorderThickness="1"></ItemsControl> </Grid> </Grid>
My Results:
To verify my implementation, I create a view like so. The
TextBlock.Text
is bound to the first item in the collection. TheItemsControl
is bound to the collection itself.- Pressing the "Add Employee" button adds an
EmployeeViewModel
object in the collection and both theTextBlock
andItemsControl
are updated as expected. - Pressing the "Add Employee" again, the
ItemsControl
is updated with another entry. Great! - Pressing the "Increment Employee Age" button. The
Age
property of each item is incremented by 1. ThePropertyChanged
event is raised. TheItemPropertyChanged
event handler is invoked. TheTextblock
is updated as expected. However, theItemsControl
is not updated.
I am under the impression that the
ItemsControl
should be updated too when theEmployee.Age
is changed according to the answer in Notify ObservableCollection when Item changes.-
Frank Liu about 9 years@michael, I want the
ItemsControl
to refresh when an item in the collection is updated.
- You declare an
-
Frank Liu about 9 yearsThanks for the reply. I added a setter for the
Employees
property (the ObservableCollection) and called theRaisePropertyChanged
in the setter. But it doesnot trigger theItemsControl
to update when pressing the increment employee age button. -
Frank Liu about 9 yearsGreat answer. I understand using ItemTemplate to bind to the item's property can work around this problem. In fact, if you use the ItemTemplate, we do even need the handle the CollectionChanged event to hook up ItemPropertyChanged handler to each item. I think there might be a way to fix this without using the ItemTemplate. If no other answer is presented. I will mark yours as accepted.
-
Clemens about 9 years@FrankLiu Declaring an ItemTemplate is the standard way of visualizing items in an ItemsControl or a derived control like ListBox. You may want to read the Data Templating Overview article on MSDN.
-
Rachel about 9 yearsGood eye, I didn't even notice OP was relying on the
.ToString()
to display the item! You're correct, the right way to do this would be to create aTemplate
for displaying the item, and bind the UI elements to the properties so they react to change notifications. @FrankLiu If you'd like to avoid defining anItemTemplate
for your ItemsControl, you can also use an Implicit DataTemplate instead. It is a way of telling WPF that anytime it encounters an item of typeEmployeeViewModel
in the Visual Tree, it should render it using the template specified instead of the default .ToString() -
Frank Liu about 9 years@Rachel, I am confused now. If ItemTemplate/DateTemplate is the way to fix the problem. What is the purpose of handling the PropertyChanged event of each item? We dont even need to handle the CollectionChanged event in the
MainWindowViewModel
and all that plumbing. I thought the problem in Notify ObservableCollection when Item changes is same as my problem. -
AzzamAziz about 9 years@FrankLiu I think you're confusing the two and when they should be used. The DataTemplate is there to simply show what you ask XAML to show. It is not there to make sure that when your properties are updated it will update XAML as well. It doesn't care about that. As for the usage of INotifyPropertyChanged, you don't necessarily need it. You only need it when trying to make sure it will change in the UI when someone else modifies that property. Look here.
-
Frank Liu about 9 years@AzzamAziz, I think misinterpreted the intention of Rachel's solution. It just provides a notification to the ViewModel, when an item is updated in the collection. The notification is not for UI to refresh the ItemsControl. Again, your answer is good. There are other ways to fix the problem, such as using DisplayMemberPath to update the view. Or subclass the ObservableCollection to generate a CollectionChanged event when Item PropertyChanged. Anyway, thanks for help everyone.
-
AzzamAziz about 9 yearsI'm not sure how that would work as you are not binding to a property but to a method which is ToString() To my knowledge that does not work. I'm curious as how your other solution may work though? Any links? Glad I could help. :)