Автоматически INotifyPropertyChanged

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


Антон предлагает динамические прокси. Фактически я использовал библиотеку "Замок" для чего-то подобного в прошлом, и хотя она уменьшает количество кода, который мне приходилось писать, он добавил около 30 секунд к моему времени запуска программы (ymmv) - потому что это времени выполнения.

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


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

Ответы

Ответ 1

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


Очень удобный инструмент, который я использовал для своих проектов, - это Уведомить Property Weaver 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; }
}

Ответ 2

Оператор nameof был реализован в С# 6.0 с .NET 4.6 и VS2015 в июле 2015 года. Для С# < 6,0

Мы используем код ниже (From 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);

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

РЕДАКТИРОВАТЬ: Теперь я вижу, что вам, возможно, понадобилось еще меньше работы, но да... материал, по крайней мере, делает его намного проще. И вы предотвратите все страшные проблемы со ссылкой на свойства, используя строки.

Ответ 3

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 через nuget, который также предоставляет этот атрибут.

Ответ 4

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

Ответ 5

INotifyPropertyChanged безопасный тип INotifyPropertyChanged: см. Здесь

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

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

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

Ответ 6

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

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

Ответ 7

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

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

Ответ 8

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

Ответ 9

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

Процитировать с веб-сайта...


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

Вместо этого напишите свойства следующим образом:

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

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

Ответ 10

Улучшение для вызова события в дочерних классах:

Вызывается благодаря: this.NotifyPropertyChange(() = > PageIndex);

Добавьте это в класс NotificationExtensions:

    /// <summary>
    /// <para>Lève l'évènement de changement de valeur sur l'objet <paramref name="sender"/>
    /// pour la propriété utilisée dans la lambda <paramref name="property"/>.</para>
    /// </summary>
    /// <param name="sender">L'objet portant la propriété et l'évènement.</param>
    /// <param name="property">Une expression lambda utilisant la propriété subissant la modification.</param>
    public static void NotifyPropertyChange(this INotifyPropertyChanged sender, Expression<Func<Object>> property)
    {
        if (sender == null)
            return;

        // Récupère le nom de la propriété utilisée dans la lambda en argument
        LambdaExpression lambda = property as LambdaExpression;
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            UnaryExpression unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }
        ConstantExpression constantExpression = memberExpression.Expression as ConstantExpression;
        PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;


        // il faut remonter la hierarchie, car meme public, un event n est pas visible dans les enfants
        FieldInfo eventField;
        Type baseType = sender.GetType();
        do
        {
            eventField = baseType.GetField(INotifyPropertyChangedEventFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
            baseType = baseType.BaseType;
        } while (eventField == null);

        // on a trouvé l'event, on peut invoquer tt les delegates liés
        MulticastDelegate eventDelegate = eventField.GetValue(sender) as MulticastDelegate;
        if (eventDelegate == null) return; // l'event n'est bindé à aucun delegate
        foreach (Delegate handler in eventDelegate.GetInvocationList())
        {
            handler.Method.Invoke(handler.Target, new Object[] { sender, new PropertyChangedEventArgs(propertyInfo.Name) });
        }
    }

Ответ 11

Чтобы сделать реализацию быстрее, вы можете использовать фрагмент

Из http://aaron-hoffman.blogspot.it/2010/09/visual-studio-code-snippet-for-notify.html

классы ViewModel проектов, следующих за шаблоном M-V-VM, часто необходимо поднять событие PropertyChanged (чтобы помочь с реализацией интерфейса INotifyPropertyChanged) из набора свойств. Это утомительная задача, которая, надеюсь, когда-нибудь будет решена с помощью компилятора как службы...

Ядро фрагмента (для которого полный кредит принадлежит автору, кто не я) является следующим

  <Code Language= "csharp "> 
    <![CDATA[public $type$ $property$ 
{ 
    get { return _$property$; } 
    set 
    { 
        if (_$property$ != value) 
        { 
            _$property$ = value; 
            OnPropertyChanged($property$PropertyName); 
        } 
    } 
} 
private $type$ _$property$; 
public const string $property$PropertyName = "$property$";$end$]]> 
</Code> 

Ответ 12

Нет единой реализации 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 );

Ответ 13

Просто используйте этот атрибут над объявлением автоматического объявления

[NotifyParentProperty(true)]
public object YourProperty { get; set; }