如何在C# 6.0中实现INotifyPropertyChanged接口?

24

针对这个问题的答案已经进行了编辑,新版中指出在C# 6.0中,可以通过以下的OnPropertyChanged过程来实现INotifyPropertyChanged接口:

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

然而,从那个答案中并不清楚相应的属性定义应该是什么样子。当使用这种方式时,在C# 6.0中,实现完整的INotifyPropertyChanged的样子是什么?


1
另一个问题/答案已经包含了所有的要点...每个集合只是set { SetField(ref name, value); }SetField方法已经完整地显示出来了。 - Marc Gravell
2
@MarcGravell,是的,但我不确定C#5和C#6的添加是要增强还是取代SetField位,而且我无法请求澄清这个问题,所以我不得不提出一个新问题。我很高兴我这样做了,因为看到整个类写出来消除了所有的歧义,使它非常容易理解。 - T.C.
实际上,那是C# 5。 - BrainSlugs83
3个回答

34

在吸收了各种变化之后,代码看起来会像这样。我已经用注释突出显示了更改的部分以及它们如何帮助。

public class Data : INotifyPropertyChanged
{ 
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        //C# 6 null-safe operator. No need to check for event listeners
        //If there are no listeners, this will be a noop
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // C# 5 - CallMemberName means we don't need to pass the property's name
    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 { return name; }
        //C# 5 no need to pass the property name anymore
        set { SetField(ref name, value); }
    }
}

1
SetField 的结果值用于什么? - dadhi
1
@panagiotis-kanavos:关于您建议我直接与其他问题的贡献者沟通,我会更进一步地建议我应该将我的初始问题作为评论发布在那里的答案下面。然而,StackOverflow的规则不允许我这样做,所以这是我能做到的最好的。 - T.C.
1
为什么SetField返回true/false? - Francesco Bonizzi
这篇文章建议通过省略 EqualityComparer<T>.Default(直接使用 Object.Equals)来进一步简化。您同意吗? - kmote
2
在C#6中,您还可以使用类似lambda的语法来编写getter和setter: get => name; set => SetField(ref name, value); - mmix
显示剩余10条评论

15

我在我的项目中使用相同的逻辑。我有一个基类用于应用程序中所有视图模型:

using System.ComponentModel;
using System.Runtime.CompilerServices;

public class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

每个视图模型都继承自这个类。现在,只需要在每个属性的setter中调用OnPropertyChanged()即可。

public class EveryViewModel : PropertyChangedBase
{
    private bool initialized;
    public bool Initialized
    {
        get
        {
            return initialized;
        }
        set
        {
            if (initialized != value)
            {
                initialized = value;
                OnPropertyChanged();
            }
        }
    }

为什么它能够工作?

[CallerMemberName] 是编译器自动填充的函数调用成员名称。当我们从 Initialized 中调用 OnPropertyChanged 时,编译器会将 nameof(Initialized) 作为参数放入到 OnPropertyChanged 中。

另一个需要记住的重要细节是

框架要求 PropertyChanged 和您绑定的所有属性都必须是 public


1
这可能是原问题的一个好答案,但在这里却是一个不好的答案。OP问了一下经过多次更新后现有代码会是什么样子。最好的情况是,在这里应该是一个评论。 - Panagiotis Kanavos
1
此外,您忽略了原始答案的重点 - 而不是在各处传播硬编码值检查和通知调用,您能否以通用方式封装所有这些内容,以允许自定义相等比较? - Panagiotis Kanavos
1
我不同意这些批评。在我看来很棒。虽然比较有时候也有一些可以做的事情,但是...呃 - Marc Gravell
我想我过分强调了代码示例,而应该强调“为什么它能工作?”。大家都要关注“为什么它能工作?” - Amadeusz Wieczorek

5

我知道这个问题很旧了,但以下是我的实现方式。

Bindable使用一个字典作为属性存储。很容易添加必要的重载方法,让子类使用ref参数来管理自己的后备字段。

  • 无需幻数字符串
  • 无需反射
  • 可以改进以抑制默认字典查找

代码如下:

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

        /// <summary>
        /// Gets the value of a property
        /// <typeparam name="T"></typeparam>
        /// <param name="name"></param>
        /// <returns></returns>
        protected T Get<T>([CallerMemberName] string 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>
        protected void Set<T>(T value, [CallerMemberName] string 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)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

使用方法如下

public class Item : Bindable
{
     public Guid Id { get { return Get<Guid>(); } set { Set<Guid>(value); } }
}

这种方法有两个问题。首先要注意值类型的装箱\拆箱,其次它会丢失类型安全性。调用方可能会调用 Set<string>("myProp") 但却 Get<int>("myProp"),这将导致无效的强制转换异常。因此,调用方需要确保正确调用它。尽管可以合理地假设调用方不会这样做,但通常情况下,您会希望设计时考虑到这种可能性。 - 1adam12
@1adma12 装箱\拆箱?会丧失类型安全性吗?无效的转换异常?您能进一步解释这三个问题吗?我在许多地方都使用这段代码,有没有改进的机会呢? - Aaron. S
C#中有关装箱/拆箱的信息是可用的,类型安全问题在于编译器不会阻止您从与原始“设置”不同的类型中“获取”值。现在可以说这些是理论问题,如果您对权衡感到满意,则无需更改任何内容。实际上,WPF的依赖属性以类似的方式工作,并且它们已经接受了类似的权衡。我的观点更多地是为了解释为什么这不是INotifyPropertyChanged的通常接受模式。您通常会看到由私有字段支持的属性。 - 1adam12
更多关于装箱/拆箱的内容:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing - 1adam12
1
@JasonLokiSmith 同意!我无法确定类型安全问题,甚至不知道它可能是什么。感谢你的确认! - Aaron. S
显示剩余3条评论

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