实现INotifyProperties tyChanged-是否存在更好的方法?

微软应该为INotifyPropertyChanged实现一些时髦的东西,就像在自动属性中一样,只需指定{get; set; notify;} 我觉得这样做很有意义。或者这样做有什么并发症吗?

我们可以自己在我们的属性中实现类似于“通知”的东西吗?在您的类中实现INotifyPropertyChanged是否有一个优雅的解决方案,或者唯一的方法是在每个属性中引发PropertyChanged事件。

如果没有,我们可以编写一些东西来自动生成引发PropertyChanged事件的代码吗?

365794 次浏览

我自己还没有机会尝试这个,但是下次我建立一个对INotifyProperties tyChanged有很大要求的项目时,我打算编写一个后夏普属性,它将在编译时注入代码。类似于:

[NotifiesChange]
public string FirstName { get; set; }

将成为:

private string _firstName;


public string FirstName
{
get { return _firstname; }
set
{
if (_firstname != value)
{
_firstname = value;
OnPropertyChanged("FirstName")
}
}
}

我不确定这是否会在实践中工作,我需要坐下来尝试一下,但我不明白为什么不。我可能需要让它接受一些参数,用于需要触发多个OnProperties tyChanged的情况(例如,如果我在上面的类中有一个FullName属性)

目前我在Resharper中使用自定义模板,但即使这样,我也厌倦了我所有的属性都这么长。


啊,一个快速的谷歌搜索(我应该在写这篇文章之前做的)显示至少有一个人在这里之前做过这样的事情。不完全是我想到的,但足够接近表明这个理论是好的。

如果不使用类似postSharp的东西,我使用的最小版本使用如下内容:

public class Data : INotifyPropertyChanged
{
// boiler-plate
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}


// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}

每个属性就像这样:

private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}

这不是很大;如果你愿意,它也可以用作基类。SetFieldbool返回告诉你它是否是一个no-op,以防你想应用其他逻辑。


使用C#5更容易:

protected bool SetField<T>(ref T field, T value,
[CallerMemberName] string propertyName = null)
{...}

可以这样称呼:

set { SetField(ref name, value); }

编译器将自动添加"Name"


C#6.0使实现更容易:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

现在是C#7:

protected void OnPropertyChanged(string propertyName)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));


protected bool SetField<T>(ref T field, T value,[CallerMemberName] string propertyName =  null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}


private string name;
public string Name
{
get => name;
set => SetField(ref name, value);
}

而且,对于C#8和Nullable引用类型,它看起来像这样:

public event PropertyChangedEventHandler? PropertyChanged;


protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));


protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}


private string name;
public string Name
{
get => name;
set => SetField(ref name, value);
}

我真的很喜欢Marc的解决方案,但我认为它可以稍微改进,以避免使用“魔法字符串”(它不支持重构)。与其将属性名称用作字符串,不如将其设为lambda表达式:

private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, () => Name); }
}

只需将以下方法添加到Marc的代码中,它就会起作用:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
if (selectorExpression == null)
throw new ArgumentNullException("selectorExpression");
MemberExpression body = selectorExpression.Body as MemberExpression;
if (body == null)
throw new ArgumentException("The body must be a member expression");
OnPropertyChanged(body.Member.Name);
}


protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(selectorExpression);
return true;
}

顺便说一句,这是受这篇博客文章的启发。

谈论大量的过度设计。这比只是用正确的方式复杂得多,而且几乎没有任何好处。如果您的IDE支持代码片段(Visual Studio/Mono开发do),那么您可以使实现变得简单得可笑。您实际需要键入的只是属性的类型和属性名称。额外的三行代码将自动生成。

一种非常类似AOP的方法是动态地将INotifyProperties tyChanged内容注入到已经实例化的对象上。你可以使用Castle DynamicProxy之类的东西来做到这一点。这是一篇解释该技术的文章:

添加INotifyProperties tyChanged到一个现有的对象

在实现这些类型的属性时,您可能需要考虑的其他事情是INotifyProperties tyChang*ed*ing都使用事件参数类。

