Как вызвать событие PropertyChanged без использования имени строки - PullRequest
39 голосов
/ 07 июля 2010

Было бы хорошо иметь возможность вызывать событие PropertyChanged без явного указания имени измененного свойства.Я хотел бы сделать что-то вроде этого:

    public string MyString
    {
        get { return _myString; }
        set
        {
            ChangePropertyAndNotify<string>(val=>_myString=val, value);
        }
    }

    private void ChangePropertyAndNotify<T>(Action<T> setter, T value)
    {
        setter(value);
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(setter.Method.Name));
        }
    }

В этом случае полученное имя является именем лямбда-метода: "b__0 ".

  1. Могу ли я быть уверен, что обрезка"b__0 "всегда будет указывать правильное имя свойства?
  2. Есть ли какие-либо другие уведомления об изменении свойства (от самого свойства)?

Спасибо.

Ответы [ 8 ]

37 голосов
/ 22 марта 2013

Добавлен ответ C # 6

В C # 6 (и любой версии VB, поставляемой с Visual Studio 2015) у нас есть оператор nameof, который делает вещи проще, чем когда-либо. В моем исходном ответе ниже я использую функцию C # 5 (атрибуты информации о вызывающем абоненте) для обработки общего случая «самоизменяющихся» уведомлений. Оператор nameof может использоваться во всех случаях, и он особенно полезен в сценарии уведомления «related-property-change».

Для простоты, я думаю, я сохраню подход атрибута информации о вызывающем абоненте для общих самоизменяющихся уведомлений. Меньшее количество операций ввода означает меньше шансов на опечатки и ошибки, вызванные копированием / вставкой ... компилятор здесь гарантирует, что вы выбрали правильный тип / член / переменную, но не гарантирует, что вы выбрали правильный. Затем просто использовать новый оператор nameof для уведомлений об изменениях связанных свойств. В приведенном ниже примере демонстрируется ключевое поведение атрибутов информации о вызывающем абоненте ... атрибут не влияет на параметр, если параметр указан вызывающим абонентом (то есть информация о вызывающем абоненте предоставляется для значения параметра, только если параметр опущен вызывающая сторона).

Стоит также отметить, что оператор nameof также может использоваться обработчиками событий PropertyChanged. Теперь вы можете сравнить значение PropertyName в событии (которое является string) с определенным свойством, используя оператор nameof, исключая больше волшебных строк.

Справочная информация для nameof здесь: https://msdn.microsoft.com/en-us/library/dn986596.aspx

Пример:

public class Program
{
    void Main()
    {
        var dm = new DataModel();
        dm.PropertyChanged += propertyChangedHandler;
    }

    void propertyChangedHandler(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == nameof(DataModel.NumberSquared))
        {
            //do something spectacular
        }
    }
}


public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(nameof(this.NumberSquared)); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}

Оригинальный ответ C # 5

Начиная с C # 5, лучше всего использовать атрибуты информации о вызывающем абоненте, это разрешается во время компиляции, не требуется никакого отражения.

Я реализую это в базовом классе, производные классы просто вызывают метод OnPropertyChanged из своих установщиков свойств. Если какое-то свойство неявно меняет другое значение, я могу также использовать «явную» версию метода в установщике свойств, которая затем перестает быть «безопасной», но это редкая ситуация, которую я просто принимаю.

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

Это последнее предложение реализовано ниже (думаю, я начну его использовать!)

public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        OnPropertyChangedExplicit(propertyName);
    }

    protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
    {
        var memberExpression = (MemberExpression)projection.Body;
        OnPropertyChangedExplicit(memberExpression.Member.Name);
    }

    void OnPropertyChangedExplicit(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(() => NumberSquared); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}
29 голосов
/ 07 июля 2010

Обновление : исходный код не совместим с Windows Phone, так как для получения объекта источника события он использует LambdaExpression.Compile (). Вот обновленный метод расширения (также с удаленными проверками параметров):

    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            var expression = body.Expression as ConstantExpression;
            handler(expression.Value, new PropertyChangedEventArgs(body.Member.Name));
        }
    }

Использование остается, как показано ниже.


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

public static class INotifyPropertyChangedHelper
{
    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("'propertyExpression' should be a member expression");

            var expression = body.Expression as ConstantExpression;
            if (expression == null)
                throw new ArgumentException("'propertyExpression' body should be a constant expression");

            object target = Expression.Lambda(expression).Compile().DynamicInvoke();

            var e = new PropertyChangedEventArgs(body.Member.Name);
            handler(target, e);
        }
    }

    public static void Raise<T>(this PropertyChangedEventHandler handler, params Expression<Func<T>>[] propertyExpressions)
    {
        foreach (var propertyExpression in propertyExpressions)
        {
            handler.Raise<T>(propertyExpression);
        }
    }
}

