Как повысить событие PropertyChanged без использования имени строки

Было бы хорошо иметь возможность поднять событие 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));
        }
    }

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

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

Спасибо.

Ответ 1

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

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

Для простоты, я думаю, что я буду поддерживать атрибут атрибута вызывающего абонента для обычных самоизмененных уведомлений. Меньше ввод текста означает меньше шансов на ошибки опечатки и копирование/вставку... компилятор здесь гарантирует, что вы выберете действительный тип/член/переменную, но это не гарантирует, что вы выберете правильный. Просто использовать новый оператор 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; } }
}

Исходный ответ С# 5

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

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

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

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

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

Ответ 2

Обновить. Исходный код не поддерживает 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));
        }
    }

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


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

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.

Ответ 3

В следующем примере вам нужно передать 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");
    }
}

Ответ 4

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

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 );
            }
        }
    }

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

Ответ 5

Так я нашел это:

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.");
    }
 }

Ответ 7

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

Это моя версия generics, которая следует за тем же шаблоном event/delegate, запущенным 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
    }
}

Ответ 8

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

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

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

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