Уведомлять ObservableCollection при изменении элемента - PullRequest
36 голосов
/ 13 декабря 2011

Я нашел по этой ссылке

ObservableCollection не замечает, когда элемент в нем изменяется (даже с INotifyPropertyChanged)

некоторые методы, чтобы уведомить Observablecollection о том, что элемент изменился,TrulyObservableCollection в этой ссылке, кажется, то, что я ищу.

public class TrulyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
    public TrulyObservableCollection()
    : base()
    {
        CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
    }

    void TrulyObservableCollection_CollectionChanged(object sender, 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);
    }
}

Но когда я пытаюсь использовать его, я не получаю уведомления о коллекции.Я не уверен, как правильно реализовать это в моем коде C #:

XAML:

    <DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItemsSource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
        <DataGrid.Columns>
            <DataGridCheckBoxColumn Binding="{Binding MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </DataGrid.Columns>
    </DataGrid>

ViewModel:

public class MyViewModel : ViewModelBase
{
    private TrulyObservableCollection<MyType> myItemsSource;
    public TrulyObservableCollection<MyType> MyItemsSource
    {
        get { return myItemsSource; }
        set 
        { 
            myItemsSource = value; 
            // Code to trig on item change...
            RaisePropertyChangedEvent("MyItemsSource");
        }
    }

    public MyViewModel()
    {
        MyItemsSource = new TrulyObservableCollection<MyType>()
        { 
            new MyType() { MyProperty = false },
            new MyType() { MyProperty = true },
            new MyType() { MyProperty = false }
        };
    }
}

public class MyType : ViewModelBase
{
    private bool myProperty;
    public bool MyProperty
    {
        get { return myProperty; }
        set 
        {
            myProperty = value;
            RaisePropertyChangedEvent("MyProperty");
        }
    }
}

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChangedEvent(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
            PropertyChanged(this, e);
        }
    }
}

Когда я запускаю программу, яустановите флажок 3 в false, true, false, как при инициализации свойства.но когда я изменяю состояние одного из ckeckbox, программа проходит через item_PropertyChanged, но никогда в коде свойства MyItemsSource.

Ответы [ 7 ]

77 голосов
/ 13 декабря 2011

Точка, которую вы прокомментировали как // Code to trig on item change..., сработает только тогда, когда объект коллекции изменяется, например, когда он устанавливается на новый объект или устанавливается на нуль.

С вашей текущей реализацией TrulyObservableCollection, чтобы обработать события, измененные свойством вашей коллекции, зарегистрируйте что-то в CollectionChanged событии MyItemsSource

public MyViewModel()
{
    MyItemsSource = new TrulyObservableCollection<MyType>();
    MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;

    MyItemsSource.Add(new MyType() { MyProperty = false });
    MyItemsSource.Add(new MyType() { MyProperty = true});
    MyItemsSource.Add(new MyType() { MyProperty = false });
}


void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    // Handle here
}

Лично мне действительно не нравится эта реализация.Вы вызываете событие CollectionChanged, в котором говорится, что вся коллекция была сброшена при каждом изменении свойства.Конечно, он будет обновлять пользовательский интерфейс каждый раз, когда изменяется элемент в коллекции, но я вижу, что это плохо сказывается на производительности, и, похоже, нет способа определить, какое свойство изменилось, что является одним из ключевых элементов информации.Мне обычно нужно что-то делать на PropertyChanged.

Я предпочитаю использовать обычный ObservableCollection и просто подключать события PropertyChanged к его элементам на CollectionChanged.Если ваш пользовательский интерфейс правильно привязан к элементам в ObservableCollection, вам не нужно указывать обновлению пользовательского интерфейса при изменении свойства элемента в коллекции.

public MyViewModel()
{
    MyItemsSource = new ObservableCollection<MyType>();
    MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;

    MyItemsSource.Add(new MyType() { MyProperty = false });
    MyItemsSource.Add(new MyType() { MyProperty = true});
    MyItemsSource.Add(new MyType() { MyProperty = false });
}

void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems != null)
        foreach(MyType item in e.NewItems)
            item.PropertyChanged += MyType_PropertyChanged;

    if (e.OldItems != null)
        foreach(MyType item in e.OldItems)
            item.PropertyChanged -= MyType_PropertyChanged;
}

