web-dev-qa-db-ja.com

フィールドが別のフィールドに依存しているときにプロパティの変更を通知する最良の方法

setなしでアイテムのフィールドのプロパティが変更されたことを通知するためのc#の最良の方法は何ですか?getは他のフィールドに依存していますか?

例えば ​​:

_public class Example : INotifyPropertyChanged
{
    private MyClass _item;
    public event PropertyChangedEventHandler PropertyChanged;

    public MyClass Item
    {
        get
        {
            return _item;
        }
        protected set
        {
            _item = value;
            OnPropertyChanged("Item");
        }
    }

    public object Field
    {
        get
        {
            return _item.Field;
        }
    }
#if !C#6
    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
#else
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        // => can be called in a set like this: 
        // public MyClass Item { set { _item = value; OnPropertyChanged();} }
        // OnPropertyChanged will be raised for "Item"
    }
#endif
}
_

Itemを設定するときに_"Field"_のPropertyChangedを上げる最良の方法は何ですか? Itemを設定するときに、OnPropertyChanged("Field");を呼び出したかったのですが、フィールドが多いと、コードがすぐに醜くなり、保守できなくなります。

編集:

次のように機能する関数/メソッド/属性があるのでしょうか。

_[DependOn(Item)]
public object Field
{
    get
    {
        return _item.Field;
    }
}
_

=> Itemが変更されると、すべての依存フィールドがプロパティの変更を通知します。

存在しますか?

10
A.Pissicat

1つの方法は、OnPropertyChangedを複数回呼び出すだけです。

public MyClass Item
{
    get
    {
        return _item;
    }
    protected set
    {
        _item = value;
        OnPropertyChanged("Item");
        OnPropertyChanged("Field");
    }
}

ただし、これはあまり保守可能ではありません。別のオプションは、取得専用プロパティにセッターを追加し、他のプロパティから設定することです。

public MyClass Item
{
    get
    {
        return _item;
    }
    protected set
    {
        _item = value;
        OnPropertyChanged("Item");
        Field = _item.Field;
    }
}

public object Field
{
    get
    {
        return _field;
    }
    private set
    {
        _field = value;
        OnPropertyChanged("Field");
    }
}

属性を使用してプロパティ間のこの関係を示すための組み込みのメカニズムはありませんが、それを行うヘルパークラスを作成することは可能です。

私はここにどのように見えるかの非常に基本的な例を作りました:

[AttributeUsage( AttributeTargets.Property )]
public class DepondsOnAttribute : Attribute
{
    public DepondsOnAttribute( string name )
    {
        Name = name;
    }

    public string Name { get; }
}

public class PropertyChangedNotifier<T> : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public PropertyChangedNotifier( T owner )
    {
        mOwner = owner;
    }

    public void OnPropertyChanged( string propertyName )
    {
        var handler = PropertyChanged;
        if( handler != null ) handler( mOwner, new PropertyChangedEventArgs( propertyName ) );

        List<string> dependents;
        if( smPropertyDependencies.TryGetValue( propertyName, out dependents ) )
        {
            foreach( var dependent in dependents ) OnPropertyChanged( dependent );
        }
    }

    static PropertyChangedNotifier()
    {
        foreach( var property in typeof( T ).GetProperties() )
        {
            var dependsOn = property.GetCustomAttributes( true )
                                    .OfType<DepondsOnAttribute>()
                                    .Select( attribute => attribute.Name );

            foreach( var dependency in dependsOn )
            {
                List<string> list;
                if( !smPropertyDependencies.TryGetValue( dependency, out list ) )
                {
                    list = new List<string>();
                    smPropertyDependencies.Add( dependency, list );
                }

                if (property.Name == dependency)
                    throw new ApplicationException(String.Format("Property {0} of {1} cannot depends of itself", dependency, typeof(T).ToString()));

                list.Add( property.Name );
            }
        }
    }

    private static readonly Dictionary<string, List<string>> smPropertyDependencies = new Dictionary<string, List<string>>();

    private readonly T mOwner;
}

これはそれほど堅牢ではありません(たとえば、プロパティ間に循環依存関係を作成すると、変更されたプロパティが無限の再帰状況でスタックする可能性があります)。一部の.NET 4.5およびC#6の機能を使用して簡単にすることもできますが、読者のための演習としてすべて残しておきます。それはおそらく継承もうまく処理しません。

このクラスを使用するには:

public class Example : INotifyPropertyChanged
{
    private MyClass _item;
    private PropertyChangedNotifier<Example> _notifier;

