Лучший способ вызвать OnPropertyChanged

У нас есть проект WPF, который следует шаблону MVVM.

в модели представления есть много кода, который выглядит так:

    private string m_Fieldname;
    public string Fieldname
    {
        get { return m_Fieldname; }
        set
        {
            m_Fieldname = value;
            OnPropertyChanged("Fieldname");
        }
    }

есть ли способ сделать это, что потребует меньше кода?

было бы неплохо что-то вроде этого:

[NotifyWhenChanged]
public string Fieldname { get; set ; }

5 ответов


вы могли бы взглянуть на PostSharp. У них даже есть образец на Привязка Данных. Код, взятый оттуда:

/// <summary>
/// Aspect that, when apply on a class, fully implements the interface 
/// <see cref="INotifyPropertyChanged"/> into that class, and overrides all properties to
/// that they raise the event <see cref="INotifyPropertyChanged.PropertyChanged"/>.
/// </summary>
[Serializable]
[IntroduceInterface( typeof(INotifyPropertyChanged), 
                     OverrideAction = InterfaceOverrideAction.Ignore )]
[MulticastAttributeUsage( MulticastTargets.Class, 
                          Inheritance = MulticastInheritance.Strict )]
public sealed class NotifyPropertyChangedAttribute : InstanceLevelAspect, 
                                                     INotifyPropertyChanged
{

    /// <summary>
    /// Field bound at runtime to a delegate of the method <c>OnPropertyChanged</c>.
    /// </summary>
    [ImportMember( "OnPropertyChanged", IsRequired = false)] 
    public Action<string> OnPropertyChangedMethod;

    /// <summary>
    /// Method introduced in the target type (unless it is already present);
    /// raises the <see cref="PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">Name of the property.</param>
    [IntroduceMember( Visibility = Visibility.Family, IsVirtual = true, 
                      OverrideAction = MemberOverrideAction.Ignore )]
    public void OnPropertyChanged( string propertyName )
    {
        if ( this.PropertyChanged != null )
        {
           this.PropertyChanged( this.Instance, 
                                  new PropertyChangedEventArgs( propertyName ) );
        }
    }

    /// <summary>
    /// Event introduced in the target type (unless it is already present);
    /// raised whenever a property has changed.
    /// </summary>
    [IntroduceMember( OverrideAction = MemberOverrideAction.Ignore )]
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Method intercepting any call to a property setter.
    /// </summary>
    /// <param name="args">Aspect arguments.</param>
    [OnLocationSetValueAdvice, 
     MulticastPointcut( Targets = MulticastTargets.Property, 
         Attributes = MulticastAttributes.Instance)]
    public void OnPropertySet( LocationInterceptionArgs args )
    {
        // Don't go further if the new value is equal to the old one.
        // (Possibly use object.Equals here).
        if ( args.Value == args.GetCurrentValue() ) return;

        // Actually sets the value.
        args.ProceedSetValue();

        // Invoke method OnPropertyChanged (our, the base one, or the overridden one).
        this.OnPropertyChangedMethod.Invoke( args.Location.Name );

    }
}
использование так же просто, как это:
[NotifyPropertyChanged]
public class Shape
{
   public double X { get; set; }
   public double Y { get; set; }
}

примеры взяты с сайта PostSharp и вставлены для завершения ответа


похоже, что Framework 4.5 немного упрощает это:

private string m_Fieldname;
public string Fieldname
{
    get { return m_Fieldname; }
    set
    {
        m_Fieldname = value;
        OnPropertyChanged();
    }
}

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

это не совсем автоматизирует вещи в той степени, в которой вы ищете, но с помощью CallerMemberNameAttribute делает передачу имени свойства в виде строки ненужной.

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


у Джоша Смита есть хорошая статья об использовании DynamicObject для этого здесь

в основном это включает в себя наследование от DynamicObject, а затем подключение к TrySetMember. CLR 4.0 только, к сожалению, хотя это также может быть возможно с помощью ContextBoundObject в более ранних версиях, но это, вероятно, повредит производительности, в первую очередь подходит для удаленного взаимодействия\WCF.


ИМХО, Постшарповый подход, как и в принятом ответе, очень хорош и, конечно, является прямым ответом на заданный вопрос.

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

/// <summary>
/// Base class for classes that need to implement <see cref="INotifyPropertyChanged"/>
/// </summary>
public class NotifyPropertyChangedBase : INotifyPropertyChanged
{
    /// <summary>
    /// Raised when a property value changes
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Updates a field for a named property
    /// </summary>
    /// <typeparam name="T">The type of the field</typeparam>
    /// <param name="field">The field itself, passed by-reference</param>
    /// <param name="newValue">The new value for the field</param>
    /// <param name="propertyName">The name of the associated property</param>
    protected void UpdatePropertyField<T>(ref T field, T newValue, [CallerMemberName] string propertyName = null)
    {
        if (!EqualityComparer<T>.Default.Equals(field, newValue))
        {
            field = newValue;
            OnPropertyChanged(propertyName);
        }
    }

    /// <summary>
    /// Raises the <see cref="PropertyChanged"/> event.
    /// </summary>
    /// <param name="propertyName">The name of the property that has been changed</param>
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.DynamicInvoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

используется, например, так:

private int _value;
public int Value
{
    get { return _value; }
    set { UpdatePropertyField(ref _value, value); }
}

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

основные функции выше, которые отличают его от некоторых других реализаций:

