14

Given a standard view model implementation, when a property changes, is there any way to determine the originator of the change? In other words, in the following view model, I would like the "sender" argument of the "PropertyChanged" event to be the actual object that called the Prop1 setter:

public class ViewModel : INotifyPropertyChanged
{
    public double Prop1
    {
        get { return _prop1; }
        set
        {
            if (_prop1 == value)
                return;
            _prop1 = value;

            // here, can I determine the sender?
            RaisePropertyChanged(propertyName: "Prop1", sender: this);
        }
    }
    private double _prop1;

    // TODO implement INotifyPropertyChanged
}

Alternatively, is it possible to apply CallerMemberNameAttribute to a property setter?

McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • 2
    The `sender` parameter of the PropertyChanged event must always be `this`. Otherwise you would break the usage of `INotifyPropertyChanged`. Notice that `PropertyChangedEventArgs` only contains a property name and not which instance that belongs to. The instance that the property belongs to must be the sender. If you don't pass `this` the code that tries to access the property by name will throw an exception. – shf301 Nov 02 '13 at 00:23
  • @shf301 I can see that `sender` would logically be the instance that owns the event ... but that is not in any way *required*, is it? Or are you saying that WPF/Silverlight bindings use the `sender` parameter to retrieve the updated value? – McGarnagle Nov 02 '13 at 00:37
  • How else would the consumer of the `NotifyPropertyChanged` event know which object raised the event? What if an object registers to `NotifyPropertyChanged` on two different objects? – shf301 Nov 02 '13 at 00:44
  • @shf301 it's an interesting point, so I tested it -- it seems that a WPF binding still works if the `sender` is some other object. Presumably the subscribing object/binding holds a reference to the source object, and doesn't rely on `sender`. – McGarnagle Nov 02 '13 at 00:58
  • "but that is not in any way required, is it?" Unless you know every single subscriber to your class, you should assume that somewhere, one might rely on `INotifyPropertyChanged` being implemented as documented. It's not that I *expect* your idea to break, but if it does, the blame lies solely with you, and there is a real possibility that it *could* break on, say, the not yet existing .NET 5, third-party controls, property forwarders, plenty of other helper classes that all hook into `PropertyChanged`. –  Dec 24 '13 at 22:42
  • Have you considered using commands instead? In a command you can pass in a parameter. What MVVM framework are you using? I have used MVVM light in the past and was able to do something similar. – Eric Scherrer Dec 31 '13 at 19:00

4 Answers4

20

If I understood correctly, you're asking about the caller of the setter. That means, the previous method call in the call stack before getting to the setter itself (which is a method too).

Use StackTrace.GetFrames method for this. For example (taken from http://www.csharp-examples.net/reflection-callstack/):

using System.Diagnostics;

[STAThread]
public static void Main()
{
  StackTrace stackTrace = new StackTrace();           // get call stack
  StackFrame[] stackFrames = stackTrace.GetFrames();  // get method calls (frames)

  // write call stack method names
  foreach (StackFrame stackFrame in stackFrames)
  {
    Console.WriteLine(stackFrame.GetMethod().Name);   // write method name
  }
}

The output:

Main
nExecuteAssembly
ExecuteAssembly
RunUsersAssembly
ThreadStart_Context
Run
ThreadStart

Basically, what you're asking for would be stackFrames[1].GetMethod().Name.

Tengiz
  • 8,011
  • 30
  • 39
  • 1
    Great, this is what I asked for. Using `stackFrames[1].GetMethod().Type` gets the type of the class that called the setter. Thanks! – McGarnagle Jan 01 '14 at 11:47
  • 2
    be aware the `StackTrace` object is fairly useless in release builds with optimised code and no debug info. Check your release build, the info may not be there. – CAD bloke Feb 17 '20 at 03:49
5

My first approach to your problem would be to derive from PropertyEventArgs. The new class would have a member called, for instance PropertyChangeOrigin in addition to PropertyName. When you invoke the RaisePropertyChanged, you supply an instance of the new class with the PropertyChangeOrigin set from the information gleaned from the CallerMemberName attribute. Now, when you subscribe to the event, the subscriber could try casting the eventargs to your new class and use the information if the cast is successful.

Noctis
  • 11,507
  • 3
  • 43
  • 82
Boluc Papuccuoglu
  • 2,318
  • 1
  • 14
  • 24
1

This is what I always use as a middle-ground between INotifyPropertyChanged and my View Models:

public class NotifyOnPropertyChanged : INotifyPropertyChanged
{
    private IDictionary<string, PropertyChangedEventArgs> _arguments;

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    public void OnPropertyChanged([CallerMemberName] string property = "")
    {
        if(_arguments == null)
        {
            _arguments = new Dictionary<string, PropertyChangedEventArgs>();
        }

        if(!_arguments.ContainsKey(property))
        {
            _arguments.Add(property, new PropertyChangedEventArgs(property));
        }

        PropertyChanged(this, _arguments[property]);
    }
}

Two things here. It uses the [CallerMemberName] attribute to set the property name. This makes the usage syntax as follows:

public string Words
{
    set
    {
        if(value != _words)
        {
            _words = value;
            OnPropertyChanged( );
        }
    }
}

Beyond that, it stores the PropertyChangedEventArgs object in a dictionary so it's not created a ton of times for properties that are frequently set. I believe this addresses your problem. Good luck!

Will Custode
  • 4,576
  • 3
  • 26
  • 51
0

Whenever I have had to pass in extra information down into a VM I have a great success with using commands:

Commands, RelayCommands and EventToCommand

Eric Scherrer
  • 3,328
  • 1
  • 19
  • 34