Подтвердить что ты не робот

Лучший способ запуска 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 ; }
4b9b3361

Ответ 1

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

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

Однако для тех, кто не может или не хочет использовать такой инструмент, как PostSharp, для расширения синтаксиса языка С#, можно получить большую часть преимуществ, избегая повторения кода с помощью базового класса, который реализует 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="onChangedCallback">A delegate to be called if the field value has changed. The old value of the field is passed to the delegate.</param>
    /// <param name="propertyName">The name of the associated property</param>
    protected void UpdatePropertyField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        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?.Invoke(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)). Экземпляр IEqualityComparer<T> кэшируется, поэтому после первого сравнения для любого данного типа T он очень эффективен.
  2. Метод OnPropertyChanged() - это virtual. Это позволяет производным типам легко и эффективно обрабатывать события изменения свойств централизованным образом, без необходимости подписываться на само событие PropertyChanged (например, для нескольких уровней наследования), а также, конечно, дает производному типу лучший контроль над тем, как и когда он обрабатывает событие измененного свойства относительно повышения фактического события PropertyChanged.

Ответ 5

Хорошо, это не очищает код, но сокращает время написания всего этого кода. Теперь я могу пропустить список из 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;

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

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

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

Ответ 6

Я бы использовал пакет PropertyChanged.Fody NuGet. Это так просто, как:

[PropertyChanged.ImplementPropertyChanged]
public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };

    public string Prop1 { get; set; }
    public string Prop2 { get; set; }
}

Все публичные свойства будут вызывать событие PropertyChanged под капотом.

Постскриптум Синтаксис изменен в новых версиях. Этот пример для версии 1.52.1