Автоматически INotifyPropertyChanged - PullRequest
57 голосов
/ 09 февраля 2009

Есть ли способ автоматически получать уведомления об изменениях свойств в классе без необходимости писать OnPropertyChanged в каждом сеттере? (У меня есть сотни свойств, которые я хочу знать, изменились ли они).


Антон предлагает динамические прокси . Я фактически использовал библиотеку «Castle» для чего-то похожего в прошлом, и хотя это действительно уменьшает объем кода, который я должен был написать, он прибавил около 30 секунд ко времени запуска моей программы (ymmv) - потому что это решение во время выполнения.

Мне интересно, есть ли решение времени компиляции, возможно, с использованием атрибутов времени компиляции ...


Slashene и TcKs дают предложения, которые генерируют повторяющийся код - к сожалению, не все мои свойства являются простым случаем m_Value = value - многие из них имеют собственный код в установщиках, поэтому код cookie-cutter из фрагментов и xml не действительно выполнимо для моего проекта.

Ответы [ 13 ]

43 голосов
/ 01 ноября 2011

РЕДАКТИРОВАТЬ: Автор NotifyPropertyWeaver устарел инструмент в пользу более общего Fody . (Руководство по миграции для людей, переходящих от ткачей к фоде.)


Очень удобный инструмент, который я использовал для своих проектов: Уведомить Уивера о недвижимости Fody .

Он устанавливается как шаг сборки в ваших проектах и ​​во время компиляции внедряет код, который вызывает событие PropertyChanged.

Повышение свойств. PropertyChanged выполняется путем установки на них специальных атрибутов :

[ImplementPropertyChanged]
public string MyProperty { get; set; }

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

[ImplementPropertyChanged]
public double Radius { get; set; }

[DependsOn("Radius")]
public double Area 
{
    get { return Radius * Radius * Math.PI; }
}
39 голосов
/ 09 февраля 2009

Оператор nameof был реализован в C # 6.0 с .NET 4.6 и VS2015 в июле 2015 года. Следующее все еще действует для C # <6.0 </h2> Мы используем код ниже (с http://www.ingebrigtsen.info/post/2008/12/11/INotifyPropertyChanged-revisited.aspx). Прекрасно работает:) public static class NotificationExtensions { #region Delegates /// <summary> /// A property changed handler without the property name. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="sender">The object that raised the event.</param> public delegate void PropertyChangedHandler<TSender>(TSender sender); #endregion /// <summary> /// Notifies listeners about a change. /// </summary> /// <param name="EventHandler">The event to raise.</param> /// <param name="Property">The property that changed.</param> public static void Notify(this PropertyChangedEventHandler EventHandler, Expression<Func<object>> Property) { // Check for null if (EventHandler == null) return; // Get property name var lambda = Property as LambdaExpression; MemberExpression memberExpression; if (lambda.Body is UnaryExpression) { var unaryExpression = lambda.Body as UnaryExpression; memberExpression = unaryExpression.Operand as MemberExpression; } else { memberExpression = lambda.Body as MemberExpression; } ConstantExpression constantExpression; if (memberExpression.Expression is UnaryExpression) { var unaryExpression = memberExpression.Expression as UnaryExpression; constantExpression = unaryExpression.Operand as ConstantExpression; } else { constantExpression = memberExpression.Expression as ConstantExpression; } var propertyInfo = memberExpression.Member as PropertyInfo; // Invoke event foreach (Delegate del in EventHandler.GetInvocationList()) { del.DynamicInvoke(new[] { constantExpression.Value, new PropertyChangedEventArgs(propertyInfo.Name) }); } } /// <summary> /// Subscribe to changes in an object implementing INotifiyPropertyChanged. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="ObjectThatNotifies">The object you are interested in.</param> /// <param name="Property">The property you are interested in.</param> /// <param name="Handler">The delegate that will handle the event.</param> public static void SubscribeToChange<T>(this T ObjectThatNotifies, Expression<Func<object>> Property, PropertyChangedHandler<T> Handler) where T : INotifyPropertyChanged { // Add a new PropertyChangedEventHandler ObjectThatNotifies.PropertyChanged += (s, e) => { // Get name of Property var lambda = Property as LambdaExpression; MemberExpression memberExpression; if (lambda.Body is UnaryExpression) { var unaryExpression = lambda.Body as UnaryExpression; memberExpression = unaryExpression.Operand as MemberExpression; } else { memberExpression = lambda.Body as MemberExpression; } var propertyInfo = memberExpression.Member as PropertyInfo; // Notify handler if PropertyName is the one we were interested in if (e.PropertyName.Equals(propertyInfo.Name)) { Handler(ObjectThatNotifies); } }; } } Используется, например, таким образом: public class Employee : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _firstName; public string FirstName { get { return this._firstName; } set { this._firstName = value; this.PropertyChanged.Notify(()=>this.FirstName); } } } private void firstName_PropertyChanged(Employee sender) { Console.WriteLine(sender.FirstName); } employee = new Employee(); employee.SubscribeToChange(() => employee.FirstName, firstName_PropertyChanged); Некоторые синтаксические ошибки в примере могут существовать. Не проверял это. Но у вас должна быть хотя бы концепция:) РЕДАКТИРОВАТЬ: Теперь я вижу, что вы, возможно, хотели еще меньше работы, но да ... материал выше по крайней мере делает это намного проще. И вы предотвращаете все страшные проблемы со ссылками на свойства, используя строки.

