【问题标题】:Raise an event whenever a property's value changed?每当属性值发生变化时引发事件?
【发布时间】:2011-01-15 20:22:51
【问题描述】:

有一个属性,名为ImageFullPath1

public string ImageFullPath1 {get; set; }

只要它的值发生变化,我就会触发一个事件。我知道要更改 INotifyPropertyChanged,但我想用事件来做。

【问题讨论】:

    标签: c# .net events properties


    【解决方案1】:

    INotifyPropertyChanged 接口用事件实现的。该接口只有一个成员PropertyChanged,这是一个消费者可以订阅的事件。

    Richard 发布的版本不安全。下面是如何安全地实现这个接口:

    public class MyClass : INotifyPropertyChanged
    {
        private string imageFullPath;
    
        protected void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
                handler(this, e);
        }
    
        protected void OnPropertyChanged(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }
    
        public string ImageFullPath
        {
            get { return imageFullPath; }
            set
            {
                if (value != imageFullPath)
                {
                    imageFullPath = value;
                    OnPropertyChanged("ImageFullPath");
                }
            }
        }
    
        public event PropertyChangedEventHandler PropertyChanged;
    }
    

    请注意,它会执行以下操作:

    • 抽象属性更改通知方法,以便您可以轻松地将其应用于其他属性;

    • 在尝试调用 PropertyChanged 委托之前复制它(如果不这样做将产生竞争条件)。

    • 正确实现INotifyPropertyChanged接口。

    如果您想另外为正在更改的特定属性创建通知,您可以添加以下代码:

    protected void OnImageFullPathChanged(EventArgs e)
    {
        EventHandler handler = ImageFullPathChanged;
        if (handler != null)
            handler(this, e);
    }
    
    public event EventHandler ImageFullPathChanged;
    

    然后在OnPropertyChanged("ImageFullPath")行之后添加OnImageFullPathChanged(EventArgs.Empty)行。

    因为我们有 .Net 4.5,所以存在 CallerMemberAttribute,它允许摆脱源代码中属性名称的硬编码字符串:

        protected void OnPropertyChanged(
            [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }
    
        public string ImageFullPath
        {
            get { return imageFullPath; }
            set
            {
                if (value != imageFullPath)
                {
                    imageFullPath = value;
                    OnPropertyChanged();
                }
            }
        }
    

    【讨论】:

    • +1 表示到目前为止,该线程中唯一一个对事件进行空值检查的人是正确的。
    • @Aaronaught:如何将事件方法与事件联系起来?您能解释一下吗?我的意思是,我需要在哪里编写事件的实现?
    • 为什么使用本地变量“handler”,为什么不直接使用 if (ImageFullPathChanged != null) ImageFullPathChanged(this, e);
    • 因为编译器将在右值之前处理左值 ImageFullPathChanged?.Invoke... 将始终否定竞争条件,即如果 ImageFullPathChanged 为空,则永远不会调用它。它只是 Roslyn 将在构建时处理的语法糖。需要检查 IL 输出来验证但非常确定。
    • 我会将来自OnPropertyChanged("ImageFullPath"); 调用的参数替换为nameof(ImageFullPath)。这样,您将在编译时检查属性名称,以便在您更改它时,如果您忘记在方法调用中替换它,您将收到一条错误消息。
    【解决方案2】:

    我使用的模式与 Aaronaught 基本相同,但如果您有很多属性,最好使用一点通用方法魔术来让您的代码更丰富一点DRY

    public class TheClass : INotifyPropertyChanged {
        private int _property1;
        private string _property2;
        private double _property3;
    
        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) {
            PropertyChangedEventHandler handler = PropertyChanged;
            if(handler != null) {
                handler(this, e);
            }
        }
    
        protected void SetPropertyField<T>(string propertyName, ref T field, T newValue) {
            if(!EqualityComparer<T>.Default.Equals(field, newValue)) {
                field = newValue;
                OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
            }
        }
    
        public int Property1 {
            get { return _property1; }
            set { SetPropertyField("Property1", ref _property1, value); }
        }
        public string Property2 {
            get { return _property2; }
            set { SetPropertyField("Property2", ref _property2, value); }
        }
        public double Property3 {
            get { return _property3; }
            set { SetPropertyField("Property3", ref _property3, value); }
        }
    
        #region INotifyPropertyChanged Members
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        #endregion
    }
    

    通常我还将 OnPropertyChanged 方法设为虚拟,以允许子类覆盖它以捕获属性更改。

    【讨论】:

    • 现在使用 .NET 4.5,您甚至可以使用 CallerMemberNameAttribute msdn.microsoft.com/en-us/library/… 免费获取属性名称。
    • 这很好用。谢谢你的例子。为 CallerMemberName +1
    • 自上次编辑时间过去了,有些事情发生了变化。调用事件委托的更好方法:PropertyChanged?.Invoke(this, e);
    【解决方案3】:

    当属性改变时引发事件正是 INotifyPropertyChanged 所做的。实现 INotifyPropertyChanged 需要一个成员,那就是 PropertyChanged 事件。你自己实现的任何东西都可能与那个实现相同,所以不使用它没有任何好处。

    【讨论】:

    • +1 表示真相。即使您想为每个属性实现一个单独的XChangedEvent,您也已经完成了这项工作,因此请继续实现 INotifyPropertyChanged。未来(如 WPF)会感谢你,因为这是未来希望你做的事情。
    【解决方案4】:
    public event EventHandler ImageFullPath1Changed;
    
    public string ImageFullPath1
    {
        get
        {
            // insert getter logic
        }
        set
        {
            // insert setter logic       
    
            // EDIT -- this example is not thread safe -- do not use in production code
            if (ImageFullPath1Changed != null && value != _backingField)
                ImageFullPath1Changed(this, new EventArgs(/*whatever*/);
        }
    }                        
    

    也就是说,我完全同意瑞恩的观点。这种情况正是 INotifyPropertyChanged 存在的原因。

    【讨论】:

    • 此事件调用存在竞争条件。在检查和后续调用之间,ImageFullPath1Changed 的值可以更改为 null。不要调用这样的事件!
    • 您对 ImageFullPath1Changed 事件的 null 检查不安全。鉴于可以从您的类外部异步订阅/取消订阅事件,它可能会在您的 null 检查后变为 null 并导致 NullReferenceException。相反,您应该在检查 null 之前获取本地副本。请参阅 Aaronaught 的回答。
    【解决方案5】:

    如果您将属性更改为使用支持字段(而不是自动属性),您可以执行以下操作:

    public event EventHandler ImageFullPath1Changed;
    private string _imageFullPath1 = string.Empty;
    
    public string ImageFullPath1 
    {
      get
      {
        return imageFullPath1 ;
      }
      set
      {
        if (_imageFullPath1 != value)
        { 
          _imageFullPath1 = value;
    
          EventHandler handler = ImageFullPathChanged;
          if (handler != null)
            handler(this, e);
        }
      }
    }
    

    【讨论】:

    • 您对 ImageFullPath1Changed 事件的 null 检查不安全。鉴于可以从您的类外部异步订阅/取消订阅事件,它可能会在您的 null 检查后变为 null 并导致 NullReferenceException。相反,您应该在检查 null 之前获取本地副本。查看 Aaronaught 的回答
    • @Simon P Stevens - 感谢您提供的信息。更新了反映的答案。
    • @Oded 我尝试使用你的方法,但是对于上面的代码handler(this, e), e does not exist in current context我在发什么消息吗?
    • @autrevo - e 只是一个例子。您需要传入EventArgs 的实例。如果没有要通过的,可以使用EventArgs.Empty
    • @Simon P Stevens 我更喜欢使用。公共事件 EventHandler ImageFullPath1Changed = 委托 {};然后避免必须检查 null....
    【解决方案6】:

    已经有了很好的答案,但有些人仍然感到困惑

    • EventArgs 和谁可以使用它
    • 其中一些关于如何传递自定义参数
    class Program
        {
            static void Main(string[] args)
            {
                Location loc = new Location();
    
                loc.LocationChanged += (obj, chngLoc) =>
                {
                    Console.WriteLine("Your LocId Is");
                    Console.WriteLine(chngLoc.LocId);
                    Console.WriteLine(chngLoc.LocCode);
                    Console.WriteLine(chngLoc.LocName);
                    Console.ReadLine();
                };
    
                Console.WriteLine("Default Location Is");
                Console.WriteLine(loc.LocId);
    
                Console.WriteLine("Change Location");
                loc.LocId = Console.ReadLine();
            }
        }
    
        public class Location
        {
    
            private string _locId = "Default Location";
            public string LocId
            {
                get
                {
                    return _locId;
                }
                set
                {
    
                    _locId = value;
                    if (LocationChanged != null && value != LocId)
                    {
                        B1Events b1 = new B1Events();
                        b1.LocCode = "Changed LocCode";
                        b1.LocId = value;
                        b1.LocName = "Changed LocName";
                        LocationChanged(this, b1);
                    }
                    
                }
            }
             public event EventHandler<B1Events> LocationChanged;
        }
    
        public class B1Events : EventArgs
        {
            public string LocId { get; set; }
            public string LocCode{ get; set; }
            public string LocName { get; set; }
        }
    
    
    
     
    

    【讨论】: