Sunday, February 14, 2016

CallerMemberName vs. nameof() in INotifyPropertyChanged

Last month, we took a look at some of the framework and language changes that make it easier to implement INotifyPropertyChanged in our classes. Two of those changed included the "CallerMemberName" attribute and the "nameof()" expression.

Antoine left a great question since the functionality of "CallerMemberName" and "nameof()" seem so similar:
I don't see the point of using nameof() instead of the CallerMemberName though, is it about performance?
Let's take a closer look, first to see if there are any performance differences, and then see an example of why I prefer to use "nameof()": calculated properties.

Review: Using a String Parameter Directly
Let's look at why we want to use either "CallerMemberName" or "nameof()". For this, we'll do a quick review of how we implemented INotifyPropertyChanged before we had these.


This is a typical implementation of INotifyPropertyChanged (although you'll probably notice that we're using the null-conditional operator that we learned about before).

We could then call "RaisePropertyChanged" in our property setter by passing in the name of the property as a string:


This works, but wherever we have quoted strings, we have good places for things to break. For example, if we type the name of the property incorrectly, it will not work. If we change the name of our property, it's likely that our refactoring tool will not change the string, and it will not work.

So we'd like an option where we don't have to use a quoted string for the property name.

Review: Using "CallerMemberName" Attribute
One option is to use the "CallerMemberName" attribute (which was added to the framework in .NET 4.5), we put the attribute into our implementation of INotifyPropertyChanged:


Because the "CallerMemberName" attribute is placed before the first parameter of our "RaisePropertyChanged" method, the name of the calling item will be substituted here. So that means our property setter can look like this:


Notice that we do not pass a parameter to "RaisePropertyChanged". But because we have the attribute on the parameter, the string "FirstName" (the name of our property) is passed in automatically.

This fixes our initial problem because we no longer have a quoted string to worry about.

For more information on the "CallerMemberName" attribute, see "Using the CallerMemberName Attribute for Better XAML Data Binding".

Review: Using the "nameof()" Expression
A new option that we got with C# 6 (and the Visual Studio 2015 compiler) is the "nameof()" expression. To use this, we don't need to do anything special with our implementation of "INotifyPropertyChanged":


Notice that this implementation is exactly the same as our first example where we use the string parameter directly.

But instead of using a string parameter, we use the "nameof()" expression in the property setter:


When we use "nameof(FirstName)" in our code, it will pass the string "FirstName" as a parameter to "RaisePropertyChanged", so we get the correct value. And this has the added benefit of not being a quoted string.

This means that if we type "FirstName" incorrectly, we will get a compiler error. In addition, if we rename the property, our refactoring tools will automatically update the "nameof()" expression as well.

So why do I prefer "nameof()" to "CallerMemberName"?

Are There Performance Differences?
The first thing to do is look at performance. It turns out that all three of these implementations generate the same compiled code.

To take a look at the sample code, go to the "property-change-tracker" project on GitHub: jeremybytes/property-change-tracker. Inside of the "TestHelpers.Tests" project, there are classes that have all three implementations:


FakeClassStandardProperties
The "FakeClassStandardProperties" uses the quoted string parameters. If we look at the IL (intermediate language) that the compiler generates for the "FirstName" property, we see this code:


(Just a snippet). Notice the string "FirstName". This quoted string is used as the parameter for the next line, which is a call to "RaisePropertyChanged".

That's not much of a surprise, but let's look at the other classes.

FakeClassCallerMemberName
The "FakeClassCallerMemberName" uses the "CallerMemberName" attribute like we saw above. Let's look at the IL that is generated by the compiler:


This also has the quoted string "FirstName" which then gets used as a parameter for "RaisePropertyChanged". The compiler generates the same code as if we pass in the string directly.

So this means that there is no performance difference (at least at runtime), when we use the "CallerMemberName" attribute in this sceneario.

FakeClassNameOf
The "FakeClassNameOf" uses the "nameof()" expression like we saw above. Here's the IL that's generated:


Again we see the "FirstName" quoted string in our IL.

So this means that there is no performance difference if we use the "nameof()" expression in this scenario.

With no performance differences, why do I prefer "nameof()" over "CallerMemberName"? Let's take a look at another scenario: calculated properties.

