Search code examples
c#inotifypropertychangedpostsharp

Trigger OnPropertyChanged in PostSharp's INotifyPropertyChanged aspect manually


I'm using the NotifyPropertyChanged aspect from PostSharp.

I have a situation where I need to trigger the PropertyChanged event manually, because my use case seems too complex for PostSharp to figure out. Here's the relevant excerpt from my view model:

private RelayCommand _authenticateCommand;
[IgnoreAutoChangeNotificationAttribute]
public ICommand AuthenticateCommand
{
    get { return _authenticateCommand ?? (_authenticateCommand = new RelayCommand(Authenticate, _ => IsNotAuthenticated)); }
}

private void Authenticate(object obj)
{
    var result = _dialogService.OpenAuthenticationDialog();
    if (result.HasValue && result.Value)
    {
        _mainViewModel.Update();
    }
    // At this point I need to trigger NotifyPropertyChanged for IsNotAuthenticated,
    // because the authentication dialog causes changes
    // to the _authenticationViewModel's IsLoggedIn property.
}

public bool IsNotAuthenticated
{
    get
    {
        return !_authenticationViewModel.IsLoggedIn;
    }
}

As described in my comment in the code above, when the authentication dialog is finished, I need to trigger the NotifyPropertyChanged event for IsNotAuthenticated, because it's likely that the dialog changed the value.

Reading the documentation, it seems like it should be easy:

Target classes can customize the NotifyPropertyChangedAttribute aspect by implementing one or many of the members documented in the NotifyPropertyChangedAttributeTargetClass class.

and from the NotifyPropertyChangedAttributeTargetClass.OnPropertyChanged page:

Raises the PropertyChanged event. If this method is already present in the target code, the NotifyPropertyChangedAttribute aspect will use it to raise the PropertyChanged event. Otherwise, the aspect will introduce the method into the target class.

Great, so I tried implementing an empty OnPropertyChanged method in my view model, thinking that it would be replaced by PostSharp's version, but alas, it instead just broke all notification logic. This is how the generated code looks before I replace OnPropertyChanged:

protected void OnPropertyChanged(string propertyName)
{
    this.<>z__aspect0.OnPropertyChanged(propertyName);
}

and this is how it looks after:

protected void OnPropertyChanged(string propertyName)
{
    this.<>z__aspect0.OnMethodEntry(null);
    try
    {
    }
    finally
    {
        this.<>z__aspect0.OnMethodExit(null);
    }
}

Testing the code, it works in the first case, and not in the second. So my implementation of an empty OnPropertyChanged causes the feature to stop working.

I guess the documentation is lying? It explicitly says (emphasis mine)

If this method is already present in the target code, the NotifyPropertyChangedAttribute aspect will use it to raise the PropertyChanged event." -- but the generated code above and my testing says otherwise.

What I've ended up doing "in the meantime", until I figure out what I am or PostSharp is doing wrong, is create my own aspect, like this:

[Serializable]
class PropertyChangedTriggerAttribute : MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        var inpc = args.Method.DeclaringType;
        if (inpc == null) throw new InvalidOperationException("PropertyChangedTriggerAttribute used on an object that doesn't have an OnPropertyChanged(System.String) method.");
        var opc = inpc.GetMethod("OnPropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
        var value = (string) args.Arguments.GetArgument(0);
        opc.Invoke(args.Instance, new object[] { value });
    }
}

And then tag a method in my class with it:

[PropertyChangedTrigger]
private void TriggerOnPropertyChanged(string propertyName)
{   
}

Now I can call that, and it in turn triggers OnPropertyChanged for me.

Any ideas on what I'm doing wrong here? Anyone gotten this to work for them?


Solution

  • I think you have misunderstood the following:

    Raises the PropertyChanged event. If this method is already present in the target code, the NotifyPropertyChangedAttribute aspect will use it to raise the PropertyChanged event. Otherwise, the aspect will introduce the method into the target class.

    If the method is already present in the class, NotifyPropertyChangedAttribute will use it. That means that the method needs to do the actual notification, because aspect is using it instead of it's own implementation. Hence, you need to raise PropertyChanged event. That implies that you need to implement INotifyPropertyChanged interface yourself like this:

    [NotifyPropertyChanged]
    public class TestClass : INotifyPropertyChanged
    {
        public int Property1 { get; set; }
        public int Property2 { get; set; }
    
        public event PropertyChangedEventHandler PropertyChanged;
    
        public void DoSomething()
        {
            this.Property1 = 42;
            this.OnPropertyChanged( "Property2" );
        }
    
        protected void OnPropertyChanged( string propertyName )
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if ( handler != null )
                handler( this, new PropertyChangedEventArgs(propertyName) );
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            TestClass test = new TestClass();
            INotifyPropertyChanged inpc = Post.Cast<TestClass, INotifyPropertyChanged>(test);
    
            inpc.PropertyChanged += ( s, ea ) =>
            {
                Console.WriteLine("Notification received for {0}", ea.PropertyName);
            };
    
            test.DoSomething();
        }
    }
    

    This way, it is going to work as you want, i.e. the output would be:

    Notification received for Property2
    Notification received for Property1
    

    Note that the order is like this because Property1 change is raised automatically (by the aspect) when the DoSomething method is exiting.