如果您有大量正在设置的属性,那么事件参数类实例的数量可能很大,您应该考虑缓存它们,因为它们是可能发生字符串爆炸的区域之一。

看看这个实现,并解释为什么要构思它。

Josh Smiths Blog

我刚刚发现ActiveSharp-自动通知属性变更,我还没有使用它,但它看起来不错。

引用它的网站…


发送属性更改通知 没有将属性名称指定为 字符串。

相反,像这样写属性:

public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); }  // <-- no property name here
}

请注意,不需要将属性的名称作为字符串包含在内。ActiveSharp可以可靠且正确地自行计算。它的工作原理基于您的属性实现按ref传递支持字段(_foo)的事实。(ActiveSharp使用“by ref”调用来识别传递的支持字段,并从字段标识属性)。

让我介绍一下我自己的方法Yappi。 它属于运行时代理|派生类生成器,向现有对象或类型添加新功能,例如Cast Project的动态代理。

它允许在基类中实现一次INotifyProperties tyChanged,然后以以下样式声明派生类,仍然支持INotifyProperties tyChanged用于新属性:

public class Animal:Concept
{
protected Animal(){}
public virtual string Name { get; set; }
public virtual int Age { get; set; }
}

派生类或代理构造的复杂性可以隐藏在以下行后面:

var animal = Concept.Create<Animal>.New();

所有INotifyProperties tyChanged实现工作都可以这样完成:

public class Concept:INotifyPropertyChanged
{
//Hide constructor
protected Concept(){}


public static class Create<TConcept> where TConcept:Concept
{
//Construct derived Type calling PropertyProxy.ConstructType
public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
//Create constructing delegate calling Constructor.Compile
public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
}




public event PropertyChangedEventHandler PropertyChanged;


protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
{
var caller = PropertyChanged;
if(caller!=null)
{
caller(this, eventArgs);
}
}


//define implementation
public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
{
public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
{
return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
}
/// <summary>
/// Overriding property setter implementation.
/// </summary>
/// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
/// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
/// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
/// <typeparam name="TResult">Type of property.</typeparam>
/// <param name="property">PropertyInfo of property.</param>
/// <returns>Delegate, corresponding to property setter implementation.</returns>
public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
{
//This code called once for each declared property on derived type's initialization.
//EventArgs instance is shared between all events for each concrete property.
var eventArgs = new PropertyChangedEventArgs(property.Name);
//get delegates for base calls.
Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);


var comparer = EqualityComparer<TResult>.Default;


return (pthis, value) =>
{//This code executes each time property setter is called.
if (comparer.Equals(value, getter(pthis))) return;
//base. call
setter(pthis, value);
//Directly accessing Concept's protected method.
pthis.OnPropertyChanged(eventArgs);
};
}
}
}

它对于重构是完全安全的,在类型构造之后不使用反射并且足够快。

从. Net 4.5开始,终于有了一个简单的方法来做到这一点。

. Net 4.5引入了新的呼叫者信息属性。

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
// make sure only to call this if the value actually changes


var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(caller));
}
}

为函数添加比较器可能也是个好主意。

EqualityComparer<T>.Default.Equals

更多示例这里这里

另见呼叫者信息(C#和Visual Basic)

看这里:http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

它是用德语写的,但你可以下载ViewModelBase.cs.cs-File中的所有评论都是用英语写的。

使用这个ViewModelBase-Class可以实现类似于众所周知的依赖属性的可绑定属性:

public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}

使用反射的想法:

class ViewModelBase : INotifyPropertyChanged {


public event PropertyChangedEventHandler PropertyChanged;


bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {


// Get Name of Property
string name = mb.Name.Substring(4);


// Detect Change
bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);


// Return if no change
if (!changed) return false;


// Update value
oldValue = newValue;


// Raise Event
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}//if


// Notify caller of change
return true;


}//method


string name;


public string Name {
get { return name; }
set {
Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
}
}//method


}//class