Consistency and Calculated Properties
I've written a lot of user-facing applications. And quite often, these applications would have calculated properties -- properties that were based on 1 or more other properties. Here's an example:


If we have an order form that has both "Quantity" and "Price" properties, then we may also want a "TotalPrice" property that is calculated from these values.

In this case, if the "Quantity" is updated, then I want the "TotalPrice" property to be recalculated. In our data binding world, this means that we also need to call "RaisePropertyChanged" for our "TotalPrice" property.

Using the CallerMemberName Attribute
If we use the "CallerMemberName" attribute in this scenario, then our "Quantity" property may look like this:


The first "RaisePropertyChanged" (without a parameter), will get "Quantity" passed in. But for the second call to "RaisePropertyChanged", we need to explicitly pass a parameter so that we can also raise the PropertyChanged event for "TotalPrice".

Now, we know that using a hard-coded string here is not a good idea. That's the whole reason we want to use one of these other features. So, it makes more sense to update the code like this:


So the first call to "RaisePropertyChanged" (without the parameters) still raises the PropertyChanged event for "Quantity", but then the second call will raise it for "TotalPrice". And again, we get the benefits of not using a hard-coded string here.

Consistency with the nameof() Expression
And this is where I want to take things one step further. If I only have *one* call to "RaisePropertyChanged", then I don't have to do much thinking -- of course, this will reference the current property.

But when I have *two* calls to "RaisePropertyChanged", and one of them doesn't have any parameters, I need to start thinking a little more. Instead of this, I'd rather just use the "nameof()" expression for both calls.


This gives my brain something that is really easy to process. Without having to think about details, I know that this raises the PropertyChanged event for both "Quantity" and "TotalPrice".

Wrap Up
The "CallerMemberName" attribute and the "nameof()" expression both give us the same key benefits: (1) they eliminate the quoted string in our properties, and (2) they don't affect runtime performance.

But once we start to look at other scenarios -- like having calculated properties -- we see that "nameof()" can give us consistency in our properties that makes them easier to read and understand. And when we're dealing with code, human readability is a key feature.

In the applications I've spent most of my career with, calculated properties are pretty common. Because of that, I prefer the "nameof()" expression. But if you have classes that have no calculated properties (which is common when using entity classes), you may get better readability by using the "CallerMemberName" attribute.

A big part of making these types of decisions is to understand our environment. This helps us make the right decision for our particular application.

Happy Coding!

5 comments:

  1. In fact, if `CallerMemberName` is gone, it's even better to not allow the parameter to be optional. That way, you don't forget what's going on and it's very explicit and easy to process when you really mean `RaisePropertyChanged(string.Empty)` (binding lingo for "all properties changed").

    ReplyDelete
    Replies
    1. Hi Joseph. In my code, I've been using the optional parameter since default parameters were available. Previously, I would pass the explicit "null" to signify that all properties should be re-bound, but I found that having the default as "null" worked well (I don't remember where I picked up this pattern from; I didn't come up with it myself). This easily was understandable before "CallerMemberName" came along.

      According to the PropertyChanged documentation, either "null" or "String.Empty" can be used to indicate that all properties have changed. I've been using "null" in my code. I haven't considered removing the parameter default, but it's something worth considering.

      Delete
  2. 'This means that if we type "FirstName" incorrectly, we will get a compiler error.'

    That depends on how we typed it incorrectly.
    Is there anything we can do to ensure we're not accidentally using `nameof(firstName)` and notifying for the field rather than the property `nameof(FirstName)`?

    ReplyDelete
  3. Hi, Jeremy. Have you looked into https://github.com/Fody/PropertyChanged? It will save you from these problems almost entirely.It's pretty slick!

    BTW - I had the fortune of attending some of your talks at CodeStock this year. They were great!

    ReplyDelete
    Replies
    1. Hi Derrick. I have a love/hate relationship with code injectors. They do some pretty cool stuff, but then debugging gets more "interesting" since the code doesn't match the IL. At some point, I'll just have to get over it and move on. Thanks for the tip; it looks pretty cool. INotifyPropertyChanged is one of those things that should be automatic anyway.
      -Jeremy

      Delete