Вот как вы можете использовать этого помощника в своем классе, чтобы вызвать событие для одного или нескольких свойств:

PropertyChanged.Raise(() => this.Now);
PropertyChanged.Raise(() => this.Age, () => this.Weight);

Обратите внимание, что этот помощник также не может использоваться в случае, если PropertyChanged равен null.

6 голосов
/ 07 июля 2010

В следующем примере вы должны передать 3 значения (вспомогательное поле, новое значение, свойство как лямбда), но нет никаких волшебных строк, и событие изменения свойства возникает, только когда оно действительно не равно.

class Sample : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { this.SetProperty(ref _name, value, () => this.Name); }
    }


    protected void SetProperty<T>(ref T backingField, T newValue, Expression<Func<T>> propertyExpression)
    {
        if (backingField == null && newValue == null)
        {
            return;
        }

        if (backingField == null || !backingField.Equals(newValue))
        {
            backingField = newValue;
            this.OnPropertyChanged(propertyExpression);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyExpression.GetPropertyName()));
        }
    }

}

И следующий код содержит методы расширения для получения имени свойства из лямбда-выражения.

public static class Extensions
{
    public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> propertyExpression)
    {
        return propertyExpression.Body.GetMemberExpression().GetPropertyName();
    }

    public static string GetPropertyName(this MemberExpression memberExpression)
    {
        if (memberExpression == null)
        {
            return null;
        }

        if (memberExpression.Member.MemberType != MemberTypes.Property)
        {
            return null;
        }

        var child = memberExpression.Member.Name;
        var parent = GetPropertyName(memberExpression.Expression.GetMemberExpression());

        if (parent == null)
        {
            return child;
        }
        else
        {
            return parent + "." + child;
        }
    }

    public static MemberExpression GetMemberExpression(this Expression expression)
    {
        var memberExpression = expression as MemberExpression;

        if (memberExpression != null)
        {
            return memberExpression;
        }

        var unaryExpression = expression as UnaryExpression;


        if (unaryExpression != null)
        {
            memberExpression = (MemberExpression)unaryExpression.Operand;

            if (memberExpression != null)
            {
                return memberExpression;
            }

        }
        return null;
    }

    public static void ShouldEqual<T>(this T actual, T expected, string name)
    {
        if (!Object.Equals(actual, expected))
        {
            throw new Exception(String.Format("{0}: Expected <{1}> Actual <{2}>.", name, expected, actual));
        }
    }

}

Наконец, несколько тестовых кодов:

class q3191536
{
    public static void Test()
    {
        var sample = new Sample();
        var propertyChanged = 0;

        sample.PropertyChanged += 
            new PropertyChangedEventHandler((sender, e) => 
                {
                    if (e.PropertyName == "Name")
                    {
                        propertyChanged += 1;
                    }
                }
            );

        sample.Name = "Budda";

        sample.Name.ShouldEqual("Budda", "sample.Name");
        propertyChanged.ShouldEqual(1, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");
    }
}
5 голосов
/ 08 июля 2010

Я использую метод расширения

public static class ExpressionExtensions {
    public static string PropertyName<TProperty>(this Expression<Func<TProperty>> projection) {
        var memberExpression = (MemberExpression)projection.Body;

        return memberExpression.Member.Name;
    }
}

в сочетании со следующим методом. Метод определен в классе, который реализует интерфейс INotifyPropertyChanged (обычно базовый класс, из которого получены мои другие классы).

protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection) {
    var e = new PropertyChangedEventArgs(projection.PropertyName());

    OnPropertyChanged(e);
}

Тогда я могу вызвать PropertyChanged-Event следующим образом

private double _rate;
public double Rate {
        get {
            return _rate;
        }
        set {
            if (_rate != value) {
              _rate = value;                     
              OnPropertyChanged(() => Rate );
            }
        }
    }

Используя этот подход, его легко переименовывать свойства (в Visual Studio), потому что он гарантирует, что соответствующий вызов PropertyChanged также обновляется.

2 голосов
/ 28 мая 2013

В уже опубликованных решениях есть две проблемы:
1) Некоторые требуют от вас создания базового класса и наследования от него. Это огромная проблема, которая может вывести из строя цепочку наследования классов и заставить вас начать перепроектирование вашего домена просто для того, чтобы позволить «дополнительную» разработку, подобную этой.
2) Несмотря на то, что существующие решения позволяют вам указать, какое свойство будет запускать измененное событие с помощью лямбда-выражения, они все еще записывают и распространяют строковое представление имени свойства, поскольку они полагаются на существующий класс PropertyChangedEventArgs . Таким образом, любой код, который фактически использует ваше событие PropertyChanged , все равно должен выполнять сравнение строк, что снова нарушает любой автоматический рефакторинг, который вам может понадобиться в будущем, не говоря уже о том, что ваша поддержка времени компиляции выходит за пределы окна, которое одно из основных положений о разрешении лямбда-выражений вместо строк.

