INotifyPropertyChanged 自动装配, 或如何摆脱冗余代码






4.48/5 (10投票s)
INotifyPropertyChanged 自动装配,
在过去的一周里,大多数 WPF 门徒都在讨论如何摆脱 INotifyPropertyChanged
实现中硬编码的属性名称字符串,以及如何在继续使用自动属性实现的同时,保持 WPF 绑定正常工作。这个话题是由 Karl Shifflett 启动的,他提出了一种有趣的方法,即使用 StackFrame 来完成这项任务。 在此讨论中,还提出了其他方法,包括代码片段、R#、观察者模式、Cinch 框架、静态反射、弱引用 等。 我也提出了我们类中使用的这种方法,并承诺会写博客文章来介绍它。 因此,今天的主题是如何使用 PostSharp 来自动实现 INotifyPropertyChanged 接口
,仅基于自动设置器。
因此,我希望我的代码看起来像这样
public class AutoWiredSource {
public double MyProperty { get; set; }
public double MyOtherProperty { get; set; }
}
同时能够完全注意到任何属性的任何更改,并使我能够将这些属性绑定到其他元素。
<StackPanel DataContext="{Binding Source={StaticResource source}}">
<Slider Value="{Binding Path=MyProperty}" />
<Slider Value="{Binding Path=MyProperty}" />
</StackPanel>
如何实现它? 如何让编译器用以下代码替换我的代码?
private double _MyProperty;
public double MyProperty {
get { return _MyProperty; }
set {
if (value != _MyProperty) {
_MyProperty = value; OnPropertyChanged("MyProperty");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
var handler = PropertyChanged as PropertyChangedEventHandler;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
很简单:使用面向切面编程将一组指令注入到预编译的源代码中。
首先,我们需要构建一个属性,该属性将用于标记需要更改跟踪的类。 此属性应组合(复合)方面,以包含用于更改跟踪的所有方面。 我们所做的一切就是获取所有 set
方法,并将组合方面添加到
[Serializable, DebuggerNonUserCode, AttributeUsage(
AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false,
Inherited = false),
MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false,
Inheritance = MulticastInheritance.None, AllowExternalAssemblies = true)]
public sealed class NotifyPropertyChangedAttribute : CompoundAspect {
public int AspectPriority { get; set; }
public override void ProvideAspects(object element,
LaosReflectionAspectCollection collection) {
Type targetType = (Type)element;
collection.AddAspect(targetType, new PropertyChangedAspect {
AspectPriority = AspectPriority });
foreach (var info in targetType.GetProperties(
BindingFlags.Public | BindingFlags.Instance).Where(
pi => pi.GetSetMethod() != null)) {
collection.AddAspect(info.GetSetMethod(), new NotifyPropertyChangedAspect(
info.Name) { AspectPriority = AspectPriority });
}
}
}
下一个方面是更改跟踪组合方面。 它仅用于标记
[Serializable]
internal sealed class PropertyChangedAspect : CompositionAspect {
public override object CreateImplementationObject(
InstanceBoundLaosEventArgs eventArgs) {
return new PropertyChangedImpl(eventArgs.Instance);
}
public override Type GetPublicInterface(Type containerType) {
return typeof(INotifyPropertyChanged);
}
public override CompositionAspectOptions GetOptions() {
return CompositionAspectOptions.GenerateImplementationAccessor;
}
}
接下来,也是最有趣的一个,我们将把它放在方法边界上进行跟踪。 这里有一些要点。 首先,我们不希望在实际值没有更改时触发 PropertyChanged
事件,因此我们将处理方法在其进入和退出时进行检查。
[Serializable]
internal sealed class NotifyPropertyChangedAspect : OnMethodBoundaryAspect {
private readonly string _propertyName;
public NotifyPropertyChangedAspect(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
_propertyName = propertyName;
}
public override void OnEntry(MethodExecutionEventArgs eventArgs) {
var targetType = eventArgs.Instance.GetType();
var setSetMethod = targetType.GetProperty(_propertyName);
if (setSetMethod == null) throw new AccessViolationException();
var oldValue = setSetMethod.GetValue(eventArgs.Instance,null);
var newValue = eventArgs.GetReadOnlyArgumentArray()[0];
if (oldValue == newValue) eventArgs.FlowBehavior = FlowBehavior.Return;
}
public override void OnSuccess(MethodExecutionEventArgs eventArgs) {
var instance = eventArgs.Instance as IComposed<INotifyPropertyChanged>;
var imp = instance.GetImplementation(
eventArgs.InstanceCredentials) as PropertyChangedImpl;
imp.OnPropertyChanged(_propertyName);
}
}
我们几乎完成了,我们只需要创建一个实现 INotifyPropertyChanged
的类,并提供一个内部方法来调用。
[Serializable]
internal sealed class PropertyChangedImpl : INotifyPropertyChanged {
private readonly object _instance;
public PropertyChangedImpl(object instance) {
if (instance == null) throw new ArgumentNullException("instance");
_instance = instance;
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
var handler = PropertyChanged as PropertyChangedEventHandler;
if (handler != null) handler(_instance, new PropertyChangedEventArgs(propertyName));
}
}
我们完成了。 最后一步是引用 PostSharp Laos 和 Public 程序集,并标记编译器以使用 Postsharp 目标(在您的项目文件 (*.csproj) 中)。
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<DontImportPostSharp>True</DontImportPostSharp>
</PropertyGroup>
<Import Project="PostSharp\PostSharp-1.5.targets" />
现在我们完成了。 我们可以使用清晰的语法,如下所示,使所有具有 public
设置器的属性都可跟踪。 唯一的缺点是您必须将两个 PostSharp 文件与您的项目一起拖动。 但总而言之,它比在整个项目中手动通知更改跟踪要方便得多。
[NotifyPropertyChanged]
public class AutoWiredSource {
public double MyProperty { get; set; }
}
祝您愉快,做一个好人。 还要尝试思考可以使用 PostSharp(或任何其他面向切面编程引擎)完成的其他极其有用的事情。