开发者

Better way to trigger OnPropertyChanged

We have a WPF Project that follows the MVVM pattern.

In the View Model there is a lot of code that looks like this:

    private string m_Fieldname;
    public string Fieldname
    {
        get { return m_Fieldname; }
        set
        {
            m_Fieldname = value;
 开发者_JAVA百科           OnPropertyChanged("Fieldname");
        }
    }

Is there a way to do this that would require less code?

Would be nice with something like this:

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


You could have a look at PostSharp. They even have a sample at Data Binding. The code taken from there:

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

    }
}

Usage is then as simple as this:

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

Examples taken from PostSharp site and inserted for completing the answer


It looks like as if the Framework 4.5 slightly simplifies this:

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

This doesn't quite automate things to the extent you're looking for, but using the CallerMemberNameAttribute makes passing the property name as a string unnecessary.

If you're working on Framework 4.0 with KB2468871 installed, you can install the Microsoft BCL Compatibility Pack via nuget, which also provides this attribute.


Josh Smith has a good article on using DynamicObject to do this here

Basically it involves inheriting from DynamicObject and then hooking into TrySetMember. CLR 4.0 only, unfortunately, although it may also be possible using ContextBoundObject in earlier versions but that would probably hurt performance, being primarily suited for remoting\WCF.


IMHO, the PostSharp approach, as in the accepted answer, is very nice and is of course the direct answer to the question asked.

However, for those who can't or won't use a tool like PostSharp to extend the C# language syntax, one can get most of the benefit of avoiding code repetition with a base class that implements INotifyPropertyChanged. There are many examples lying around, but none have so far been included in this useful and well-trafficked question, so here is the version I generally use:

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

Used, for example, like this:

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

Not quite as concise as being able to just apply a code attribute to an auto-implemented property as in the PostSharp approach, but still goes a long way to speeding the implementation of view models and other similar types.

The key features above that distinguish it from some other implementations:

  1. Equality is compared using EqualityComparer<T>.Default. This ensures that value types can be compared without being boxed (a common alternative would be object.Equals(object, object)). The IEqualityComparer<T> instance is cached, so after the first comparison for any given type T, it's very efficient.
  2. The OnPropertyChanged() method is virtual. This allows derived types to easily and efficiently handle property changed events in a centralized way, without having to subscribe to the PropertyChanged event itself (e.g. for multiple levels of inheritance) and also of course gives the derived type better control over how and when it handles the property changed event relative to raising the actual PropertyChanged event.


Ok this doesn't clean the code up but it shortens the amount of time writing all of this code. I can now blow through a list of 20+ properties in a couple of minutes.

First you need to define all of your private variables, I'm assuming your first character is a lower case. Now copy that those variables into another list as the macro removes the original line.

For example:

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

Then put your cursor somewhere on that line and run this macro. Again this replaces the line with a public property so make sure you have the same private member variable defined before this in your class.

I'm sure this script could be cleaned up, but it saved me hours of tedious work today.

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


I'd use PropertyChanged.Fody NuGet package. It's as simple as:

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

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

All public properties will raise PropertyChanged event under the hood.

P.S. Syntax changed in newer versions. This example is for version 1.52.1

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