Это моя универсальная версия, которая следует тому же шаблону события / делегата, запущенному MS, что означает, что не требуется никаких базовых классов и методов расширения.

public class PropertyChangedEventArgs<TObject> : EventArgs
{
    private readonly MemberInfo _property;

    public PropertyChangedEventArgs(Expression<Func<TObject, object>> expression)
    {
        _property = GetPropertyMember(expression);
    }

    private MemberInfo GetPropertyMember(LambdaExpression p)
    {
        MemberExpression memberExpression;
        if (p.Body is UnaryExpression)
        {
            UnaryExpression ue = (UnaryExpression)p.Body;
            memberExpression = (MemberExpression)ue.Operand;
        }
        else
        {
            memberExpression = (MemberExpression)p.Body;
        }
        return (PropertyInfo)(memberExpression).Member;
    }

    public virtual bool HasChanged(Expression<Func<TObject, object>> expression)
    {
        if (GetPropertyMember(expression) == Property)
            return true;
        return false;
    }

    public virtual MemberInfo Property
    {
        get
        {
            return _property;
        }
    }
}

public delegate void PropertyChangedEventHandler<TObject>(object sender, PropertyChangedEventArgs<TObject> e);

public interface INotifyPropertyChanged<TObject>
{
    event PropertyChangedEventHandler<TObject> PropertyChanged;
}

Теперь вы можете использовать его в таком классе:

public class PagedProduct : INotifyPropertyChanged<PagedProduct>
{
    IPager _pager;

    public event PropertyChangedEventHandler<PagedProduct> PropertyChanged = delegate { };

    public PagedProduct() { }

    public IPager Pager
    {
        get { return _pager; }
        set
        {
            if (value != _pager)
            {
                _pager = value;
                // let everyone know this property has changed.
                PropertyChanged(this, new PropertyChangedEventArgs<PagedProduct>(a => a.Pager));
            }
        }
    }
}

И, наконец, вы можете прослушать события в этом объекте и определить, какое свойство изменилось, также используя лямбда-выражение!

void SomeMethod()
{
    PagedProduct pagedProducts = new PagedProduct();
    pagedProducts.PropertyChanged += pagedProducts_PropertyChanged;
}

void pagedProducts_PropertyChanged(object sender, PropertyChangedEventArgs<PagedProduct> e)
{
    // lambda expression is used to determine if the property we are interested in has changed. no strings here
    if (e.HasChanged(a => a.Pager))
    {
        // do something mind blowing like ordering pizza with a coupon
    }
}
2 голосов
/ 03 мая 2011

Я нашел способ сделать это:

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

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

    public void RaisePropertyChanged(Expression<Func<T, object>> expression)
    {
        var propertyName = GetPropertyFromExpression(expression);

        this.OnPropertyChanged(propertyName);
    }

    public string GetPropertyFromExpression(System.Linq.Expressions.Expression expression)
    {
        if (expression == null)
            throw new ArgumentException("Getting property name form expression is not supported for this type.");

        var lamda = expression as LambdaExpression;
        if (lamda == null)
            throw new NotSupportedException("Getting property name form expression is not supported for this type.");

        var mbe = lamda.Body as MemberExpression;
        if (mbe != null)
            return mbe.Member.Name;

        var unary = lamda.Body as UnaryExpression;
        if (unary != null)
        {
            var member = unary.Operand as MemberExpression;
            if (member != null)
                return member.Member.Name;
        }

        throw new NotSupportedException("Getting property name form expression is not supported for this type.");
    }
 }
1 голос
/ 27 июля 2010

Существует несколько подходов к этому без использования имени свойства.

Лучше всего просто читать блоги.

http://www.pochet.net/blog/2010/06/25/inotifypropertychanged-implementations-an-overview/

http://justinangel.net/AutomagicallyImplementingINotifyPropertyChanged

0 голосов
/ 23 мая 2011

Я использую простой метод расширения, чтобы получить имя свойства, чтобы избежать проблем с волшебными строками. Он также поддерживает удобочитаемость кода, т. Е. Явно указывает, что происходит.

Метод расширения выглядит следующим образом:

public static string GetPropertyName(this MethodBase methodBase)
{
    return methodBase.Name.Substring(4);
}

Это означает, что ваши наборы свойств устойчивы к изменениям имен и выглядят следующим образом:

private string _name;
public string Name
{
    get { return _name; }
    set 
    {
            name = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName()); 
    }
}

Я написал больше об этом методе расширения здесь , и я опубликовал соответствующий фрагмент кода здесь .

...