  1. равенство сравнивается с помощью EqualityComparer<T>.Default. Это гарантирует, что типы значений могут сравниваться без коробки (общей альтернативой будет object.Equals(object, object)). The IEqualityComparer<T> экземпляр кэшируется, поэтому после первого сравнения для любого заданного типа T, это очень эффективно.
  2. на OnPropertyChanged() метод virtual. Это позволяет производным типам легко и эффективно обрабатывать события, измененные свойством, централизованно, без необходимости подписываться на событие (например, для нескольких уровней наследования), а также конечно дает производному типу лучший контроль над тем, как и когда он обрабатывает событие изменения свойства относительно повышения фактического PropertyChanged событие.

Ok это не очищает код, но сокращает время написания всего этого кода. Теперь я могу продуть список свойств 20+ за пару минут.

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

например:

private int something1 = 0;
private int something2 = 0;
private int something3 = 0;
private int something4 = 0;
private int something5 = 0;
private int something6 = 0;

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

Я уверен, что этот сценарий можно было бы очистить, но это сэкономило мне часы утомительной работы сегодня.

Sub TemporaryMacro()
    DTE.ActiveDocument.Selection.StartOfLine(VsStartOfLineOptions.VsStartOfLineOptionsFirstText)
    DTE.ActiveDocument.Selection.Delete(7)
    DTE.ActiveDocument.Selection.Text = "public"
    DTE.ActiveDocument.Selection.CharRight()
    DTE.ExecuteCommand("Edit.Find")
    DTE.Find.FindWhat = " "
    DTE.Find.Target = vsFindTarget.vsFindTargetCurrentDocument
    DTE.Find.MatchCase = False
    DTE.Find.MatchWholeWord = False
    DTE.Find.Backwards = False
    DTE.Find.MatchInHiddenText = False
    DTE.Find.PatternSyntax = vsFindPatternSyntax.vsFindPatternSyntaxLiteral
    DTE.Find.Action = vsFindAction.vsFindActionFind
    If (DTE.Find.Execute() = vsFindResult.vsFindResultNotFound) Then
        Throw New System.Exception("vsFindResultNotFound")
    End If
    DTE.ActiveDocument.Selection.CharRight()
    DTE.ActiveDocument.Selection.WordRight(True)
    DTE.ActiveDocument.Selection.CharLeft(True)
    DTE.ActiveDocument.Selection.Copy()
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.CharRight(True)
    DTE.ActiveDocument.Selection.ChangeCase(VsCaseOptions.VsCaseOptionsUppercase)
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.StartOfLine(VsStartOfLineOptions.VsStartOfLineOptionsFirstText)
    DTE.ExecuteCommand("Edit.Find")
    DTE.Find.FindWhat = " = "
    DTE.Find.Target = vsFindTarget.vsFindTargetCurrentDocument
    DTE.Find.MatchCase = False
    DTE.Find.MatchWholeWord = False
    DTE.Find.Backwards = False
    DTE.Find.MatchInHiddenText = False
    DTE.Find.PatternSyntax = vsFindPatternSyntax.vsFindPatternSyntaxLiteral
    DTE.Find.Action = vsFindAction.vsFindActionFind
    If (DTE.Find.Execute() = vsFindResult.vsFindResultNotFound) Then
        Throw New System.Exception("vsFindResultNotFound")
    End If
    DTE.ActiveDocument.Selection.CharLeft()
    DTE.ActiveDocument.Selection.EndOfLine(True)
    DTE.ActiveDocument.Selection.Delete()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "{"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "get { return "
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = "; }"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "set"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "{"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "if("
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = " != value)"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "{"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = " = value;"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "OnPropertyChanged("""
    DTE.ActiveDocument.Selection.Paste()
    DTE.ActiveDocument.Selection.Text = """);"
    DTE.ActiveDocument.Selection.StartOfLine(VsStartOfLineOptions.VsStartOfLineOptionsFirstText)
    DTE.ExecuteCommand("Edit.Find")
    DTE.Find.FindWhat = """"
    DTE.Find.Target = vsFindTarget.vsFindTargetCurrentDocument
    DTE.Find.MatchCase = False
    DTE.Find.MatchWholeWord = False
    DTE.Find.Backwards = False
    DTE.Find.MatchInHiddenText = False
    DTE.Find.PatternSyntax = vsFindPatternSyntax.vsFindPatternSyntaxLiteral
    DTE.Find.Action = vsFindAction.vsFindActionFind
    If (DTE.Find.Execute() = vsFindResult.vsFindResultNotFound) Then
        Throw New System.Exception("vsFindResultNotFound")
    End If
    DTE.ActiveDocument.Selection.CharRight()
    DTE.ActiveDocument.Selection.CharRight(True)
    DTE.ActiveDocument.Selection.ChangeCase(VsCaseOptions.VsCaseOptionsUppercase)
    DTE.ActiveDocument.Selection.Collapse()
    DTE.ActiveDocument.Selection.EndOfLine()
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "}"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "}"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.Text = "}"
    DTE.ActiveDocument.Selection.NewLine()
    DTE.ActiveDocument.Selection.LineDown()
    DTE.ActiveDocument.Selection.EndOfLine()
End Sub