void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "MyProperty")
        DoWork();
}
8 голосов
/ 02 августа 2013

Я решил этот случай с помощью статического действия


public class CatalogoModel 
{
    private String _Id;
    private String _Descripcion;
    private Boolean _IsChecked;

    public String Id
    {
        get { return _Id; }
        set { _Id = value; }
    }
    public String Descripcion
    {
        get { return _Descripcion; }
        set { _Descripcion = value; }
    }
    public Boolean IsChecked
    {
        get { return _IsChecked; }
        set
        {
           _IsChecked = value;
            NotifyPropertyChanged("IsChecked");
            OnItemChecked.Invoke();
        }
    }

    public static Action OnItemChecked;
} 

public class ReglaViewModel : ViewModelBase
{
    private ObservableCollection<CatalogoModel> _origenes;

    CatalogoModel.OnItemChecked = () =>
            {
                var x = Origenes.Count;  //Entra cada vez que cambia algo en _origenes
            };
}
2 голосов
/ 13 января 2017

Я знаю, что уже поздно, но, возможно, это поможет другим.Я создал класс NotifyObservableCollection, который решает проблему отсутствия уведомления для самого элемента, когда свойство элемента изменяется.Использование так же просто, как ObservableCollection.

public class NotifyObservableCollection<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);
    }

. Когда элементы добавляются или удаляются, класс перенаправляет событие items PropertyChanged в событие коллекций PropertyChanged.

использование:

public abstract class ParameterBase : INotifyPropertyChanged
{
    protected readonly CultureInfo Ci = new CultureInfo("en-US");
    private string _value;

    public string Value {
        get { return _value; }
        set {
            if (value == _value) return;
            _value = value;
            OnPropertyChanged();
        }
    }
}

public class AItem {
    public NotifyObservableCollection<ParameterBase> Parameters {
        get { return _parameters; }
        set {
            NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
            if (_parameters != null) _parameters.CollectionChanged -= cceh;
            _parameters = value;
            //needed for Binding to AItem at xaml directly
            _parameters.CollectionChanged += cceh; 
        }
    }

    public NotifyObservableCollection<ParameterBase> DefaultParameters {
        get { return _defaultParameters; }
        set {
            NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
            if (_defaultParameters != null) _defaultParameters.CollectionChanged -= cceh;
            _defaultParameters = value;
            //needed for Binding to AItem at xaml directly
            _defaultParameters.CollectionChanged += cceh;
        }
    }


public class MyViewModel {
    public NotifyObservableCollection<AItem> DataItems { get; set; }
}

Если теперь свойство элемента в DataItems изменяется, следующий xaml получит уведомление, хотя он привязывается к Parameters[0] или к самому элементу, за исключением изменяющегося свойства Valueпредмета (конвертеры в триггерах называются надежными при каждом изменении).

<DataGrid CanUserAddRows="False" AutoGenerateColumns="False" ItemsSource="{Binding DataItems}">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding Parameters[0].Value}" Header="P1">
            <DataGridTextColumn.CellStyle>
                <Style TargetType="DataGridCell">
                    <Setter Property="Background" Value="Aqua" />
                    <Style.Triggers>
                        <DataTrigger Value="False">
                            <!-- Bind to Items with changing properties -->
                            <DataTrigger.Binding>
                                <MultiBinding Converter="{StaticResource ParameterCompareConverter}">
                                    <Binding Path="DefaultParameters[0]" />
                                    <Binding Path="Parameters[0]" />
                                </MultiBinding>
                            </DataTrigger.Binding>
                            <Setter Property="Background" Value="DeepPink" />
                        </DataTrigger>
                        <!-- Binds to AItem directly -->
                        <DataTrigger Value="True" Binding="{Binding Converter={StaticResource CheckParametersConverter}}">
                            <Setter Property="FontWeight" Value="ExtraBold" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </DataGridTextColumn.CellStyle>
        </DataGridTextColumn>
2 голосов
/ 10 ноября 2016

Вы можете использовать метод расширения, чтобы получать уведомления об измененном свойстве элемента в коллекции общим способом.