另一个想法…

 public class ViewModelBase : INotifyPropertyChanged
{
private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
_propertyStore[propertyName] = value;
OnPropertyChanged(propertyName);
}
protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
{
object ret;
if (_propertyStore.TryGetValue(propertyName, out ret))
{
return (T)ret;
}
else
{
return default(T);
}
}


//Usage
//public string SomeProperty {
//    get { return GetValue<string>();  }
//    set { SetValue(value); }
//}


public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

如果您在. NET 4.5中使用动态,则无需担心INotifyPropertyChanged

dynamic obj = new ExpandoObject();
obj.Name = "John";

如果Name绑定到某个控件,它就可以正常工作。

所有这些答案都非常好。

我的解决方案是使用代码片段来完成这项工作。

这使用了最简单的调用属性更改事件。

保存此代码段并在使用'fullprop'代码段时使用它。

该位置可以在Visual Studio的“工具\代码段管理器…”菜单中找到。

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>inotifypropfull</Title>
<Shortcut>inotifypropfull</Shortcut>
<HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
<Description>Code snippet for property and backing field with notification</Description>
<Author>Ofir Zeitoun</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>Property type</ToolTip>
<Default>int</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>Property name</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>field</ID>
<ToolTip>The variable backing this property</ToolTip>
<Default>myVar</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[private $type$ $field$;


public $type$ $property$
{
get { return $field$;}
set {
$field$ = value;
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs("$property$"));
}
}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>

您可以根据需要修改调用(使用上述解决方案)

另一种组合解决方案是使用StackFrame:

public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected void Set<T>(ref T field, T value)
{
MethodBase method = new StackFrame(1).GetMethod();
field = value;
Raise(method.Name.Substring(4));
}


protected void Raise(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(propertyName));
}
}
}

用法:

public class TempVM : BaseViewModel
{
private int _intP;
public int IntP
{
get { return _intP; }
set { Set<int>(ref _intP, value); }
}
}

=>这里我的解决方案具有以下特点

 public ResourceStatus Status
{
get { return _status; }
set
{
_status = value;
Notify(Npcea.Status,Npcea.Comments);
}
}
  1. 不后悔
  2. 短记号法
  3. 你的商业代码中没有神奇的字符串
  4. 跨应用程序的属性变更事件Args的可重用性
  5. 可以在一条语句中通知多个属性

还有福迪,它有一个Add I Notify属性更改接口加载项,可以让你这样写:

[AddINotifyPropertyChangedInterface]
public class Person
{
public string GivenNames { get; set; }
public string FamilyName { get; set; }
}

…并在编译时注入属性更改的通知。

当然有更好的方法。 在这里:

一步一步的教程缩小了我,基于这个有用的文章

  • 创建新项目
  • 在项目中安装Castle core包

安装包Castle. Core

  • 仅安装mvvm灯库

安装包MvvmLightLibs

  • 在project中添加两个类:

通知器

public class NotifierInterceptor : IInterceptor
{
private PropertyChangedEventHandler handler;
public static Dictionary<String, PropertyChangedEventArgs> _cache =
new Dictionary<string, PropertyChangedEventArgs>();


public void Intercept(IInvocation invocation)
{
switch (invocation.Method.Name)
{
case "add_PropertyChanged":
handler = (PropertyChangedEventHandler)
Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
invocation.ReturnValue = handler;
break;
case "remove_PropertyChanged":
handler = (PropertyChangedEventHandler)
Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
invocation.ReturnValue = handler;
break;
default:
if (invocation.Method.Name.StartsWith("set_"))
{
invocation.Proceed();
if (handler != null)
{
var arg = retrievePropertyChangedArg(invocation.Method.Name);
handler(invocation.Proxy, arg);
}
}
else invocation.Proceed();
break;
}
}


private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
{
PropertyChangedEventArgs arg = null;
_cache.TryGetValue(methodName, out arg);
if (arg == null)
{
arg = new PropertyChangedEventArgs(methodName.Substring(4));
_cache.Add(methodName, arg);
}
return arg;
}
}

