70

There is a property, it's named ImageFullPath1

public string ImageFullPath1 {get; set; }

I'm going fire an event whenever its value changed. I am aware of changing INotifyPropertyChanged, but I want to do it with events.

wonea
  • 4,783
  • 17
  • 86
  • 139
Mohammad Dayyan
  • 21,578
  • 41
  • 164
  • 232

6 Answers6

173

The INotifyPropertyChanged interface is implemented with events. The interface has just one member, PropertyChanged, which is an event that consumers can subscribe to.

The version that Richard posted is not safe. Here is how to safely implement this interface:

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

Note that this does the following things:

  • Abstracts the property-change notification methods so you can easily apply this to other properties;

  • Makes a copy of the PropertyChanged delegate before attempting to invoke it (failing to do this will create a race condition).

  • Correctly implements the INotifyPropertyChanged interface.

If you want to additionally create a notification for a specific property being changed, you can add the following code:

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

public event EventHandler ImageFullPathChanged;

Then add the line OnImageFullPathChanged(EventArgs.Empty) after the line OnPropertyChanged("ImageFullPath").

Since we have .Net 4.5 there exists the CallerMemberAttribute, which allows to get rid of the hard-coded string for the property name in the source code:

    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();
            }
        }
    }
Oliver
  • 43,366
  • 8
  • 94
  • 151
Aaronaught
  • 120,909
  • 25
  • 266
  • 342
  • 29
    +1 for being the only person in this thread so far to get the null check on the event correct. – Simon P Stevens Feb 11 '10 at 18:55
  • @Aaronaught: How to wire up the event-method with event?could you please explain.. I mean, where do I need to write the implementation for the event? – techBeginner Oct 26 '11 at 18:33
  • Why use the local var "handler", why not just if (ImageFullPathChanged != null) ImageFullPathChanged(this, e); – dlchambers Jul 14 '15 at 14:03
  • VS2015 wants me to simplify the `OnImageFullPathChanged` code to `ImageFullPathChanged?.Invoke(this, e)`. Will the race condition still exist for this code? I assume it would, but I do not understand the .NET architecture that well. –  Sep 29 '16 at 16:43
  • @jp2code that's a feature of c# not .NET and so you know var?.Invoke is like a variation of a ternary if (var != null) ? Invoke... in simplified form – Lou Watson Oct 03 '16 at 23:52
  • @LouisWatson, I understand that. I am also used to setting a var to an object, but if an object goes out of scope so does the var (i.e. `EventHandler handler = ImageFullPathChanged;\nif (handler != null)...` –  Oct 04 '16 at 10:56
  • 1
    since the compiler will process lvalues before rvalues ImageFullPathChanged?.Invoke... will always negate the race condition, i.e. ImageFullPathChanged will never be invoked if it is null. Its just syntactic sugar which Roslyn would then process on build. would need to check IL output to verify but pretty certain. – Lou Watson Oct 11 '16 at 01:06
  • You say Richard's implementation is not 'safe', I'm curious as to what you mean by 'safe'? – n00dles Jul 06 '17 at 23:49
  • I would like to suggest an edit to this outstanding answer you have created. I believe where your answer says _Then add the line OnImageFullPathChanged(EventArgs.Empty) after the line OnPropertyChanged("ImageFullPath")_. Should be the following: Then add the line OnImageFullPathChanged(EventArgs.Empty) **in place of** the line OnPropertyChanged("ImageFullPath"). – Mike Sportsman Sep 07 '17 at 18:46
  • 5
    I would replace the parameter from call of `OnPropertyChanged("ImageFullPath");` with `nameof(ImageFullPath)`. This way you will have compile time checking of the name of the property so in case you will ever change it, you will receive an error message if you forget to replace it in the method call too. – meJustAndrew Mar 02 '18 at 13:08
37

I use largely the same patterns as Aaronaught, but if you have a lot of properties it could be nice to use a little generic method magic to make your code a little more 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
}

Usually I also make the OnPropertyChanged method virtual to allow sub-classes to override it to catch property changes.

wonea
  • 4,783
  • 17
  • 86
  • 139
Mikael Sundberg
  • 4,607
  • 4
  • 32
  • 36
  • 12
    Now with .NET 4.5 you can even get the property name for free using the CallerMemberNameAttribute http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.callermembernameattribute.aspx. – fsimonazzi Oct 25 '12 at 17:40
  • This is working great. Thanks for the example. +1 for the CallerMemberName – curob Aug 31 '16 at 16:43
  • 1
    Time went since last edit and some things changed. Maybe better way to invoke event delegate: `PropertyChanged?.Invoke(this, e);` – Tzwenni Sep 02 '20 at 09:24
9

Raising an event when a property changes is precisely what INotifyPropertyChanged does. There's one required member to implement INotifyPropertyChanged and that is the PropertyChanged event. Anything you implemented yourself would probably be identical to that implementation, so there's no advantage to not using it.

Ryan Brunner
  • 14,723
  • 1
  • 36
  • 52
  • 2
    +1 for truth. Even if you want to implement a separate `XChangedEvent` for every property, you're already doing the work, so go ahead and implement INotifyPropertyChanged too. The future (like WPF) will thank you, because that's what the future expects you to do. – Greg D Feb 11 '10 at 18:48
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*/);
    }
}                        

That said, I completely agree with Ryan. This scenario is precisely why INotifyPropertyChanged exists.

Richard Berg
  • 20,629
  • 2
  • 66
  • 86
  • 3
    This event invocation has a race condition. The value of `ImageFullPath1Changed` can change to `null` between the check and the subsequent invocation. Do not invoke events like this! – Aaronaught Feb 11 '10 at 18:52
  • 2
    Your check for null of the ImageFullPath1Changed event is not safe. Given that events can be subscribed/unsubscribed to/from asynchronously from outside of your class, it could become null after your null check and cause a NullReferenceException. Instead, you should take a local copy before checking for null. See Aaronaught's answer. – Simon P Stevens Feb 11 '10 at 18:54
4

If you change your property to use a backing field (instead of an automatic property), you can do the following:

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);
    }
  }
}
Oded
  • 489,969
  • 99
  • 883
  • 1,009
  • Your check for null of the ImageFullPath1Changed event is not safe. Given that events can be subscribed/unsubscribed to/from asynchronously from outside of your class, it could become null after your null check and cause a NullReferenceException. Instead, you should take a local copy before checking for null. See Aaronaught's answer – Simon P Stevens Feb 11 '10 at 18:54
  • 1
    @Simon P Stevens - thanks for the info. Updated answer to reflect. – Oded Feb 11 '10 at 19:12
  • @Oded I tried using your approach, but for the above code `handler(this, e), e does not exist in current context` Am I mssing something? – Abhijeet Nov 10 '12 at 05:59
  • 1
    @autrevo - The `e` is simply an example. You need to pass in an instance of `EventArgs`. If you don't have any to pass, you can use `EventArgs.Empty`. – Oded Nov 10 '12 at 08:09
  • @Simon P Stevens I prerfer using. public event EventHandler ImageFullPath1Changed = delegate {}; Then avoid having to check for null.... – madrang Mar 20 '14 at 05:47
0

There is already have good answers but some people are still confused

  • EventArgs and who they can use it
  • And some of them about how to pass custom parameters
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; }
    }



 
Jameel Nazir
  • 134
  • 2
  • 7