Lepszy sposób wyzwalania OnPropertyChanged

Mamy projekt WPF, który podąża za wzorcem MVVM.

W modelu widoku jest dużo kodu, który wygląda tak:

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

Czy jest na to sposób, który wymagałby mniej kodu?

Byłoby miło z czymś takim:

[NotifyWhenChanged]
public string Fieldname { get; set ; }
Author: stakx, 2011-08-15

5 answers

Możesz rzucić okiem na PostSharp . Mają nawet próbkę w Data Binding . Kod zaczerpnięty stamtąd:

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

    }
}

Użycie jest wtedy tak proste:

[NotifyPropertyChanged]
public class Shape
{
   public double X { get; set; }
   public double Y { get; set; }
}

Przykłady zaczerpnięte ze strony PostSharp i wstawione do uzupełnienia odpowiedzi

 11
Author: Sascha,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-15 10:30:39

Wygląda to tak, jakby Framework 4.5 nieco uprościł to:

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

To nie do końca automatyzuje rzeczy w zakresie, którego szukasz, ale za pomocą CallerMemberNameAttribute sprawia, że podanie nazwy właściwości jako ciągu znaków jest niepotrzebne.

Jeśli pracujesz nad frameworkiem 4.0 z zainstalowanym kb2468871, możesz zainstalować Microsoft BCL Compatibility Pack poprzez nuget , który również dostarcza ten atrybut.

 5
Author: takrl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-06 07:21:33

Josh Smith ma dobry artykuł o używaniu DynamicObject do tego tutaj

Zasadniczo polega na dziedziczeniu z DynamicObject, a następnie podłączaniu do TrySetMember. CLR 4.0 tylko, niestety, chociaż może być również możliwe przy użyciu ContextBoundObject we wcześniejszych wersjach, ale to prawdopodobnie zaszkodzi wydajności, jest przede wszystkim nadaje się do remoting\WCF.

 3
Author: Mark Green,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-15 12:55:57

IMHO, podejście PostSharp, jak w przyjętej odpowiedzi, jest bardzo ładne i jest oczywiście bezpośrednią odpowiedzią na zadane pytanie.

Jednakże, dla tych, którzy nie mogą lub nie chcą użyć narzędzia takiego jak PostSharp do rozszerzenia składni języka C#, można uzyskać większość korzyści unikania powtarzania kodu za pomocą klasy bazowej, która implementuje INotifyPropertyChanged. Jest wiele przykładów, ale do tej pory żaden z nich nie został uwzględniony w tym przydatnym i dobrze przemyślanym pytaniu, więc oto wersja I ogólnie użycie:

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

Używane na przykład tak:

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

Nie jest tak zwięzła, jak możliwość zastosowania atrybutu kodu do automatycznie zaimplementowanej właściwości, jak w podejściu PostSharp, ale nadal przyspiesza implementację modeli widoków i innych podobnych typów.

Najważniejsze cechy, które odróżniają go od niektórych innych implementacji:

  1. równość porównuje się za pomocą EqualityComparer<T>.Default. Zapewnia to możliwość porównywania typów wartości bez pudełka (powszechną alternatywą byłoby object.Equals(object, object)). Instancja IEqualityComparer<T> jest buforowana, więc po pierwszym porównaniu dla danego typu T jest bardzo wydajna.
  2. metoda OnPropertyChanged() to virtual. Pozwala to typom pochodnym łatwo i efektywnie obsługiwać zdarzenia zmienione właściwościami w sposób scentralizowany, bez konieczności subskrybowania samego zdarzenia PropertyChanged (np. dla wielu poziomów dziedziczenia), a także oczywiście daje typowi pochodnemu lepszą kontrolę nad tym, jak i kiedy obsługuje zdarzenia właściwość zmieniła Zdarzenie względem wywołania rzeczywistego zdarzenia PropertyChanged.
 2
Author: Peter Duniho,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-09 20:27:45

Ok to nie oczyszcza kodu, ale skraca czas pisania całego kodu. Mogę teraz przejrzeć listę 20 + nieruchomości w kilka minut.

Najpierw musisz zdefiniować wszystkie swoje prywatne zmienne, zakładam, że twój pierwszy znak jest małą literą. Teraz skopiuj te zmienne do innej listy, gdy makro usunie oryginalną linię.

Na przykład:

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

Następnie umieść kursor gdzieś na tej linii i uruchom to makro. Again this zastępuje linię właściwością publiczną, więc upewnij się, że masz tę samą prywatną zmienną członkowską zdefiniowaną wcześniej w klasie.

Jestem pewien, że ten scenariusz da się posprzątać, ale zaoszczędziło mi to wiele godzin żmudnej pracy.

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
 0
Author: FodderZone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-11-01 21:08:07