代理创建者

public class ProxyCreator
{
public static T MakeINotifyPropertyChanged<T>() where T : class, new()
{
var proxyGen = new ProxyGenerator();
var proxy = proxyGen.CreateClassProxy(
typeof(T),
new[] { typeof(INotifyPropertyChanged) },
ProxyGenerationOptions.Default,
new NotifierInterceptor()
);
return proxy as T;
}
}
  • 创建您的视图模型,例如:

-

 public class MainViewModel
{
public virtual string MainTextBox { get; set; }


public RelayCommand TestActionCommand
{
get { return new RelayCommand(TestAction); }
}


public void TestAction()
{
Trace.WriteLine(MainTextBox);
}
}
  • 将绑定放入xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
    
  • Put line of code in code-behind file MainWindow.xaml.cs like this:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Enjoy.

enter image description here

Attention!!! All bounded properties should be decorated with keyword virtual because they used by castle proxy for overriding.

我在博客中介绍了一个Bindable类http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable使用字典作为属性包。为子类添加必要的重载以使用ref参数管理自己的支持字段非常容易。

  • 没有魔法线
  • 没有反射
  • 可以改进以抑制默认字典查找

代码:

public class Bindable : INotifyPropertyChanged {
private Dictionary<string, object> _properties = new Dictionary<string, object>();


/// <summary>
/// Gets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
protected T Get<T>([CallerMemberName] string name = null) {
Debug.Assert(name != null, "name != null");
object value = null;
if (_properties.TryGetValue(name, out value))
return value == null ? default(T) : (T)value;
return default(T);
}


/// <summary>
/// Sets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="name"></param>
/// <remarks>Use this overload when implicitly naming the property</remarks>
protected void Set<T>(T value, [CallerMemberName] string name = null) {
Debug.Assert(name != null, "name != null");
if (Equals(value, Get<T>(name)))
return;
_properties[name] = value;
OnPropertyChanged(name);
}


public event PropertyChangedEventHandler PropertyChanged;


protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

它可以像这样使用:

public class Contact : Bindable {
public string FirstName {
get { return Get<string>(); }
set { Set(value); }
}
}

我认为人们应该更加关注性能;当有很多对象要绑定时(想想一个有10,000多行的网格),或者如果对象的值经常变化(实时监控应用程序),它确实会影响UI。

我在这里和其他地方找到了各种实现并进行了比较;检查它INotifyProperties tyChanged实现的性能比较


这是一个结果 运行时与实现

我在基本库中创建了一个扩展方法以供重用:

public static class INotifyPropertyChangedExtensions
{
public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
PropertyChangedEventHandler handler, ref T field, T value,
[CallerMemberName] string propertyName = "",
EqualityComparer<T> equalityComparer = null)
{
bool rtn = false;
var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
if (!eqComp.Equals(field,value))
{
field = value;
rtn = true;
if (handler != null)
{
var args = new PropertyChangedEventArgs(propertyName);
handler(sender, args);
}
}
return rtn;
}
}

这适用于. Net 4.5,因为来电者姓名属性。 如果您想将其与早期版本一起使用。Net版本,您必须将方法声明从:...,[CallerMemberName] string propertyName = "", ...更改为...,string propertyName, ...

用法:

public class Dog : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _name;


public string Name
{
get { return _name; }
set
{
this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
}
}
}

用这个

using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;




public static class ObservableFactory
{
public static T Create<T>(T target)
{
if (!typeof(T).IsInterface)
throw new ArgumentException("Target should be an interface", "target");


var proxy = new Observable<T>(target);
return (T)proxy.GetTransparentProxy();
}
}


internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
private readonly T target;


internal Observable(T target)
: base(ImplementINotify(typeof(T)))
{
this.target = target;
}


public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;


if (methodCall != null)
{
return HandleMethodCall(methodCall);
}


return null;
}


public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;






