private string _propertyA; public string PropertyA { get { return _propertyA; } set { // P1 repetitive code if (value == _propertyA) return; _propertyA = value; // P2 "magic" string RaisePropertyChanged("PropertyA"); } } private void OnPropertyChanged(object sender, PropertyChangeEventArg e) { // P2 "magic" string usage if (e.PropertyName == "PropertyA") HandlePropertyAChanged(); }
There are two basic problem with this implementation. The first problem is the repetitive code, the 3 lines of code inside set method is pretty much the same for majority of the property and in most implementation it is simply copied and pasted everywhere. The second issue, magic string, is a slightly more challenging problem to deal with. The big deal with the magic string is when code changes and the name of the property changes the programmer has to not only change the string inside the property's setter but everywhere that listens to the PropertyChanged event.
The first problem can be solved by encapsulate the code into a generic method, and there are a few different strategies for dealing with the second problem and we will explore the pros and cons of different solutions. The second problem can be solved by using constant member for the string thus change only need to occur at one place and we gain the refactor support or using reflection. There's also other implementation by leveraging new .Net feature of optional method parameter mark with attribute. What we will do in this article is to explore a few different implementations that attempt to address these two issue and compare the performance of each implementation. As a bonus there is also and implementation that uses weak event handler to address the possibility of memory leak.
Implementations
Simple
The simple implementation is included here as reference. It does use the constant string to represent property name for refactor support.Setter
The setter class introduce a helper method to set property value and raise property changed event.protected bool Set<T>(string propertyName, ref T field, T value) { if (field == null || EqualityComparer<T>.Default.Equals(field, value)) { return false; } field = value; RaisePropertyChanged(propertyName); return true; }The property setter than simply become
private DateTime _time; public DateTime Time { get { return _time; } set { Set(TimeProperty, ref _time, value); } }This help to reduce boilerplate code and make sure the behaviors are consistent.
Delegate Setter
The delegate setter implementation uses Action<T> lambda to update the value so it doesn't have to have the field parameter passed in as reference, the set helper looks like thisprotected bool Set<T>(string propertyName, Action<T> setter, T field, T value) { if (field == null || EqualityComparer<T>.Default.Equals(field, value)) { return false; } setter(value); RaisePropertyChanged(propertyName); return true; }To use the helper method we would need to create an inner class that holds the property value
private class Inner : IDisplayText { public string DisplayText { get; set; } public DateTime Time { get; set; } public bool Status { get; set; } public int Count { get; set; } } private readonly Inner _inner = new Inner();The property setting would then call the helper method like this
public DateTime Time { get { return _inner.Time; } set { Set(TimeProperty, (t) => _inner.Time = t, _inner.Time, value); } }
Lambda
The lambda method utilize the Expression<Func<T>> to capture the property that's been updated and does not rely on constant member for property name. Again a helper method is introduced to encapsulate the property changed behavior. See conclusion about performance difference between .net 4.0 to .net 4.5.protected bool Set<T>(Expression<Func<T>> expression, ref T field, T value) { if (field == null || EqualityComparer<T>.Default.Equals(field, value)) { return false; } field = value; RaisePropertyChanged(GetPropertyName(expression)); return true; } protected string GetPropertyName<T>(Expression<Func<T>> expression) { MemberExpression memberExpression = (MemberExpression)expression.Body; return memberExpression.Member.Name; }
Field
The field implementation introduces a helper class to capture the backing field for the propertyprotected class Field<T> { public string PropertyName; public T Value; public Field(string propertyName) { PropertyName = propertyName; } public static implicit operator T(Field<T> t) { return t.Value; } }Again with a helper method for setting the property
protected bool Set<T>(Field<T> field, T value) { if (field == null || EqualityComparer<T>.Default.Equals(field.Value, value)) return false; field.Value = value; RaisePropertyChanged(field.PropertyName); return true; }The definition of a property looks like this
private readonly Field<DateTime> _time = new Field<DateTime>(TimeProperty); public DateTime Time { get { return _time; } set { Set(_time, value); } }
Lambda Field
Lambda Field implementation looks to improve the runtime performance (see results section) of Lambda implementation by combining Lambda and Field implementation. The Field class definition looks like thisprotected class Field<T> { public string PropertyName; public T Value; public Field(Expression<Func<T>> expression) { PropertyName = GetPropertyName(expression); } protected string GetPropertyName<T>(Expression<Func<T>> expression) { MemberExpression memberExpression = (MemberExpression)expression.Body; return memberExpression.Member.Name; } public static implicit operator T(Field<T> t) { return t.Value; } }The helper method is the same as Field implementation and the definition of property is
private readonly Field<DateTime> _time = new Field<DateTime>(() => Time); public string TimeProperty { get { return _time.PropertyName; } } public DateTime Time { get { return _time; } set { Set(_time, value); } }
Field2
Field2 is a modified Field implementation to remove constant string for property name. It borrows Lambda implementation's method of determining property name. The constants for property names are changed to static readonly fields of the class and are set at application startup by the static constructor to reduce overheadpublic static readonly string TimeProperty; public static readonly string StatusProperty; public static readonly string CountProperty; public static readonly string DisplayTextProperty; static Field2Model() { var dummy = new Field2Model(0); TimeProperty = GetPropertyName(() => dummy.Time); StatusProperty = GetPropertyName(() => dummy.Status); CountProperty = GetPropertyName(() => dummy.Count); DisplayTextProperty = GetPropertyName(() => dummy.DisplayText); } private static string GetPropertyName<T>(Expression<Func<T>> expression) { MemberExpression memberExpression = (MemberExpression)expression.Body; return memberExpression.Member.Name; }The property implementation is the same as Field
private readonly Field<DateTime> _time = new Field<DateTime>(TimeProperty); public DateTime Time { get { return _time; } set { Set(_time, value); } }
CallerMemberName
This implementation uses a .Net 4.5+ attribute CallerMemberNameAttribtue. The implementation simplified all the Setter like implementation by adding a optional parameter and mark the parameter with [CallerMemberName]. The attribute cause the compiler to add the function/property's name as string literal to the method call thus the operation is pretty cheap and has about the same speed as setter.The setter method look like this
protected bool SetThe property implementation is similar to setter without the property name as one of the argument(ref T field, T value, [CallerMemberName]string propertyName = "") { if (field == null || EqualityComparer .Default.Equals(field, value)) { return false; } field = value; RaisePropertyChanged(propertyName); return true; }
private DateTime _time; public DateTime Time { get { return _time; } set { Set(ref _time, value); } }
AOP
The last implementation leverages aspect orientated library such as PostSharp or Fody. PostSharp has some example on how to do it here. I used Fody because it was requested and free.Test Methodology
A test harness was built to bind the models implementing different property changed strategy. The test application binds a collection of models to a grid and them update the models a million times to generate property changed events. A timer is used to measure program execution time, this is not the CPU time, but the result is sufficient for the comparison. The test harness also create 1 to 100000 objects to account for object construction time, as you will see there are implementations where object construction is somewhat expensive.Results
The values showing in the chart is in milliseconds and account for total construction time and property update time.Object Count | Simple | Setter | Lambda | Field | Lambda Field | Field 2 | Field With WEH | Delegate Setter | CMN Attribute | Fody |
1 | 752 | 827 | 1778 | 958 | 1104 | 970 | 955 | 942 | 814 | 815 |
10 | 730 | 814 | 1751 | 956 | 1105 | 959 | 955 | 936 | 810 | 814 |
100 | 733 | 818 | 1765 | 955 | 1103 | 961 | 949 | 938 | 810 | 811 |
1000 | 731 | 842 | 1885 | 959 | 1109 | 956 | 956 | 944 | 814 | 820 |
10000 | 750 | 874 | 2745 | 984 | 1205 | 979 | 981 | 1022 | 870 | 863 |
100000 | 837 | 999 | 2837 | 1122 | 1950 | 1095 | 1123 | 1171 | 985 | 998 |
The execution time for each implementation is pretty much the same across each implementation regardless of object count, the spike up you see for 100,000 object test is the construction cost.
Conclusion
Simple: use this if you don't mind typing or copy and pasteSetter: good implementation for simpler maintenance
Delegate Setter: good implementation if you are wrapping another class such as entity or dto
Lambda: bad performance not recommended. An interesting point here is the runtime went from 7000+ ms to 1700+ ms when project is convert from .net 4.0 to .net 4.5
Field: preferred method of implementation if you need refactor support in event handler, slightly more boiler plate code compare to setter/CallerMemberName implementation
Lambda Field: use this one if you really really want to use Lambda, but it has a performance penalty if your program create lots of objects. CallerMemberName is preferred over this implementation.
Field 2: recommended method if you just hate literal string for some reason but still want refactor support
CallerMemberName: simple to implement and has good performance, the recommended implementation. However you lose the refactor support in event handler.
APO: no boilerplate code, but your lose refactor support and dependency on external libraries.
You can get the source code to the implementations and test harness at github
22 comments:
Outstanding work!
Many thanks!
It's surprising just how slow the lambda method turned out to be in the benchmark. Obviously, it had to be slower than using a constant string, but a whole order of magitude?
By the way, I don't think you need to actually construct the dummy object in "Field 2". Use a type instead of var, and assign it to null:
Field2Model dummy = null;
That will clearly save some space, but it also saves some complexity in the constructor in case you're using the Property values.
You said : "I didn't include [CallerMemberName] implementation it is .Net 4.5 only and the performance is just as bad as Lambda."
I don't think so : CallerMemberName are changed into literal values at compile time, so there is no overhead at runtime.
Jean-Yves,
I actually did implemented and tested [CallerMemberName], and the way I implemented it, it is just as slow as lambda method. Maybe if I didn't have the optimal implementation, but I used popular implementation proposed by many people on the internet. If you can show me the decompiled code* stating otherwise I am willing to give it another shot.
*if it is converted to literal value at compile time, by decompile the code (reflector,ispy,etc) it should show the literal value as parameter of method call in the caller.
Thanks for your reply, Peijen.
I asked a "self answered" question on StackOverflow for that, with my test and conclusion :
http://stackoverflow.com/questions/22580623/inotifypropertychanged-is-callermembername-slow-compared-to-alternatives
Combined to your "SETTER" implementation, this can be a pretty good solution.
Cheers, and thanks for your article.
Great work, thanks!
Any chance you could benchmark PropertyChanged.Fody (http://www.nuget.org/packages/PropertyChanged.Fody/)?
Thank you
@Comphenix,
The dummy = null; change did not yield significant difference. If you look closer the code you are referring to is in the static constructor thus it's only ran once per application.
@JYL,
Thanks for pointing out that I was wrong with [CallerMemberName] performance. I have update the article to reflect that. However it does have a slight downside of losing refactor support, but I guess there are probably ways around it.
@Bruno,
Updated with Fody implementation
I don't get why you would lose refactoring support when using fody. I think it's actually a big improvement over having property names in strings.
Great article!
I've downloaded sorces, file TestResult.cs is missing in package.
@Lev,
I have added TestResult.cs, the file got ignored.
@Leroy
I am talking about event handler side of thing. Prior to C# 6 you have to handle event as such
void EventHandler(obj sender, PropertyChangedEventArg e)
{
if (e.PropertyName == "MyProperty")
{
// do stuff
}
}
With field implementation you can do
if (e.PropertyName == MyPropertyField.PropertyName) ...
However with C# 6 this just become so much easier
if (e.PropertyName == nameof(MyProperty)) ...
I will probably do a follow up article with cleaned up code base some day.
If you want to waste the time of a of people, just leave our code fragments they way they are, and never post working code.
I wasted an hour trying to get your field example to compile.
@Anonymous
Did you check the whole solution posted to github? It's at the end of the page. What error did you encounter?
Could you please clarify why you loose refactor support using CallerMemberName? From my point of view it's the other way around:
Assume you have a property Foo
int Foo { get {return _foo;} set {_foo = value: OnPropertyChanged("Foo")}};
Now assume that you rename the Foo property to MyProperty. Do you got already what i want to mention? No? OnPropertyChanged is not updated! You will still fire a property changed for Foo instead of MyProperty!
int MyProperty{ get {return _foo;} set {_foo = value: OnPropertyChanged("Foo")}};
this will simply fuck up your code within seconds. And you don't get this error at compile time!!! If you have databinding within WPF you not even get a run time error. It simply does not update the UI Item.
I would say if you want to write maintainable code their are only 2 solultions:
- make a base class with OnPropertyChanged using CallerMemberNameAttribute
- or at least the new nameof() operator in the OnPropertyChanged requieres C# 6.0 and is more work as the above solution is more generic.
But maybe you could bring in some light to the dark and explain where you loose refactoring....
@Anonymouse,
You lose refactoring support at the listener. CallerMemberName or nameof() in the OnPropertyChanged will work perfectly when you change the property's name. However if you have an event handler that's listening to events and do something like
private void PropertyChangedHandler(e, args) {
if (args.PropertyName == "FirstName") { UpdateFullName(); }
else if (args.PropertyName == "Email") { VerfyEmail(); }
}
In the handler you are checking for the name of the property changed and do something with it, this is where you would lose refactor support. However C# 6.0's nameof() feature would help with this.
This is an incredible example for practical use of ref, generics, lambdas, Func<> and Action<> and all to solve one problem. If I ever need to direct someone to a real way of using these, I'm linking this post.
As for your benchmarking and everything, if you decide to revisit this you should take a look at the CIL to see if they really are so different after being compiled.
Are you looking to make money from your traffic with popunder ads?
In case you are, have you ever used exoClick?
Why do a field == null check in the [CMN] Setter? That way I can never assign a value to referencetype properties.
Post a Comment