3

I've written the following extension method:

public static void NotifyChanged<T>(this INotifyPropertyChanged inpc, ref T current, T newValue, Action<PropertyChangedEventArgs> eventRaiser, [CallerMemberName] string? name = null) where T : IEquatable<T> { if (current.Equals(newValue)) { return; } current = newValue; eventRaiser(new PropertyChangedEventArgs(name)); } 

that can be used like this:

public class Foo : Bar, INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; private string? rootExpression; public string? RootExpression { get => rootExpression; set => this.NotifyChanged(ref rootExpression, value, args => PropertyChanged?.Invoke(this, args)); } } 

This saves much of the boilerplate of writing INPC-aware properties.

However, I now get a compiler warning error at the call to NotifyChanged:

The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'INotifyPropertyChangedExtensions.NotifyChanged(INotifyPropertyChanged, ref T, T, Action, string?)'. Nullability of type argument 'string?' doesn't match constraint type 'System.IEquatable'.

AFAICT the error is saying that string? cannot be cast to IEquatable<string?>, only string can be cast to IEquatable<string>.

How can I resolve this? Apply some attribute? Or something else?

1 Answer 1

6

Your problem is:

where T : IEquatable<T> 

This says that T must be a non-nullable IEquatable<T>. You want it to be nullable. You can say this by adding a ?:

where T : IEquatable<T>? 

Note that this will then complain on if (current.Equals(newValue)), which will throw if current is null.


The normal way to do this is not by constraining T to be an IEquatable<T>, but to use EqualityComparer<T>.Default. If T implements IEquatable<T>, this gives you an equality comparer which calls IEquatable<T>.Equals, otherwise it falls back to calling the normal object.Equals.

This also solves your issue of an NRE if current is null:

public static void NotifyChanged<T>( this INotifyPropertyChanged inpc, ref T current, T newValue, Action<PropertyChangedEventArgs> eventRaiser, [CallerMemberName] string? name = null) { if (EqualityComparer<T>.Default.Equals(current, newValue)) { return; } current = newValue; eventRaiser(new PropertyChangedEventArgs(name)); } 

It's also rare to pass in eventRaiser: normally you'd make NotifyChanged a method on a base class which implements INotifyPropertyChanged, rather than making it an extension method. Then you can just let NotifyChanged raise the PropertyChanged event itself, or you write another method such as OnPropertyChanged which raises PropertyChanged and call that from NotifyChanged.

If you do want to pass in the event to raise, you could just pass in the PropertyChangedEventHandler:

public static void NotifyChanged<T>( this INotifyPropertyChanged _, ref T current, T newValue, PropertyChangedEventHandler eventHandler, [CallerMemberName] string? name = null) { if (EqualityComparer<T>.Default.Equals(current, newValue)) { return; } current = newValue; eventHandler?.Invoke(this, new PropertyChangedEventArgs(name)); } this.NotifyChanged(ref rootExpression, value, PropertyChanged); 
Sign up to request clarification or add additional context in comments.

5 Comments

I can't let NotifyChanged raise the event, because it's an extension method; a class's events can only be raised within that class.
I'm trying to avoid the boilerplate of checking equality and raising the event for each property. Ideally, I would inherit from an INPC-implementing base class with a NotifyChanged method as you suggested; but I can't do that in this case: I have to inherit from a specific class which I cannot modify.
@ZevSpitz In which case, that specific bit at the end doesn't apply to you. The first two sections do, however
But I do like your suggestion of passing in the handler instead of a delegate.
also works with: where T : class?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.