IMessage HandleMethodCall(IMethodCallMessage methodCall)
{
var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;


if (isPropertySetterCall)
{
OnPropertyChanging(propertyName);
}


try
{
object methodCalltarget = target;


if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
{
methodCalltarget = this;
}


var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);


if (isPropertySetterCall)
{
OnPropertyChanged(methodCall.MethodName.Substring(4));
}


return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch (TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
return new ReturnMessage(exception, methodCall);
}
}


protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}


protected virtual void OnPropertyChanging(string propertyName)
{
var handler = PropertyChanging;
if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
}


public static Type ImplementINotify(Type objectType)
{
var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());


var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);


var moduleBuilder = dynamicAssembly.DefineDynamicModule(
tempAssemblyName.Name,
tempAssemblyName + ".dll");


var typeBuilder = moduleBuilder.DefineType(
objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);


typeBuilder.AddInterfaceImplementation(objectType);
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
var newType = typeBuilder.CreateType();
return newType;
}
}

}

我以这种方式解决(这有点费力,但在运行时肯定更快)。

在VB中(对不起,但我认为用C#翻译它并不难),我用RE进行了替换:

(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)

有:

Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n

这转换了所有代码,如下所示:

<Bindable(True)>
Protected Friend Property StartDate As DateTime?

Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
Get
Return _StartDate
End Get
Set(Value As DateTime?)
If _StartDate <> Value Then
_StartDate = Value
RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
End If
End Set
End Property

如果我想有一个更具可读性的代码,我可以相反,只需进行以下替换:

Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property

${Attr} ${Def} ${Name} As ${Type}

我抛出替换set方法的IL代码,但是我不会写很多IL编译的代码……如果有一天我写了,我会说你!

根据Thomas的回答(改编自Marc的回答),我将反射属性更改为基类:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}


protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
if (selectorExpression == null)
throw new ArgumentNullException("selectorExpression");
var me = selectorExpression.Body as MemberExpression;


// Nullable properties can be nested inside of a convert function
if (me == null)
{
var ue = selectorExpression.Body as UnaryExpression;
if (ue != null)
me = ue.Operand as MemberExpression;
}


if (me == null)
throw new ArgumentException("The body must be a member expression");


OnPropertyChanged(me.Member.Name);
}


protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(selectorExpression);
foreach (var item in additonal)
OnPropertyChanged(item);
}
}

用法与Thomas的回答相同,只是您可以传递其他属性以通知。这对于处理需要在网格中刷新的计算列是必要的。

private int _quantity;
private int _price;


public int Quantity
{
get { return _quantity; }
set { SetField(ref _quantity, value, () => Quantity, () => Total); }
}
public int Price
{
get { return _price; }
set { SetField(ref _price, value, () => Price, () => Total); }
}
public int Total { get { return _price * _quantity; } }

我有这个驱动存储在通过DataGridView公开的BindingList中的项目集合。它消除了我对网格进行手动Reore()调用的需要。

我把它作为一个片段保存下来。C#6为调用处理程序添加了一些不错的语法。

// INotifyPropertyChanged


public event PropertyChangedEventHandler PropertyChanged;


private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(property, value) == false)
{
property = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

我使用以下扩展方法(使用C#6.0)使INPC实现尽可能简单:

public static bool ChangeProperty<T>(this PropertyChangedEventHandler propertyChanged, ref T field, T value, object sender,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
if (comparer == null)
comparer = EqualityComparer<T>.Default;


if (comparer.Equals(field, value))
{
return false;
}
else
{
field = value;
propertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
return true;
}
}

INPC实现归结为(您可以每次都实现它或创建一个基类):

public class INPCBaseClass: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected bool changeProperty<T>(ref T field, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
return PropertyChanged.ChangeProperty(ref field, value, this, comparer, propertyName);
}
}

然后像这样写你的属性:

private string testProperty;
public string TestProperty
{
get { return testProperty; }
set { changeProperty(ref testProperty, value); }
}

注意:如果需要,您可以省略扩展方法中的[CallerMemberName]声明,但我想保持它的灵活性。

如果您有没有支持字段的属性,您可以重载changeProperty

protected bool changeProperty<T>(T property, Action<T> set, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
bool ret = changeProperty(ref property, value, comparer, propertyName);
if (ret)
set(property);
return ret;
}

一个使用示例是:

public string MyTestProperty
{
get { return base.TestProperty; }
set { changeProperty(base.TestProperty, (x) => { base.TestProperty = x; }, value); }
}

棱镜5实施:

public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


protected virtual bool SetProperty<T>(ref T storage,
T value,
[CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;


storage = value;
this.OnPropertyChanged(propertyName);


return true;
}


protected void OnPropertyChanged(string propertyName)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}


protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
this.OnPropertyChanged(propertyName);
}
}