30 голосов
/ 13 сентября 2012

Framework 4.5 предоставляет нам CallerMemberNameAttribute, что делает ненужной передачу имени свойства в виде строки:

private string m_myProperty;
public string MyProperty
{
    get { return m_myProperty; }
    set
    {
        m_myProperty = value;
        OnPropertyChanged();
    }
}

private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
    // ... do stuff here ...
}

Похоже на решение Svish, просто заменяя лямбда-изумление скучной функциональностью фреймворка; -)

Если вы работаете в Framework 4.0 с KB2468871 , вы можете установить Microsoft BCL Compatibility Pack через nuget , который также предоставляет этот атрибут.

11 голосов
/ 30 мая 2012

Вы можете иметь метод расширения для вашего делегата PropertyChanged и использовать его следующим образом:

public string Name
{
    get { return name; }
    set
    {
        name = value;
        PropertyChanged.Raise(() => Name);
    }
}

Подписка на конкретное изменение свойства:

var obj = new Employee();

var handler = obj.SubscribeToPropertyChanged(
    o => o.FirstName, 
    o => Console.WriteLine("FirstName is now '{0}'", o.FirstName));

obj.FirstName = "abc";

// Unsubscribe when required
obj.PropertyChanged -= handler;

метод расширения может определить имя отправителя и свойства, просто проверив дерево лямбда-выражений и без существенного влияния на производительность public static class PropertyChangedExtensions { public static void Raise<TProperty>( this PropertyChangedEventHandler handler, Expression<Func<TProperty>> property) { if (handler == null) return; var memberExpr = (MemberExpression)property.Body; var propertyName = memberExpr.Member.Name; var sender = ((ConstantExpression)memberExpr.Expression).Value; handler.Invoke(sender, new PropertyChangedEventArgs(propertyName)); } public static PropertyChangedEventHandler SubscribeToPropertyChanged<T, TProperty>( this T obj, Expression<Func<T, TProperty>> property, Action<T> handler) where T : INotifyPropertyChanged { if (handler == null) return null; var memberExpr = (MemberExpression)property.Body; var propertyName = memberExpr.Member.Name; PropertyChangedEventHandler subscription = (sender, eventArgs) => { if (propertyName == eventArgs.PropertyName) handler(obj); }; obj.PropertyChanged += subscription; return subscription; } } Если событие PropertyChanged объявлено в базовом типе, оно не будет отображаться как поле делегата в производных классах. В этом случае обходным путем является объявление защищенного поля типа PropertyChangedEventHandler и явная реализация методов доступа add и remove события:

public class Base : INotifyPropertyChanged
{
    protected PropertyChangedEventHandler propertyChanged;
    public event PropertyChangedEventHandler PropertyChanged
    {
        add { propertyChanged += value; }
        remove { propertyChanged -= value; }
    }
}

public class Derived : Base
{
    string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            propertyChanged.Raise(() => Name);
        }
    }
}
10 голосов
/ 09 февраля 2009

Реализация безопасного типа INotifyPropertyChanged: Смотрите здесь

Затем создайте свой собственный фрагмент кода:

private $Type$ _$PropertyName$;
public $Type$ $PropertyName$
{
    get
    {
        return _$PropertyName$;
    }
    set
    {
        if(value != _$PropertyName$)
        {
            _$PropertyName$ = value;
            OnPropertyChanged(o => o.$PropertyName$);               
        }
    }
}

С Дизайнером фрагмента кода и все готово! Простой, безопасный способ построить свой INotifyPropertyChanged.

4 голосов
/ 17 мая 2010

Возможно, вы захотите взглянуть на Аспектно-ориентированное программирование в целом

Frameworks => вы можете посмотреть на linfu

4 голосов
/ 09 февраля 2009

Я не знаю стандартного способа, но я знаю два обходных пути:

1) PostSharp может сделать это за вас после компиляции. Это очень полезно, но на каждую сборку уходит некоторое время.

2) Пользовательский инструмент в Visual Studio. Вы можете комбинировать это с «частичным классом». Затем вы можете создать собственный инструмент для вашего XML и сгенерировать исходный код из XML.

Например, этот xml:

<type scope="public" type="class" name="MyClass">
    <property scope="public" type="string" modifier="virtual" name="Text" notify="true" />
</type>

может быть источником для этого кода:

public partial class MyClass {
    private string _text;
    public virtual string Text {
        get { return this._Text; }
        set {
            this.OnPropertyChanging( "Text" );
            this._Text = value;
            this.OnPropertyChanged( "Text" );
        }
    }
}
2 голосов
/ 01 июня 2011

Я только что нашел ActiveSharp - автоматический INotifyPropertyChanged , я еще не использовал его, но выглядит хорошо.

Цитирую со своего сайта ...


Отправка уведомлений об изменении свойства без указания имени свойства как строка.

