5

I've got a MVVM-ish application which ended up with a model with way too many property change notifications. Specifically, I'm sometimes missing some notifications because there's too many of them.

For example I end up with properties like this:

public string CustomEmail {
    get => customEmail;
    set
    {
        customEmail = value;
        OnChanged("CustomEmail");
        OnChanged("IsSendAllowed");
        OnChanged("IsNotFaxEmail");
    }
}

Is there a better way to organise it? For example is there a way to mark a property [DependsOn("CustomEmail")] bool IsNotFaxEmail { ... }?

Or if most of the properties are used for bindings, should I be going all in on converters instead? I'd rather not end up with a silly number of converters like {Binding CustomEmail, Converter=EmailIsFaxToElementVisibilityConverter}.

Am I missing some simpler solution?

thatguy
  • 21,059
  • 6
  • 30
  • 40
viraptor
  • 33,322
  • 10
  • 107
  • 191
  • 4
    You could fire the PropertyChanged event with a null or empty property name to notify about a change of all properties. – Clemens Sep 28 '21 at 10:14
  • @Clemens that is an interesting option, thanks... I may resort to that sometimes, but I'd like to be more explicit (also for my own documentation to know the dependencies) – viraptor Sep 28 '21 at 10:17
  • 3
    That `DependsOn` is exactly what we have implemented (cannot share though), so wouldn't be surprised if one of the MVVM frameworks implements this? It's not really hard, just hook NitifyPropertyChanged and keep a map of dependencies, Also these days you'd use `nameof(CustomEmail)`. – stijn Sep 28 '21 at 10:19
  • 2
    AOP (Aspect Oriented Programming) might help with this. For example, look at this Postsharp aspect: https://doc.postsharp.net/inotifypropertychanged-conceptual. You just apply it and it will analyze dependencies itself, by actually inspecting your (compiled) code at build time and injecting all those `OnChanged` calls. – Evk Sep 28 '21 at 10:19
  • Thanks @stijn good call with `nameof` - old habits and all that... – viraptor Sep 28 '21 at 10:28

3 Answers3

6

I don't often find so many dependencies but I can outline a solution I've seen. Create an attribute. Call it AlsoRaise attribute which takes a string parameter. You can probably think of a better name. But I think dependson isn't quite right because it's the other way round.

[AlsoRaise(nameof(IsSendAllowed))]
[AlsoRaise(nameof(IsNotFaxEmail))]
public string CustomEmail

You then have something can drive your list of other properties you're going to raise change notification for as well as CustomEmail.

In a static constructor you fill a dictionary<string, string[]> using those attributes. You iterate public properties, grab those attributes.

In OnChanged you look up your property name in that dictionary and raise property changed for the property name plus any strings you find. Or none of course.

I may have forgotten some part, while since I saw this implementation.

Andy
  • 11,864
  • 2
  • 17
  • 20
  • I was actually thinking of tagging the other way, where `DependsOn` makes sense. I may just implement it as you described - I've been procrastinating on doing it, but it's not *that* hard. – viraptor Sep 28 '21 at 10:33
  • It wasn't hugely complicated. The fiddly bits the iteration of properties and attributes but you should easy find examples if you google. There's probably some detail I've forgotten in the intervening 7... or was that maybe 10... years. – Andy Sep 28 '21 at 10:44
5

You may write a NotifyPropertyChanged method that accepts multiple property names. It does not really reduce the amount of code, but at least allows to make only one method call.

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(params string[] propertyNames)
    {
        var propertyChanged = PropertyChanged;

        if (propertyChanged != null)
        {
            foreach (var propertyName in propertyNames)
            {
                propertyChanged.Invoke(this,
                   new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

public class ViewModel : ViewModelBase
{
    private string customEmail;

    public string CustomEmail
    {
        get => customEmail;
        set
        {
            customEmail = value;

            NotifyPropertyChanged(
                nameof(CustomEmail),
                nameof(IsSendAllowed),
                nameof(IsNotFaxEmail));
        }
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
4

I think PropertyChanged.Fody is exactly what you are looking for. It is a specialized library for Fody, which is a code weaving tool. It enables manipulating the IL code of an assembly on build e.g. for adding boilerplate code to classes like implementing property changed notifications.

PropertyChanged.Fody is very powerful in that it can automatically implement INotifyPropertyChanged. Below is an example from the official repository. It is all you have to write, Fody will add (weave in) the rest.

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
    public string FullName => $"{GivenNames} {FamilyName}";
}

Furthermore, dependent properties like GivenNames and FamilyName in this example are detected and will notify a property change for FullName if they are changed, too. There are lots of options to configure property changed notifications manually through attributes like:

These attributes are the most useful for your scenario. Your sample would be reduced to:

[AlsoNotifyFor("IsSendAllowed")]
[AlsoNotifyFor("IsNotFaxEmail")]
public string CustomEmail { get; set; }

Or the other way around with DependsOn:

[DependsOn("CustomEmail")]
public string IsSendAllowed { get; set; }

[DependsOn("CustomEmail")]
public string IsNotFaxEmail { get; set; }

For more examples and an overview of all attributes and other powerful mechanisms of Fody, have a look at the GitHub Wiki.

thatguy
  • 21,059
  • 6
  • 30
  • 40
  • Yes it is exactly what I'm after :) I wish I could accept multiple answers – viraptor Sep 28 '21 at 11:21
  • I heard this didn't work so well with recent versions of visual studio/framework. If you try it then I'd be interested to hear either way. Roslyn should be able to do all this lark and generate code you can see though. – Andy Sep 29 '21 at 07:10