public static class PropertySupport
{
public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
{
throw new ArgumentNullException("propertyExpression");
}


var memberExpression = propertyExpression.Body as MemberExpression;
if (memberExpression == null)
{
throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
}


var property = memberExpression.Member as PropertyInfo;
if (property == null)
{
throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
}


var getMethod = property.GetMethod;
if (getMethod.IsStatic)
{
throw new ArgumentException("The referenced property is a static property.", "propertyExpression");
}


return memberExpression.Member.Name;
}
}

这是一个Unity3D或非CallerMemberName版本的NotifyProperties tyChanged

public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
{
private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
private static readonly StackTrace stackTrace = new StackTrace();
public event PropertyChangedEventHandler PropertyChanged;


/// <summary>
///     Resolves a Property's name from a Lambda Expression passed in.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="property"></param>
/// <returns></returns>
internal string GetPropertyName<T>(Expression<Func<T>> property)
{
var expression = (MemberExpression) property.Body;
var propertyName = expression.Member.Name;


Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
return propertyName;
}


#region Notification Handlers


/// <summary>
///     Notify's all other objects listening that a value has changed for nominated propertyName
/// </summary>
/// <param name="propertyName"></param>
internal void NotifyOfPropertyChange(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}


/// <summary>
///     Notifies subscribers of the property change.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="property">The property expression.</param>
internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
{
var propertyName = GetPropertyName(property);
NotifyOfPropertyChange(propertyName);
}


/// <summary>
///     Raises the <see cref="PropertyChanged" /> event directly.
/// </summary>
/// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
internal void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}


#endregion


#region Getters


/// <summary>
///     Gets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
internal T Get<T>(Expression<Func<T>> property)
{
var propertyName = GetPropertyName(property);
return Get<T>(GetPropertyName(property));
}


/// <summary>
///     Gets the value of a property automatically based on its caller.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
internal T Get<T>()
{
var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
return Get<T>(name);
}


/// <summary>
///     Gets the name of a property based on a string.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
internal T Get<T>(string name)
{
object value = null;
if (_properties.TryGetValue(name, out value))
return value == null ? default(T) : (T) value;
return default(T);
}


#endregion


#region Setters


/// <summary>
///     Sets the value of a property whilst automatically looking up its caller name.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
internal void Set<T>(T value)
{
var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
Set(value, propertyName);
}


/// <summary>
///     Sets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="name"></param>
internal void Set<T>(T value, string propertyName)
{
Debug.Assert(propertyName != null, "name != null");
if (Equals(value, Get<T>(propertyName)))
return;
_properties[propertyName] = value;
NotifyOfPropertyChange(propertyName);
}


/// <summary>
///     Sets the value of a property based off an Expression (()=>FieldName)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="property"></param>
internal void Set<T>(T value, Expression<Func<T>> property)
{
var propertyName = GetPropertyName(property);


Debug.Assert(propertyName != null, "name != null");


if (Equals(value, Get<T>(propertyName)))
return;
_properties[propertyName] = value;
NotifyOfPropertyChange(propertyName);
}


#endregion
}

这段代码使您能够像这样编写属性支持字段:

  public string Text
{
get { return Get<string>(); }
set { Set(value); }
}

此外,在resharper中,如果您创建了一个模式/搜索片段,您还可以通过将简单的prop字段转换为上述支持来自动化您的工作流程。