Вместо этого напишите свойства как это:

public int Foo
{
    get { return _foo; }
    set { SetValue(ref _foo, value); }  // <-- no property name here
}

Обратите внимание, что нет необходимости включать имя свойства в виде строки. ActiveSharp надежно и правильно вычисляет это для себя. Он работает на основе того факта, что ваша реализация свойства передает поле поддержки (_foo) по ссылке. (ActiveSharp использует этот вызов «по ссылке», чтобы определить, какое поле поддержки было передано, и из поля оно идентифицирует свойство).

2 голосов
/ 09 февраля 2009

Вы могли бы взглянуть на Castle или Spring.NET и реализовать функции перехватчика?

1 голос
/ 04 июня 2013

Не существует единой реализации Property Changed, которая могла бы обрабатывать все способы, которыми люди хотят ее использовать. Лучше всего создать вспомогательный класс, который сделает всю работу за вас. Вот пример того, что я использую

/// <summary>
/// Helper Class that automates most of the actions required to implement INotifyPropertyChanged
/// </summary>
public static class HPropertyChanged
{
    private static Dictionary<string, PropertyChangedEventArgs> argslookup = new Dictionary<string, PropertyChangedEventArgs>();
    public static string ThisPropertyName([CallerMemberName]string name = "")
    {
        return name;
    }

    public static string GetPropertyName<T>(Expression<Func<T>> exp)
    {
        string rtn = "";
        MemberExpression mex = exp.Body as MemberExpression;
        if(mex!=null)
            rtn = mex.Member.Name;
        return rtn;
    }

    public static void SetValue<T>(ref T target, T newVal, object sender, PropertyChangedEventHandler handler, params string[] changed)
    {
        if (!target.Equals(newVal))
        {
            target = newVal;
            PropertyChanged(sender, handler, changed);
        }
    }
    public static void SetValue<T>(ref T target, T newVal, Action<PropertyChangedEventArgs> handler, params string[] changed)
    {
        if (!target.Equals(newVal))
        {
            target = newVal;
            foreach (var item in changed)
            {
                handler(GetArg(item));
            }
        }
    }

    public static void PropertyChanged(object sender,PropertyChangedEventHandler handler,params string[] changed)
    {
        if (handler!=null)
        {
            foreach (var prop in changed)
            {
                handler(sender, GetArg(prop));
            }
        }
    }
    public static PropertyChangedEventArgs GetArg(string name)
    {
        if (!argslookup.ContainsKey(name)) argslookup.Add(name, new PropertyChangedEventArgs(name));
        return argslookup[name];
    }
}

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

public class NotifyValue<T>
{
    public static implicit operator T(NotifyValue<T> item)
    {
        return item.Value;
    }

    public NotifyValue(object parent, T value = default(T), PropertyChangingEventHandler changing = null, PropertyChangedEventHandler changed = null, params object[] dependenies)
    {
        _parent = parent;
        _propertyChanged = changed;
        _propertyChanging = changing;

        if (_propertyChanged != null)
        {
            _propertyChangedArg =
                dependenies.OfType<PropertyChangedEventArgs>()
                .Union(
                    from d in dependenies.OfType<string>()
                    select new PropertyChangedEventArgs(d)
                );

        }
        if (_propertyChanging != null)
        {
            _propertyChangingArg =
                dependenies.OfType<PropertyChangingEventArgs>()
                .Union(
                    from d in dependenies.OfType<string>()
                    select new PropertyChangingEventArgs(d)
                );
        }
        _PostChangeActions = dependenies.OfType<Action>();

    }

    private T _Value;

    public T Value
    {
        get { return _Value; }
        set
        {
            SetValue(value);
        }
    }

    public bool SetValue(T value)
    {
        if (!EqualityComparer<T>.Default.Equals(_Value, value))
        {
            OnPropertyChnaging();
            _Value = value;
            OnPropertyChnaged();
            foreach (var action in _PostChangeActions)
            {
                action();
            }
            return true;
        }
        else
            return false;
    }

    private void OnPropertyChnaged()
    {
        var handler = _propertyChanged;
        if (handler != null)
        {
            foreach (var arg in _propertyChangedArg)
            {
                handler(_parent, arg);
            }           
        }
    }

    private void OnPropertyChnaging()
    {
        var handler = _propertyChanging;
        if(handler!=null)
        {
            foreach (var arg in _propertyChangingArg)
            {
                handler(_parent, arg);
            }
        }
    }

    private object _parent;
    private PropertyChangedEventHandler _propertyChanged;
    private PropertyChangingEventHandler _propertyChanging;
    private IEnumerable<PropertyChangedEventArgs> _propertyChangedArg;
    private IEnumerable<PropertyChangingEventArgs> _propertyChangingArg;
    private IEnumerable<Action> _PostChangeActions;
}

пример использования

private NotifyValue<int> _val;
public const string ValueProperty = "Value";
public int Value
{
    get { return _val.Value; }
    set { _val.Value = value; }
}

тогда в конструкторе вы делаете

_val = new NotifyValue<int>(this,0,PropertyChanged,PropertyChanging,ValueProperty );
...