0

I have a simple WPF application and I would like to know why NotifyOfPropertyChange() is not working as I expect. I have a MainWindowViewModel with two properties and one button, and when I click the button I call NotifyOfPropertyChange() to notify that all the properties have changed. I also have a list of properties compiled in the ViewModel constructor:

properties = typeof(MainWindowViewModel).GetProperties()
    .Where(p => p.DeclaringType == typeof(MainWindowViewModel));

In the constructor, I have subscribed to PropertyChanged:

PropertyChanged += (sender, args) =>
{
    if (properties.Any(p => p.Name == args.PropertyName))
        IsDirty = true;
};

Here is my entire MainViewModel:

public class MainWindowViewModel : Screen
{
    private string name;
    private IEnumerable<PropertyInfo> properties;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            NotifyOfPropertyChange(() => Name);
        }
    }

    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            age = value;
            NotifyOfPropertyChange(() => Age);
        }
    }

    private bool isDirty;

    public bool IsDirty
    {
        get { return isDirty; }
        set
        {
            isDirty = value;
            NotifyOfPropertyChange(() => IsDirty);
        }
    }

    public MainWindowViewModel()
    {
        // get list of class properties
        properties = typeof(MainWindowViewModel).GetProperties()
            .Where(p => p.DeclaringType == typeof(MainWindowViewModel));


        // if any property has been updated, set isDirty to true
        PropertyChanged += (sender, args) =>
        {
            if (properties.Any(p => p.Name == args.PropertyName))
                IsDirty = true;
        };

    }

    public void Save()
    {
        NotifyOfPropertyChange();
    }
}

When the application runs, the constructor correctly produces the list of properties: Name, Age and IsDirty. However, when the Save button is clicked, the PropertyChangedEvent is raised for other properties not associated with the viewmodel: IsInitialized, and IsActive, which are properties of screen, and is not raised for any properties in the list. Can someone tell me what is going on here or give an alternate solution? I think its pretty clear what I'm trying to do, this is a validation scenario, and I need to call a PropertyChanged and set a flag if the save button is clicked, so that all properties can be validated.

Will
  • 421
  • 2
  • 8
  • 23
  • Set breakpoints on the setters of any properties in MainWindowViewModel. Are the setters actually called? (If yes, continue stepping through the code of the setter *into* NotifyOfPropertyChange, to see what that method is really is doing...) –  Jun 02 '17 at 17:41
  • No I don't believe NotifyOfPropertyChange() calls the actual setter. – Will Jun 02 '17 at 17:45
  • Well, of course NotifyOfPropertyChange is not calling the setter. Note that the method is not called "RequestPropertyChange" or "ChangeProperty" but rather "NotifyOfPropertyChange"... Got it? :-) So, where in your code is a setter of one of your properties called? –  Jun 02 '17 at 17:46
  • I'm not calling any setters, not setting any actual properties. I'm just calling NotifyOfPropertyChange(), with no argument, so that the PropertyChanged event will be called on ALL properties. And it is being called, just not on any properties in my viewmodel. – Will Jun 02 '17 at 17:56
  • Ah, i see. NotifyOfPropertyChange() is a CalibrunMicro feature right? Hold on a second... –  Jun 02 '17 at 18:01
  • I wish i could help. Which version of Caliburn.Micro are you using? I checked the current version at https://github.com/Caliburn-Micro/Caliburn.Micro, but it does not provide a parameter-less overload of the NotifyOfPropertyChange method... (could it perhaps be that the parameter-less NotifyOfPropertyChange() overload is an implementation made by you or one of your colleagues...?) –  Jun 02 '17 at 18:06
  • (By the way, triggering a property change notification event without property name should result in an event argument without a property name -- which would represent a notification event meaning any number of properties have changed. Something about that NotifyOfPropertyChange() implementation smells fishy -- it shouldn't send multiple notification events, but rather only one with PropertyChangedEventArgs.Name being null or empty...) –  Jun 02 '17 at 18:14
  • 3.1. If you look closely at the PropertyChanged class, NotifyOfPropertyChange method, it is actually raising the .NET OnPropertyChanged event, which has a default parameter. – Will Jun 02 '17 at 18:15
  • I see, i missed that CallerMemberAttribute when i was looking first there. But calling NotifyOfPropertyChange() from the Save method should then only result in one notification with the property name being "Save" (i.e. the name of the caller, which is a method in this case) -- which of course wouldn't make much sense, but that's what it should do... –  Jun 02 '17 at 18:19

1 Answers1

2

The method signature of the NotifyOfPropertyChange() method is:

public virtual void NotifyOfPropertyChange([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null)

(Link to Caliburn.Micro github repository: https://github.com/Caliburn-Micro/Caliburn.Micro/blob/master/src/Caliburn.Micro/PropertyChangedBase.cs#L44)

Note the CallerMemberName attribute.

Calling it from within the Save method like NotifyOfPropertyChange(); will result in a PropertyChanged event with the PropertyChangedEventArgs.PropertyName set to "Save" (the name of the method calling NotifyOfPropertyChange()). That of course doesn't make sense.

To signal that any property in your class has changed, you have to explicitly pass either null or "" as argument for NotifyOfPropertyChange (effectively bypassing the CallerMemberName substitution):

public void Save()
{
    NotifyOfPropertyChange(null);
}

Passing null or an empty string as property name is valid. A PropertyChanged event without a property name signals that any one or multiple properties have changed their values.

Considering this, the PropertyChanged event handler should be improved as well to properly respect PropertyChanged events without a property name:

PropertyChanged += (sender, args) =>
{
    if (string.IsNullOrEmpty(args.PropertyName) || properties.Any(p => p.Name == args.PropertyName))
        IsDirty = true;
};