搜索模式:

public $type$ $fname$ { get; set; }

替换模式:

public $type$ $fname$
{
get { return Get<$type$>(); }
set { Set(value); }
}

我写了一篇有助于解决这个问题的文章(https://msdn.microsoft.com/magazine/mt736453)。您可以使用SolSoft. DataB的NuGet包。然后您可以编写如下代码:

public class TestViewModel : IRaisePropertyChanged
{
public TestViewModel()
{
this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
}


private readonly NotifyProperty<string> m_nameProperty;
public string Name
{
get
{
return m_nameProperty.Value;
}
set
{
m_nameProperty.SetValue(value);
}
}


// Plus implement IRaisePropertyChanged (or extend BaseViewModel)
}

好处:

  1. 基类是可选的
  2. 没有对每个“设定值”的反思
  3. 可以具有依赖于其他属性的属性,并且它们都会自动引发适当的事件(文章有一个示例)

我意识到这个问题已经有无数的答案了,但是没有一个是对的。我的问题是,我不想要任何性能上的冲击,并且愿意仅仅因为这个原因而忍受一点冗长。我也不太关心汽车属性,这导致我有以下的解决方案:

public abstract class AbstractObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;


public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}


protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
{
//Set value if the new value is different from the old
if (!Source.Equals(NewValue))
{
Source = NewValue;


//Notify all applicable properties
foreach (var i in Notify)
OnPropertyChanged(i);


return true;
}


return false;
}


public AbstractObject()
{
}
}

换句话说,如果你不介意这样做,上面的解决方案很方便:

public class SomeObject : AbstractObject
{
public string AnotherProperty
{
get
{
return someProperty ? "Car" : "Plane";
}
}


bool someProperty = false;
public bool SomeProperty
{
get
{
return someProperty;
}
set
{
SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
}
}


public SomeObject() : base()
{
}
}

优点

  • 没有反射
  • 仅在旧值时通知!=新值
  • 一次通知多个属性

缺点

  • 没有自动属性(不过,您可以添加对两者的支持!)
  • 有些冗长
  • 拳击(小型表演命中?)

唉,还是比这样做好,

set
{
if (!someProperty.Equals(value))
{
someProperty = value;
OnPropertyChanged("SomeProperty");
OnPropertyChanged("AnotherProperty");
}
}

对于每一个属性,随着额外的冗长而成为噩梦;-(

请注意,我并不是说这个解决方案的性能比其他解决方案更好,只是对于那些不喜欢其他解决方案的人来说,这是一个可行的解决方案。

我想出了这个基类来实现可观察模式,几乎满足了你的需要(“自动”实现集合和get)。我花了一个小时的时间作为原型,所以它没有很多单元测试,但证明了这个概念。请注意,它使用Dictionary<string, ObservablePropertyContext>来消除对私有字段的需求。

  public class ObservableByTracking<T> : IObservable<T>
{
private readonly Dictionary<string, ObservablePropertyContext> _expando;
private bool _isDirty;


public ObservableByTracking()
{
_expando = new Dictionary<string, ObservablePropertyContext>();


var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
foreach (var property in properties)
{
var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
{
Value = GetDefault(property.PropertyType)
};


_expando[BuildKey(valueContext)] = valueContext;
}
}


protected void SetValue<T>(Expression<Func<T>> expression, T value)
{
var keyContext = GetKeyContext(expression);
var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);


if (!_expando.ContainsKey(key))
{
throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
}


var originalValue = (T)_expando[key].Value;
if (EqualityComparer<T>.Default.Equals(originalValue, value))
{
return;
}


_expando[key].Value = value;
_isDirty = true;
}


protected T GetValue<T>(Expression<Func<T>> expression)
{
var keyContext = GetKeyContext(expression);
var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);


if (!_expando.ContainsKey(key))
{
throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
}


var value = _expando[key].Value;
return (T)value;
}