public static class ObservableCollectionExtension
{
    public static void NotifyPropertyChanged<T>(this ObservableCollection<T> observableCollection, Action<T, PropertyChangedEventArgs> callBackAction)
        where T : INotifyPropertyChanged
    {
        observableCollection.CollectionChanged += (sender, args) =>
        {
            //Does not prevent garbage collection says: /217262/ostanovili-li-obrabotchiki-sobytii-sborku-musora
            //publisher.SomeEvent += target.SomeHandler;
            //then "publisher" will keep "target" alive, but "target" will not keep "publisher" alive.
            if (args.NewItems == null) return;
            foreach (T item in args.NewItems)
            {
                item.PropertyChanged += (obj, eventArgs) =>
                {
                    callBackAction((T)obj, eventArgs);
                };
            }
        };
    }
}

public void ExampleUsage()
{
    var myObservableCollection = new ObservableCollection<MyTypeWithNotifyPropertyChanged>();
    myObservableCollection.NotifyPropertyChanged((obj, notifyPropertyChangedEventArgs) =>
    {
        //DO here what you want when a property of an item in the collection has changed.
    });
}
2 голосов
/ 13 декабря 2011

ObservableCollection и его производные вызывают внутренние изменения свойств. Код в вашем установщике должен запускаться только в том случае, если вы присваиваете новое TrulyObservableCollection<MyType> свойству MyItemsSource. То есть это должно произойти только один раз, из конструктора.

С этого момента вы будете получать уведомления об изменении свойств из коллекции, а не от установщика в вашей модели представления.

1 голос
/ 19 июня 2018

Простое решение - использовать BindingList<T> вместо ObservableCollection<T>.Действительно, уведомления об изменении элемента ретрансляции BindingList.Таким образом, с помощью списка привязок, если элемент реализует интерфейс INotifyPropertyChanged, вы можете просто получать уведомления, используя событие ListChanged .

См. Также этот SO ответ.

1 голос
/ 06 августа 2017

Все решения здесь верны, но в них отсутствует важный сценарий, в котором используется метод Clear () , который не обеспечивает OldItems в объекте NotifyCollectionChangedEventArgs.

это идеальный вариант ObservableCollection.

public class ObservableCollectionEX<T> : ObservableCollection<T>
{
    #region Constructors
    public ObservableCollectionEX() : base()
    {
        CollectionChanged += ObservableCollection_CollectionChanged;
    }
    public ObservableCollectionEX(IEnumerable<T> c) : base(c)
    {
        CollectionChanged += ObservableCollection_CollectionChanged;
    }
    public ObservableCollectionEX(List<T> l) : base(l)
    {
        CollectionChanged += ObservableCollection_CollectionChanged;
    }

    #endregion



    public new void Clear()
    {
        foreach (var item in this)
        {
            if (item is INotifyPropertyChanged i)
            {
                if (i != null)
                    i.PropertyChanged -= Element_PropertyChanged;
            }
        }
        base.Clear();
    }
    private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
            foreach (var item in e.OldItems)
            {
                if (item != null && item is INotifyPropertyChanged i)
                {
                    i.PropertyChanged -= Element_PropertyChanged;
                }
            }
        if (e.NewItems != null)
            foreach (var item in e.NewItems)
            {
                if (item != null && item is INotifyPropertyChanged i)
                {
                    i.PropertyChanged -= Element_PropertyChanged;
                    i.PropertyChanged += Element_PropertyChanged;
                }
            }
    }
    private void Element_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        //raise the event
        ItemPropertyChanged?.Invoke(sender, e);
    }


    /// <summary>
    /// the sender is the Item
    /// </summary>
    public PropertyChangedEventHandler ItemPropertyChanged;

}

Вы даже можете пройти лишнюю милю и изменить ItemPropertyChanged, чтобы предоставить список владельцев, подобный этому

Вне класса в некоторыхпространство имен:
public delegate void ListedItemPropertyChangedEventHandler(IList SourceList, object Item, PropertyChangedEventArgs e);

в классе изменить это:

private void Element_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
    //raise the event
    ItemPropertyChanged?.Invoke(this,sender, e);
}
public ListedItemPropertyChangedEventHandler ItemPropertyChanged;  
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...