问题
Since hours I am working on a very hard problem:
How is a DataGrid that is bound to an ObservableCollection correctly updated when another ObservableCollection that is inside the ObservableCollection the DataGrid is bound to changes ?
So far the DataGrid onnly refreshes when i click on the corresponding cell.
I have prepared a complete source code example to illustrate the following (very simple) situation:
There is a ViewModel that holds a List. This List is an ObservableCollection and hold two things: An integer and another List (again an ObservableCollection) that holds four integers. Then there is a DataGrid that has two columns. One column for the integer number and one column for the list of integers. This little application has buttons to modify the integers in the nested list i.e. adding +1 to one of the four integers. The GOAL is that the modification of the nested list is getting reflected by the DataGrid. The PROBLEM so far is, that this only happens on a with an external trigger (e.g. a click on the corresponding cell, or a click on one column header that sorts a column etc.)
So here is the complete code:
This is the ViewModel code that the DataGrid is bound to:
public class ViewModel: INotifyPropertyChanged {
private Items items;
public Items Items {
get { return items; }
set {
items = value;
firePropertyChanged("Items");
}
}
public ViewModel() {
Items = new Items();
}
private void firePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Items: ObservableCollection<Item> {
public Items()
: base() {
this.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
}
private void OnCollectionChanged(object o, NotifyCollectionChangedEventArgs e) {
if (e.NewItems != null) {
foreach (Object item in e.NewItems) {
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null) {
foreach (Object item in e.OldItems) {
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e) {
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
public class List: ObservableCollection<NumberItem> {
public List()
: base() {
this.CollectionChanged += new NotifyCollectionChangedEventHandler(OnCollectionChanged);
}
private void OnCollectionChanged(object o, NotifyCollectionChangedEventArgs e) {
if (e.NewItems != null) {
foreach (Object item in e.NewItems) {
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null) {
foreach (Object item in e.OldItems) {
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e) {
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
public class NumberItem : INotifyPropertyChanged {
private int number;
public int Number {
get { return number; }
set {
number = value;
firePropertyChanged("Number");
}
}
public NumberItem(int i) {
Number = i;
}
private void firePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Item : INotifyPropertyChanged {
private List list;
public List List {
get { return list; }
set {
list = value;
firePropertyChanged("List");
}
}
private int numberOne;
public int NumberOne {
get { return numberOne; }
set {
numberOne = value;
firePropertyChanged("NumberOne");
}
}
private void firePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
/// <summary>
/// This converter simply transforms the list of integers into a string.
/// </summary>
public class Converter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
List l = (List)value;
string s = "";
return s + l[0].Number + " " + l[1].Number + " " + l[2].Number + " " + l[3].Number;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
return null;
}
}
The code of the buttons that manipulate the integers of the nested list is the following:
private void plus1L(object sender, RoutedEventArgs e) {
vm.Items[0].List[0].Number += 1;
}
And finally this is the XAML where the DataGrid is getting bound:
<sdk:DataGrid x:Name="dg" Margin="17,139,21,0" ItemsSource="{Binding Items}" AutoGenerateColumns="False" VerticalAlignment="Top" Height="164" d:LayoutOverrides="Width, HorizontalMargin">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn x:Name="A" Header="A" Binding="{Binding NumberOne}"/>
<sdk:DataGridTextColumn x:Name="List" Header="List" Binding="{Binding List, Converter={StaticResource Converter}}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>*emphasized text*
回答1:
I already told you that you just need to fire change events for the List
property when you change anything about it, it's not that hard...
Edit: In the handler you change the list in some way and you do nothing.
private void plus1L(object sender, RoutedEventArgs e)
{
vm.Items[0].List[0].Number += 1;
vm.Items[0].OnPropertyChanged("List"); // This is needed if you bind to List.
}
To be even more explicit about this: The binding does not care about anything other than the property path you bind to. Everything that happens inside the bound property is unknown to it, so you need to forward internal changes.
回答2:
Why do people insist on creating classes which inherit from ObservableCollection<SomeObject>
? Do they think something is wrong with using an ObservableCollection<Item>
as a data type and using the built-in change notification???
Anyways, do this:
public class SomeViewModel : INotifyPropertyChanged
{
public ObservableCollection<MyItem> OuterCollection { get; set; }
}
public class MyItem : INotifyPropertyChanged
{
public int SomeInt { get; set; }
public ObservableCollection<int> InnerCollection { get; set; }
}
Your XAML can look like normal, however if you change a value in the InnerCollection
, WPF does not know about it because an ObservableCollection
is supposed to monitor the changes to a collection, not the changes to items in the collection.
To update the UI, you'll need to raise a PropertyChange
notification for the InnerCollection
.
myItem.InnerCollection[0]++;
myItem.RaisePropertyChanged("InnerCollection");
If the InnerCollection
contains objects which implement INotifyPropertyChanged
, you can subscribe to their PropertyChanged
events to raise the PropertyChanged
event for InnerCollection
when one of the items changes.
void SomeConstructor()
{
InnerCollection = new ObservableCollection<SomeItem>();
InnerCollection.CollectionChanged += InnerCollection_CollectionChanged;
}
void InnerCollection_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
if (e.NewItems != null)
for each (SomeItem item in e.NewItems)
item.PropertyChanged += SomeItem_PropertyChanged;
if (e.OldItems!= null)
for each (SomeItem item in e.OldItems)
item.PropertyChanged -= SomeItem_PropertyChanged;
}
void SomeItem_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RaisePropertyChanged("InnerCollection");
}
来源:https://stackoverflow.com/questions/9181562/notification-of-datagrid-when-collection-inside-collection-changes