How to implement INotifyPropertyChanged in C# 6.0?
Asked Answered
O

4

24

The answer to this question has been edited to say that in C# 6.0, INotifyPropertyChanged can be implemented with the following OnPropertyChanged procedure:

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

However, it isn't clear from that answer what the corresponding property definition should be. What does a complete implementation of INotifyPropertyChanged look like in C# 6.0 when this construction is used?

Oneman answered 23/2, 2016 at 15:56 Comment(3)
The other question / answer already contained all the bits... Each set would just be set { SetField(ref name, value); }. The SetField method was shown in full.Swag
@MarcGravell, Yes, but it wasn't clear to me whether the C#5 and C#6 additions were meant to augment or supersede the SetField bit, and I can't request clarification on that question, so I had to ask a new question. I'm glad I did because seeing the entire class written out removes all ambiguity and makes it very easy to understand.Oneman
Actually, that's C# 5.Chihuahua
I
34

After incorporating the various changes, the code will look like this. I've highlighted with comments the parts that changed and how each one helps

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); }
    }
}
Indign answered 23/2, 2016 at 16:26 Comment(15)
What is result value of SetField used for?Disremember
It's true if a replacement was actually made and the event fired. It isn't used in this snippetIndign
If the class contains SetField, then what is the purpose of OnPropertyChanged? Is it expected that some properties will call OnPropertyChanged directly, bypassing SetField? Or is OnPropertyChanged there to satisfy some now-obsolete convention?Oneman
@PanagiotisKanavos, since the question is about C#-6, could you incorporate nameof?Leopoldine
@PauloMorgado the OP asked how some other answer's code would look after all the updates. I answered simply because I realized that it really was hard to incorporate the changes if you didn't already know how all the parts worked. If you check the comments in the original answer, you'll see that there are performance considerations as well, which is why lambdas aren't usedIndign
Downvoter, please read what was asked first, then explain the relevant objection. The questions was "how would the code look after all the updates"? If you thinkIndign
@T.C shouldn't you be asking marc_s in the original question for that? It is not some old convention though, it's simple separation of concerns. OnPropertyChanged makes raising the event easier and nothing more. Without it you'd have to spread the code that creates the EventArg parameter, and raises the event everywhere. Not nice. SetField does something completely different - it encapsulates setting a field and notifying.Indign
@PauloMorgado where would nameof help? Although that should be a comment to marc_s. Will ping him to update his answer and answer the commentsIndign
@PanagiotisKanavos, the usual example is having FirstName, LastName and FullName properties where the FullName property is a combination of the other two and, thus, any change to the first two should trigger a notification change of the last one.Leopoldine
@panagiotis-kanavos, the reason I asked about OnPropertyChanged is because Resharper complains about the structure you proposed with the warning "Explicit argument passed to parameter with caller info attribute". I know Resharper isn't always correct about best practice, but I figured I should ask anyway. In any case, I find your justification for OnPropertyChanged compelling. Furthermore, I can now think of cases where it is necessary, such as cases where SetField must be bypassed because, for instance, the type in question doesn't have an equality comparer.Oneman
@panagiotis-kanavos: Regarding your suggestion that I communicate directly with the contributors to the other question, I'll go one step further and suggest that I should have posted my initial question as a comment under the answer there. However, the StackOverflow rules don't me to do either of those things, so this is the best I can do.Oneman
Why SetField returns true/false?Argive
This post suggests that this could be simplified even further by omitting EqualityComparer<T>.Default (and just using Object.Equals). Would you agree?Cleanse
@Cleanse not at all. Object.Equals only compares the references, it doesn't check equality as defined by the type. Why raise a change event if the previous and new values are considered equal? Using Object.Equals may be suitable for that authors particular case but it's not a generic solutionIndign
In c#6 you can also use lambda-like-syntax for getters and setters: get => name; set => SetField(ref name, value);Spinet
T
15

I use the same logic in my project. I have a base class for all view models in my app:

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

Every view model inherits from this class. Now, in the setter of each property I just need to call OnPropertyChanged().

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

Why does it work?

[CallerMemberName] is automatically populated by the compiler with the name of the member who calls this function. When we call OnPropertyChanged from Initialized, the compiler puts nameof(Initialized) as the parameter to OnPropertyChanged

Another important detail to keep in mind

The framework requires that PropertyChanged and all properties that you're binding to are public.

Tartary answered 24/2, 2016 at 0:21 Comment(4)
This could be a good answer to the original question, but a bad answer here. The OP asked how some existing code would look after multiple updates. At best, this should be a comment hereIndign
Besides, you miss the point of the original answer - instead of spreading hard-coded value checks and notifications calls everywhere, can you encapsulate all this in a generic way that allows custom equality comparisons ?Indign
I disagree with the criticisms. Looks great to me. There are things you can do with comparisons, but... mehSwag
I guess I emphasized on the code sample too much, and I should have emphasized "Why does it work?". Everyone, focus on "Why does it work?"Tartary
D
5

I know this question is old, but here is my implementation

Bindable uses a dictionary as a property store. It's easy enough to add the necessary overloads for a subclass to manage its own backing field using ref parameters.

  • No magic string
  • No reflection
  • Can be improved to suppress the default dictionary lookup

The code:

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

used like this

public class Item : Bindable
{
     public Guid Id { get { return Get<Guid>(); } set { Set<Guid>(value); } }
}
Dale answered 5/7, 2017 at 15:33 Comment(8)
There are 2 problems with this approach. Firstly watch out for boxing\unboxing of value types and secondly it loses type safety. A caller could call Set<string>("myProp") but Get<int>("myProp") which would cause an invalid cast exception. So it is down to the caller to ensure it is called correctly. Granted it's fairly safe to assume that a caller won't do that but still as a general rule you'd want to design that possibility outGalah
@1adma12 boxing\unboxing? loses type safety? invalid cast exception? could you explain further those three? I use this code in many places and any chance to improve it would be great!Dale
Info on boxing\unboxing in C# is available and the type safety issue is that the compiler won't stop you "Get"-ing an value of a different type to the one you original "Set"). Now arguably these are theoretical issues and if you are happy with the trade-offs there's not need to change anything. Indeed WPF's dependency properties work in a similar way to yours and they have accepted similar trade-offs. My point was more to explain why this isn't the generally accepted pattern for INotifyPropertyChanged. You will typically see properties backed with private fields.Galah
More on boxing/unboxing: learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/…Galah
@1adma12 I see now, if Get<int> and Set<string> I would get an invalid cast exception. any thoughts on how I would handle or prevent this?Dale
There isn't much you can do about. As soon as you cast to Object you loose type safety by definition. Just accept the trade-off - the convenience of not having to create individual backing fields vs. the (likely imperceptible) performance penalty of using an Object dictionary & the potential for a run time exception. If you are satisfied that the benefit outweighs the drawbacks then stick with it.Galah
@Aaron.S I am personally a big fan of this approach. Thank you for your innovative answer. Also something to note about the type safety issue. I do not see it as an issue because you will get compiler errors if your Get<T> or Set<T> generic types differ from what is expected by the property type - which enforces some type safety when you compile your code.Errolerroll
@JasonLokiSmith agreed! I couldn't figure out the type safety issue or even how it could be one. thanks for the confirmation on that!Dale
G
0

Instead of implementing the INotifyPropertyChanged manually in all classes, which can require a lot of boilerplate code, you can consider using tools that generate the code for you. Several tools are available:

Greenwood answered 6/12, 2023 at 11:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.