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

751

微软应该像自动属性那样为INotifyPropertyChanged实现一些简洁的东西,只需指定{get; set; notify;}即可。我认为这很有道理,或者有没有什么复杂性需要处理呢?

我们能否在我们的属性中自己实现类似于“notify”的东西。在您的类中实现INotifyPropertyChanged的优雅解决方案是什么,或者唯一的方法是在每个属性中引发PropertyChanged事件。

如果不行,我们能否编写一些代码来自动生成引发PropertyChanged事件的代码段?


8
"notifypropertyweaver" 可能会有用。 - Ian Ringrose
9
链接失效了。https://github.com/SimonCropp/NotifyPropertyWeaver - prime23
1
尽管有足够的替代选择,但对于我的团队来说,没有什么比Postsharp的[Domain Toolkit]更容易使用了(我认为它将与普通的Postsharp捆绑在即将推出的v3.0中)。在类上使用[NotifyPropertyChanged],在要忽略的属性上使用[NotifyPropertyChangedIgnore]。 - Adam Caviness
7
@joao2fast4u https://github.com/Fody/PropertyChanged/wiki/ConvertingFromNotifyPropertyWeaver - prime23
6
当时由于我们有大量相互依赖的积压问题,因此无法对C#进行更改。所以我猜当MVVM诞生时,我们并没有花太多精力来解决这个问题,我知道Patterns & Practices团队也尝试了几次(因此你也得到了MEF作为该研究线程的一部分)。今天,我认为[CallerMemberName]就是解决上述问题的答案。 - Scott
显示剩余5条评论
35个回答

2
我已经写了一篇有关此事的文章(https://msdn.microsoft.com/magazine/mt736453)。您可以使用SolSoft.DataBinding 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. 可以拥有依赖于其他属性的属性,它们都会自动引发适当的事件(文章中有示例)

2
另一种结合解决方案是使用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); }
    }
}

2
这个快吗?访问堆栈帧是否受某些权限要求的限制?在使用async/await的情况下,这个是否稳健? - Stéphane Gourichon
@StéphaneGourichon 不,不是这样的。在大多数情况下,访问栈帧会对性能造成相当大的影响。 - Bruno Brant
有的,您可以在http://codereview.stackexchange.com/questions/13823/improvements-to-a-viewmodelbase上看到它。 - Ofir
请注意,内联可能会在发布模式下隐藏 get_Foo 方法。 - bytecode77
在CallerMemberName出现之前,这是必要/有用的,这是想法吗? - StayOnTarget

2

我用以下方法解决了这个问题(虽然有点麻烦,但在运行时肯定更快)。

在VB中(抱歉,但我认为在C#中翻译它并不难),我使用正则表达式进行了以下替换:

(?<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?

In

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中写很多编译好的代码...如果有一天我写出来了,我会告诉你的!


2

如果您在.NET 4.5中使用动态语言,就不必担心INotifyPropertyChanged了。

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

如果“Name”与某个控件绑定,那么它就可以正常工作。

1
使用它的任何缺点? - juFo

1

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

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

(文档)


1
我正在编写一个处理INotifyPropertyChanged的库,主要思想是使用动态代理来通知更改。
这里是仓库链接:CaulyKan/NoMorePropertyChanged 使用这个库,你可以编写如下代码:
    public dynamic Test1Binding { get; set; }
    public TestDTO Test1
    {
        get { return (TestDTO)Test1Binding; }
        set { SetBinding(nameof(Test1Binding), value); }
    }

然后将所有绑定和修改都指向Test1Binding,无论TestDTO有多复杂,它都会自动通知PropertyChange和CollectionChanged。

它还可以处理依赖关系。

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

请给我一些建议。

1

我使用以下扩展方法(使用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); }
}

1
使用这个。
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;
    }
}

}


1
一个使用反射的想法:
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

这很酷,我比表达式方法更喜欢它。不过缺点是速度可能会慢一些。 - nawfal

1

在实现这些属性时,您可能需要考虑的其他事项是INotifyPropertyChanged和PropertyChanged事件都使用事件参数类。

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

请查看此实现及其产生原因的解释。

Josh Smith的博客


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接