web-dev-qa-db-ja.com

アイテムが変更されたときにObservableCollectionに通知する

このリンクで見つけた

INotifyPropertyChangedであっても)アイテムが変更されても通知されないObservableCollection

アイテムが変更されたことを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);
        }
    }
}

プログラムを実行すると、プロパティの初期化のように、false、true、falseの3つのチェックボックスがあります。しかし、ckeckboxの1つの状態を変更すると、プログラムはitem_PropertyChangedを通過しますが、MyItemsSourceプロパティコードでは実行されません。

38
Pansoul

// Code to trig on item change...としてコメントしたスポットは、新しいオブジェクトに設定されたときやnullに設定されたときなど、コレクションオブジェクトが変更されたときにのみトリガーされます。

TrulyObservableCollectionの現在の実装で、コレクションのプロパティ変更イベントを処理するには、CollectionChangedMyItemsSourceイベントに何かを登録します

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イベントを発生させています。コレクション内のアイテムが変更されるたびにUIが更新されることは確かですが、パフォーマンスが悪いことがわかり、どのプロパティが変更されたかを特定する方法がないようです。これは重要な情報の1つです通常、PropertyChangedで何かをするときに必要です。

通常のObservableCollectionを使用し、PropertyChangedイベントをCollectionChangedのアイテムに接続することを好みます。 UIがObservableCollection内のアイテムに正しくバインドされている場合、コレクション内のアイテムのプロパティが変更されたときに更新するようにUIに指示する必要はありません。

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();
}
79
Rachel

静的アクションを使用してこのケースを解決しました


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
            };
}
9
sat1582

私はそれが遅いことを知っていますが、おそらくこれは他の人を助けるでしょう。クラス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);
    }

アイテムが追加または削除されている間、クラスはアイテム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
WPFGermany

簡単な解決策は、BindingList<T> の代わりに ObservableCollection<T>。実際、BindingListリレーアイテムは通知を変更します。したがって、バインディングリストを使用すると、アイテムがインターフェイスINotifyPropertyChangedを実装している場合、 ListChangedイベント を使用して通知を簡単に取得できます。

this SO answer。

2
Malick

ObservableCollectionとその派生物は、プロパティの変更を内部的に発生させます。セッターのコードは、新しいTrulyObservableCollection<MyType>MyItemsSourceプロパティに。つまり、コンストラクタから一度だけ発生するはずです。

その時点から、ビューモデルのセッターからではなく、コレクションからプロパティ変更通知を取得します。

2
Jay

拡張メソッドを使用して、コレクション内のアイテムのプロパティの変更について一般的な方法で通知を受けることができます。

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: http://stackoverflow.com/questions/298261/do-event-handlers-stop-garbage-collection-from-occuring
            //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
Mike de Klerk

ここのすべての解決策は正しいですが、メソッド Clear() が使用される重要なシナリオがありません。このメソッドはOldItemsオブジェクトにNotifyCollectionChangedEventArgsを提供しません。

これは完璧なObservableCollectionです。

public delegate void ListedItemPropertyChangedEventHandler(IList SourceList, object Item, PropertyChangedEventArgs e);
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)                
                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) => ItemPropertyChanged?.Invoke(this, sender, e);


    public ListedItemPropertyChangedEventHandler ItemPropertyChanged;

}
1
bigworld12