private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
{
var castedExpression = expression.Body as MemberExpression;
if (castedExpression == null)
{
throw new Exception($"Invalid expression.");
}


var parameterName = castedExpression.Member.Name;


var propertyInfo = castedExpression.Member as PropertyInfo;
if (propertyInfo == null)
{
throw new Exception($"Invalid expression.");
}


return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
}


private static string BuildKey(ObservablePropertyContext observablePropertyContext)
{
return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
}


private static string BuildKey(string parameterName, Type type)
{
return $"{type.Name}.{parameterName}";
}


private static object GetDefault(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}


public bool IsDirty()
{
return _isDirty;
}


public void SetPristine()
{
_isDirty = false;
}


private class KeyContext
{
public string PropertyName { get; set; }
public Type PropertyType { get; set; }
}
}


public interface IObservable<T>
{
bool IsDirty();
void SetPristine();
}

这是用法

public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
{
public ObservableByTrackingTestClass()
{
StringList = new List<string>();
StringIList = new List<string>();
NestedCollection = new List<ObservableByTrackingTestClass>();
}


public IEnumerable<string> StringList
{
get { return GetValue(() => StringList); }
set { SetValue(() => StringIList, value); }
}


public IList<string> StringIList
{
get { return GetValue(() => StringIList); }
set { SetValue(() => StringIList, value); }
}


public int IntProperty
{
get { return GetValue(() => IntProperty); }
set { SetValue(() => IntProperty, value); }
}


public ObservableByTrackingTestClass NestedChild
{
get { return GetValue(() => NestedChild); }
set { SetValue(() => NestedChild, value); }
}


public IList<ObservableByTrackingTestClass> NestedCollection
{
get { return GetValue(() => NestedCollection); }
set { SetValue(() => NestedCollection, value); }
}


public string StringProperty
{
get { return GetValue(() => StringProperty); }
set { SetValue(() => StringProperty, value); }
}
}

我建议使用ReactiveProperty。 这是除了福迪之外最短的方法。

public class Data : INotifyPropertyChanged
{
// boiler-plate
...
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}

而是

public class Data
{
// Don't need boiler-plate and INotifyPropertyChanged


// props
public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

DOCS

虽然显然有很多方法可以做到这一点,但除了AOP神奇的答案之外,似乎没有一个答案是直接从视图模型中设置Model的属性,而无需引用本地字段。

问题是您不能引用属性。但是,您可以使用操作来设置该属性。

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
{
return false;
}


property(newValue);
RaisePropertyChanged(propertyName);
return true;
}

这可以像以下代码提取一样使用。

public int Prop {
get => model.Prop;
set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}

查看此BitBucket存储库以获取该方法的完整实现以及实现相同结果的几种不同方法,包括使用LINQ的方法和使用反射的方法。请注意,这些方法的性能较慢。

我正在编写一个处理INotifyProperties tyChanged的库,主要思想是使用动态代理来通知更改。

仓库在这里:C au ly Kan/不再改变财产

使用此库,您可以编写:

    public dynamic Test1Binding { get; set; }
public TestDTO Test1
{
get { return (TestDTO)Test1Binding; }
set { SetBinding(nameof(Test1Binding), value); }
}

然后让所有的绑定和修改都转到Test1绑定,无论TestDTO有多复杂,它都会自动通知属性更改和集合更改。

它还可以处理依赖项。

    [DependsOn("Test1Binding.TestString")]
public string Test2
{
get { return Test1Binding.TestString; }
}

请给我一些建议。

现在是2022年。现在有了官方的解决方案。

使用Microsoft MVVM Toolkit中的MVVM源生成器

这个

[ObservableProperty]
private string? name;

将产生:

private string? name;


public string? Name
{
get => name;
set
{
if (!EqualityComparer<string?>.Default.Equals(name, value))
{
OnNameChanging(value);
OnPropertyChanging();
name = value;
OnNameChanged(value);
OnPropertyChanged();
}
}
}


// Property changing / changed listener
partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);


protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}


protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

它支持. NET标准2.0和. NET>=5.0。