触发OnPropertyChanged的更好方法
我们有一个遵循MVVM模式的WPF项目。
在View Model中有很多代码如下所示:
private string m_Fieldname; public string Fieldname { get { return m_Fieldname; } set { m_Fieldname = value; OnPropertyChanged("Fieldname"); } }
有没有办法做到这需要更少的代码?
对这样的事情会很好:
[NotifyWhenChanged] public string Fieldname { get; set ; }
你可以看看PostSharp 。 他们甚至在Data Binding上有一个样本。 代码取自那里:
/// /// Aspect that, when apply on a class, fully implements the interface /// into that class, and overrides all properties to /// that they raise the event . /// [Serializable] [IntroduceInterface( typeof(INotifyPropertyChanged), OverrideAction = InterfaceOverrideAction.Ignore )] [MulticastAttributeUsage( MulticastTargets.Class, Inheritance = MulticastInheritance.Strict )] public sealed class NotifyPropertyChangedAttribute : InstanceLevelAspect, INotifyPropertyChanged { /// /// Field bound at runtime to a delegate of the method OnPropertyChanged . /// [ImportMember( "OnPropertyChanged", IsRequired = false)] public Action OnPropertyChangedMethod; /// /// Method introduced in the target type (unless it is already present); /// raises the event. /// /// Name of the property. [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 ) ); } } /// /// Event introduced in the target type (unless it is already present); /// raised whenever a property has changed. /// [IntroduceMember( OverrideAction = MemberOverrideAction.Ignore )] public event PropertyChangedEventHandler PropertyChanged; /// /// Method intercepting any call to a property setter. /// /// Aspect arguments. [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
属性名称作为字符串传递不必要。
如果您正在使用安装了KB2468871的 Framework 4.0, 则可以通过nuget安装Microsoft BCL兼容包 ,它还提供此属性。
Josh Smith在这里有一篇关于使用DynamicObject执行此操作的文章
基本上它涉及从DynamicObjectinheritance然后挂钩到TrySetMember。 不幸的是,CLR 4.0虽然在早期版本中也可能使用ContextBoundObject,但这可能会损害性能,主要适用于远程处理\ WCF。
恕我直言,PostSharp方法,如在接受的答案中,非常好,当然是问题的直接答案。
但是,对于那些不能或不会使用像PostSharp这样的工具来扩展C#语言语法的人来说,可以通过实现INotifyPropertyChanged
的基类来避免代码重复的大部分好处。 有许多例子,但到目前为止还没有一个包含在这个有用且流量很大的问题中,所以这里是我通常使用的版本:
/// /// Base class for classes that need to implement /// public class NotifyPropertyChangedBase : INotifyPropertyChanged { /// /// Raised when a property value changes /// public event PropertyChangedEventHandler PropertyChanged; /// /// Updates a field for a named property /// /// The type of the field /// The field itself, passed by-reference /// The new value for the field /// The name of the associated property protected void UpdatePropertyField(ref T field, T newValue, [CallerMemberName] string propertyName = null) { if (!EqualityComparer .Default.Equals(field, newValue)) { field = newValue; OnPropertyChanged(propertyName); } } /// /// Raises the event. /// /// The name of the property that has been changed 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方法中将代码属性应用于自动实现的属性那样简洁,但是在加速视图模型和其他类似类型的实现方面仍然有很长的路要走。
上面的关键特性将其与其他一些实现区分开来:
- 使用
EqualityComparer
比较.Default EqualityComparer
。 这样可以确保可以在不加框的情况下比较值类型(常见的替代方法是.Default object.Equals(object, object)
)。IEqualityComparer
实例被缓存,因此在对任何给定类型T
进行第一次比较之后,它非常有效。 -
OnPropertyChanged()
方法是virtual
。 这允许派生类型以集中的方式轻松有效地处理属性更改事件,而无需订阅PropertyChanged
事件本身(例如,用于多级inheritance),当然也可以使派生类型更好地控制它处理的方式和时间该属性更改了相对于提升实际PropertyChanged
事件的事件。
好的,这不会清除代码,但会缩短编写所有代码的时间。 我现在可以在几分钟内查看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