    public Example()
    {
        _notifier = new PropertyChangedNotifier<Example>( this );
    }

    public event PropertyChangedEventHandler PropertyChanged
    {
        add { _notifier.PropertyChanged += value; }
        remove { _notifier.PropertyChanged -= value; }
    }

    public MyClass Item
    {
        get
        {
            return _item;
        }
        protected set
        {
            _item = value;
            OnPropertyChanged("Item");
        }
    }

    [DependsOn( "Item" )]
    public object Field
    {
        get
        {
            return _item.Field;
        }
    }
    protected void OnPropertyChanged(string propertyName)
    {
        _notifier.OnPropertyChanged( propertyName );
    }
}
7
Kyle

私の知る限り、そのための組み込みメソッドはありません。私は通常このようにします:

_public class Foo : INotifyPropertyChanged
{
    private Bar _bar1;

    public Bar Item
    {
        get { return _bar1; }
        set 
        { 
             SetField(ref _bar1, value); 
             ItemChanged();
        }
    }

    public string MyString
    {
        get { return _bar1.Item; }
    }

    private void ItemChanged()
    {
        OnPropertyChanged("MyString");
    }
}

public class Bar
{
    public string Item { get; set; }
}
_

この方法では、プロパティ内に通知ロジックはありません。私の意見では、この方法のほうが保守しやすく、メソッドが何をするかは明らかです。

また、私はクラスのハードコードされた名前の代わりにSOのどこかで見つけたこのメソッドを使用することを好みます(プロパティの名前が変更されると、壊れます)。

OnPropertyChanged("MyString");OnPropertyChanged(GetPropertyName(() => MyString));になります

ここで、GetPropertyNameは次のとおりです。

_public static string GetPropertyName<T>(Expression<Func<T>> propertyLambda)
{
    if (propertyLambda == null) throw new ArgumentNullException("propertyLambda");

    var me = propertyLambda.Body as MemberExpression;

    if (me == null)
    {
        throw new ArgumentException("You must pass a lambda of the form: '() => Class.Property' or '() => object.Property'");
    }

    return me.Member.Name;
}
_

この後、プロパティを名前として変更するたびに、ハードコードされた文字列値を検索する代わりに、GetPropertyNameがあるすべての場所でプロパティの名前を変更する必要があります。

依存関係を組み込む組み込みの方法にも興味があるので、そこにお気に入りを入れます:)

2
Etienne Faucher

このソリューションでは、イベントはまだセッターから伝播されます(そのため、質問の内容は正確ではありません)が、依存関係を表すための、より管理しやすい方法を提供します。誰かが便利だと思うかもしれません。

解決策は、INotifyPropertyChangedイベントをトリガーするためのカスタムラッパーを作成することです。 OnPropertyChangedを手動で呼び出す代わりに、次の算術式を定義できます(できれば、後で再利用する基本クラス内にあります)。

public abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    internal void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected ViewModelPropertyChange SetPropertyValue<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
    {
        property = value;
        OnPropertyChanged(propertyName);

        return new ViewModelPropertyChange(this);
    }
}

このクラスは、呼び出し元のプロパーティーの名前を提供する必要なしに、特定のフィールドの値を設定する方法を提供します。

また、依存プロパティの定義に使用できるクラスを定義する必要があります(このクラスのインスタンスはSetPropertyValue mathodから返されます)。

public class ViewModelPropertyChange
{
    private readonly ViewModelBase _viewModel;

    public ViewModelPropertyChange(ViewModelBase viewModel)
    {
        _viewModel = viewModel;
    }

    public ViewModelPropertyChange WithDependent(string name)
    {
        _viewModel.OnPropertyChanged(name);

        return this;
    }
}

変更されているオブジェクトへの参照を格納するだけで、イベントを次のプロパティに伝達できます。

これにより、次のようにViewModelBaseから派生したクラスを作成できます。

class OurViewModel : ViewModelBase
{
    private int _partOne;
    public int PartOne
    {
        get => _partOne;
        set => SetPropertyValue(ref _partOne, value)
            .WithDependent(nameof(Total));
    }

    private int _partTwo;
    public int PartTwo
    {
        get => _partTwo;
        set => SetPropertyValue(ref _partTwo, value)
            .WithDependent(nameof(Total))
            .WithDependent(nameof(PartTwoPlus2));
    }

    public int Total {
        get => PartOne + PartTwo;
    }

    public int PartTwoPlus2 {
        get => PartTwo + 2;
    }
}
1
Tomasz