每当属性的值更改时,如何触发事件?

70

有一个属性,它被命名为ImageFullPath1

public string ImageFullPath1 {get; set; }

每当值改变时,我希望触发一个事件。我知道可以使用INotifyPropertyChanged进行更改,但我想使用事件来实现。

6个回答

173

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

29
目前为止,你是这个帖子中唯一一个正确执行了事件的空值检查的人,给你点赞。 - Simon P Stevens
@jp2code 这是 C# 的一个特性,而不是 .NET 的。你需要知道 var?.Invoke 就像是三元运算符的一种变体(如果 var 不为 null,则为 Invoke...),只是它更简化了形式。 - Lou Watson
1
由于编译器会在处理rvalues之前处理lvalues,因此ImageFullPathChanged?.Invoke...将始终消除竞争条件,即如果为null,则永远不会调用ImageFullPathChanged。这只是语法糖,Roslyn会在构建时处理它。需要检查IL输出以验证,但相当确定。 - Lou Watson
你说Richard的实现不是“安全”的,我很好奇你所说的“安全”是什么意思? - n00dles
5
我会将调用OnPropertyChanged("ImageFullPath");的参数替换为nameof(ImageFullPath)。这样,您就可以在编译时检查属性名称,以便在更改它时,如果忘记在方法调用中替换它,就会收到错误消息。 - meJustAndrew
显示剩余5条评论

37

我使用和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方法设置为虚拟的,以允许子类重写它来捕获属性更改。

12
现在使用.NET 4.5,您甚至可以免费使用CallerMemberNameAttribute获取属性名称。http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.callermembernameattribute.aspx。 - fsimonazzi
这个很好用。感谢提供的示例。CallerMemberName加1。 - curob
1
自上次编辑以来时间已经过去,有些事情发生了变化。也许更好的调用事件委托的方式是:PropertyChanged?.Invoke(this, e); - Tzwenni

9

当属性发生变化时触发事件,这正是 INotifyPropertyChanged 的作用。实现 INotifyPropertyChanged 接口需要一个必要的成员,即 PropertyChanged 事件。你自己实现的任何内容都可能与该实现完全相同,因此不使用它没有任何优势。


2
即使您想为每个属性实现单独的“XChangedEvent”,+1 也是真实的。因此,请继续实现INotifyPropertyChanged。未来(如WPF)将感谢您,因为这就是未来希望您做的事情。 - Greg D

5
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*/);
    }
}                        

话虽如此,我完全同意Ryan的观点。这正是为什么INotifyPropertyChanged存在的原因。


3
这个事件调用存在竞态条件。在检查和后续调用之间,ImageFullPath1Changed的值可能会更改为null。不要像这样调用事件! - Aaronaught
2
你对ImageFullPath1Changed事件的null检查不够安全。由于事件可以异步地从类外部订阅/取消订阅,因此在你进行null检查后,它可能会变成null并导致NullReferenceException异常。相反,你应该在检查null之前先取一个本地副本。请参考Aaronaught的答案。 - Simon P Stevens

4
如果你将属性更改为使用后备字段(而不是自动属性),你可以执行以下操作:
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
1
@Simon P Stevens - 感谢提供的信息。已更新答案以反映最新情况。 - Oded
@Oded 我尝试使用你的方法,但对于上面的代码 handler(this, e), e 在当前上下文中不存在。我有什么遗漏吗? - Abhijeet
1
@autrevo - 这里的 e 只是一个例子。你需要传入一个 EventArgs 的实例。如果你没有要传递的实例,可以使用 EventArgs.Empty - Oded
@Simon P Stevens 我更喜欢使用以下代码: public event EventHandler ImageFullPath1Changed = delegate {}; 这样就避免了需要检查 null 的情况... - madrang

0

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

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



 

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