实现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个回答

745

如果不使用类似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"); }
}

这并不是很庞大的代码库;如果需要,它还可以用作基类。从SetField返回的bool告诉您是否是无操作,以防您想应用其他逻辑。


使用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和可空引用类型,代码将如下所示:
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);
}

5
不错的技巧,Marc!我建议使用lambda表达式而不是属性名称来改进,可以看看我的答案。 - Thomas Levesque
12
@Thomas - lambda表达式固然很好,但对于实际上非常简单的东西来说,它增加了很多开销。虽然这是个方便的技巧,但我不确定它总是实用的。 - Marc Gravell
17
@Marc - 是的,这可能会降低性能... 但我真的很喜欢它在编译时被检查的事实,并且被 "重命名" 命令正确重构。 - Thomas Levesque
7
幸运的是,使用C#5,你无需妥协 - 你可以通过(正如Pedro77所指出的)[CallerMemberName]同时获得最好的两种方式。 - Marc Gravell
6
@Gusdor 语言和框架是分开的;你可以使用C# 5编译器,将目标设置为.NET 4,并且只需自己添加缺失的属性- 它就会正常工作。它只需要具有正确的名称并位于正确的命名空间中即可,无需在特定程序集中。 - Marc Gravell
显示剩余40条评论

206

.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

更多示例在这里这里

此外,请参见Caller Information (C# 和 Visual Basic)


14
太棒了!但为什么它是通用的? - abatishchev
3
它是由C# 5.0引入的。它与.net 4.5无关,但这是一个很好的解决方案! - J. Lennon
6
@J.Lennon,.NET 4.5仍然与此有关,毕竟属性来自某个地方。http://msdn.microsoft.com/en-au/library/system.runtime.compilerservices.callermembernameattribute.aspx - Daniel Little
@Lavinski 将你的应用程序更改为例如 .NET 3.5 并查看在 vs2012 中会发生什么。 - J. Lennon
有没有一种简洁的方法来使用这个,当你只有公共的getter并且正在使用私有的setter与ChangeMyProperty(string value)类型的方法调用来遵循更多的DDD模式?我假设[CallerMemberName]在这种方法下会报告错误? - Dib
显示剩余4条评论

170

我非常喜欢 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;
}

顺便说一下,这篇文章的灵感来自于这篇博客文章


7
有至少一个采用这种方法的框架,名为ReactiveUI - AlSki
2
@BrunoBrant 你确定会有性能问题吗? 根据博客文章所说,反射发生在编译时而不是运行时(即静态反射)。 - Nathaniel Elkins
1
@NathanielElkins 您是正确的。我记得使用表达式时实际上会有性能损失,但原因是之后需要编译表达式。另一个提示:如果用户指向子级别的属性,例如“foo.bar.baz”,则此方法将失败。您可以扩展算法以涵盖该情况。 - Bruno Brant
8
我相信你整个的OnPropertyChanged<T>在C# 6的nameof运算符下已经过时,这使得代码更加简洁。 - ASA
8
@Traubenfuchs,实际上,C#5的CallerMemberName属性使其变得更加简单,因为您根本不需要传递任何内容... - Thomas Levesque
显示剩余2条评论

143

还有Fody,它具有一个AddINotifyPropertyChangedInterface插件,可让您编写以下代码:

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

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


9
我认为这正是 OP 在问“我们自己能否在属性中实现类似于'notify'的东西。在您的类中实现INotifyPropertyChanged是否有优雅的解决方案”时所寻找的内容。 - Ashoat
3
这确实是唯一优雅的解决方案,并且像@CADbloke说的那样完美地运行。我开始也对这个“编织器”持怀疑态度,但我检查/重新检查了它背后的IL代码,发现它完美无缺、简单易懂,实现了你所需的一切而没有多余的东西。它还钩子和调用任何你在基类中指定的方法名称,无论是NotifyOnProp...、OnNotify...都可以,因此与您可能拥有的任何基类以及实现INotify...的基类都能很好地配合使用。 - NSGaga-mostly-inactive
2
你可以轻松地检查编织器正在执行的操作,查看构建输出窗口,它会列出所有已编织的 PropertyChanged 事项。使用正则表达式模式 "Fody/.*?:",LogCustom2,True 的 VScolorOutput 扩展可以将其高亮显示为“Custom 2”颜色。我将其设为亮粉色,以便更容易找到。只需对所有内容进行编织,这是处理大量重复键入的最简洁方式。 - CAD bloke
@mahmoudnezarsarhan 不是的,我记得配置方式有轻微变化,但Fody PropertyChanged仍然活跃。 - Larry
1
它似乎已经从Fody中被移除了。 - Damien
1
我同意@Damien的观点。从版本3.4.0开始,该属性已被弃用。按照文档建议使用AddINotifyPropertyChangedInterfaceAttribute对我来说很有效。 - Examath

72

我认为人们应该更加关注性能;当需要绑定大量对象时(比如有 10,000+ 行的网格),或者对象的值经常改变(实时监控应用程序),性能确实会影响 UI。

我尝试了多种在这里和其他地方找到的实现并进行了比较。请查看INotifyPropertyChanged 实现的性能比较


以下是结果预览Implemenation vs Runtime


16
没有性能开销:CallerMemberName在编译时被转换为文字值。只需尝试反编译您的应用程序即可。 - JYL
以下是有关编程的内容的翻译,仅返回翻译后的文本:这是相应的问题和答案:https://dev59.com/WWEh5IYBdhLWcg3wTB1c - uli78
1
@JYL,你说得对,CallerMemberName并没有增加太大的开销。上次我尝试实现时可能做错了什么。稍后我会更新博客和答案,以反映CallerMemberName和Fody实现的基准测试结果。 - Peijen
2
如果您在用户界面中有一个10,000+的网格,则应该结合多种方法来处理性能,例如分页,每页仅显示10、50、100、250个结果... - Austin Rhymer
1
Austin Rhymer,如果您有大量数据(超过50个),请使用数据虚拟化,无需加载所有数据,它只会加载当前滚动显示区域可见的数据! - Bilal
链接已经失效,因此这不再有用。 - StayOnTarget

40

我在我的博客http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/中介绍了一个Bindable类。

Bindable使用字典作为属性包。很容易添加必要的重载来使用引用参数管理子类自己的后备字段。

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

代码:

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

2
这是一个不错的解决方案,但唯一的缺点是涉及装箱/拆箱的小性能损失。 - MCattle
1
我建议使用 protected T Get<T>(T defaultValue, [CallerMemberName] string name = null),并在 Set 中检查 if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))(以在第一次设置为默认值时触发并保存)。 - Miquel
1
@Miquel 添加自定义默认值支持肯定是有用的,但是你应该小心,只有在值实际改变时才触发更改事件。将属性设置为其原有的相同值不应该引发事件。我必须承认,在大多数情况下,这是无害的,但是我曾经遇到过很多次属性被设置成相同值而事件破坏了UI响应性的情况。 - TiMoch
1
@stakx 我有一些应用程序,基于此构建,支持备忘录模式以进行撤消/重做或在无法使用nhibernate的应用程序中启用工作单元模式。 - TiMoch
1
我真的很喜欢这个特定的解决方案:简短的符号表示法,没有动态代理的东西,也没有 IL 干预等。虽然,通过使 Get 返回 dynamic 来消除每次都需要指定 T 的需求,可以使它更短。我知道,这会影响运行时性能,但现在获取器和设置器的代码终于可以始终相同并且在一行中了,感谢上帝!P.S. 在返回值类型为 dynamic 时,你应该在 Get 方法内部(写基类时)格外小心,以返回正确的默认值(这是可以做到的)。 - evilkos
显示剩余3条评论

17

我实际上还没有机会亲自尝试这个,但下一次我要设置一个对INotifyPropertyChanged有很大需求的项目时,我打算编写一个Postsharp属性,在编译时注入代码。类似于:

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

将变成:

private string _firstName;

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

我不确定这在实践中是否有效,我需要坐下来试一下,但我不认为会有问题。如果出现需要触发多个OnPropertyChanged的情况(例如我上面的类中有FullName属性),我可能需要使其接受一些参数。

目前我正在使用Resharper中的自定义模板,但即使如此,我也对所有属性都这么长感到厌烦。


啊,一个快速的谷歌搜索(我应该在写这篇文章之前进行)显示至少有一个人已经做过类似的事情在这里。虽然不完全是我想要的,但足以证明理论是正确的。


7
一个叫做 Fody 的免费工具似乎可以做同样的事情,作为一种通用的编译时代码注入器。它可以在 Nuget 中下载,其中包括其 PropertyChanged 和 PropertyChanging 插件包。 - Triynko

13

是的,肯定有更好的方法。以下是步骤教程,基于这篇有用的文章缩小。

  • 创建新项目
  • 将Castle Core包安装到项目中

Install-Package Castle.Core

  • 仅安装mvvm light库

Install-Package MvvmLightLibs

  • 在项目中添加两个类:

NotifierInterceptor

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);
        }
    }
  • Put bindings into 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>();

  • 享受吧。

enter image description here

注意!!! 所有绑定属性都应该使用关键字virtual进行修饰,因为它们将被Castle代理用于覆盖。


我很想知道你正在使用哪个版本的Castle。我正在使用3.3.0,但CreateClassProxy方法没有这些参数:type要应用的接口拦截器 - IAbstract
算了,我正在使用通用的CreateClassProxy<T>方法。有很大的差别...嗯,想知道为什么通用方法如此受限 :( - IAbstract

10

现在是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。


这个生成器相当痛苦的一点是你必须将属性更改为字段。在我们的大型项目中,这花费了我一些时间。 - ssamko
又是一个不应该需要的解决方法,但这次有大量的魔法:| - t3chb0t
1
@t3chb0t 这并不是说有很多"魔法"。生成的部分类很容易看到。只是使用属性来从私有字段生成等效的公共属性,并将所有样板代码放在一个部分类中。 - WSC
1
@WSC这种语言应该被设计成不需要为“明显的”和“自然的”设计模式生成代码和部分类。 - t3chb0t

8
一个非常类似AOP的方法是在运行时将INotifyPropertyChanged注入到已实例化的对象中。您可以使用类似Castle DynamicProxy的工具来实现这一点。以下是一篇解释该技术的文章:向现有对象添加INotifyPropertyChanged

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