Introduction
In the past I presented the MathBinding markup extension. I created it because I considered WPF's Binding
excessively complicated when I needed some simple math on top of a property.
Then I tried to create the same MathBinding
for Universal apps, but I couldn't. I don't know if I didn't find the right documentation or if it is simply impossible to create a markup extension when developing Universal Apps. So, I started playing with different ways of doing Bindings. In particular, doing Bindings in C# instead of doing them in XAML. I will be honest, I really think that most Bindings should live in the code behind. Knowing which properties to access, specially when there's any kind of calculation, data-type conversion etc, should be in code.
Well, the result ended up much better than I originally expected. The PropertyPathObserver
that I created isn't limited to bindings or to WPF. You can use it in a Console application to observe property changes (in a deep path) and execute any code you want, like a Console.WriteLine()
or updating other properties (in this case it becomes a binding). Better than that, in my tests it is between 4 and 5 times faster than WPF's Binding and, if configured correctly, it can be used with data-sources that notify property changes in alternative manners (like having one event per property).
Take a look at this sample result:
It shows the performance comparison doing 100 thousand updates between WPF's Binding
and the PropertyPathObserver
, using INotifyPropertyChanged
and using an object that has an event per property. As you can see, WPF's Binding
took almost 4 seconds to do it's job. The PropertyPathObserver
took almost 0.7 seconds to deal with the exact same type of object, and 0.34 seconds with an optimized object, which WPF's Binding
simply can't deal with.
Using the Code
PropertyPathObserver.Observe
(
() => localVariableOrInstanceField.FirstProperty.SecondProperty.Text,
(value) => textBlock.Text = value
);
The PropertyPathObserver.Observe
method does all the magic. It receives the following parameters:
- propertyPathExpression: This is an expression that describes the properties we want to access. It is important to note that it must follow a very rigid rule. It must start over a local variable or instance field, and then must access at least one property. Of course, you are allowed to keep going depth accessing another property, then another property and so on;
- valueChanged: This is an
Action<TValue>
. The TValue
is the same type of the last property we accessed in the previous parameter. Here we may use the same lambda notation that we use for the expression, but this is actual code. Nothing is parsed here. It is the code that it will execute directly. You can do a Console.WriteLine(value);
here if you want, you can call a Trim()
or even do any kind of pre-parsing before setting the value to another property;
- defaultValue: This is an optional parameter. It is actually more useful for value-types. It simply tells what value to use when there is a
null
anywhere on the property path.
As you can see, it doesn't look very hard.
Maybe it looks too verbose for a direct binding, but as soon as you need to do any kind of data manipulation, you will see that it becomes much nicer. No need to deal with converter classes, static resources etc.
Also, we must remember that it is faster. If we are dealing with objects that change frequently, it is already an advantage to use this.
Overloads
The Observe
method is overloaded. I consider the overload I just presented as the most refactoring friendly, as expressions gets changed by refactoring tools while most strings don't. Yet, the other two overloads are based on strings. A single string separated by dots, or an array of strings, each containing a single property name.
We could achieve the same effect as the previous code block by any one of these:
PropertyPathObserver.Observe<string>
(
localVariableOrInstanceField, "FirstProperty.SecondProperty.Text",
(value) => textBlock.Text = value
);
PropertyPathObserver.Observe<string>
(
localVariableOrInstanceField,
new string[] {"FirstProperty", "SecondProperty", "Text"},
(value) => textBlock.Text = value
);
The most annoying part, to me, is the need to put the property type as a generic parameter (that <string>
).
Actually, you can use a base class as the generic parameter (in this case, only object
would be valid) but you can't give a different type. No conversion will be done. It will simply throw an exception at run-time if a wrong type is given.
Advantages
Considering the time spent actually creating the binding, the last option is the fastest one, but I really don't think you will be able to notice the difference.
As I said, I consider the version that uses lambda expressions to be the more refactoring friendly (and if any property name is wrong, you get the error during compile time). Yet, the versions receiving the path as a string are somewhat more dynamic.
For example, if the localVariableOrInstanceField
(I mean, the source object) is cast as object
, you will not be able to use the lambda expression. But it works great with the overloads that receive a string
path. The real run-time object type will be used when doing the binding.
But that's all the "dynamism" supported. Since the first object type is discovered, the entire path will be based on the static types presented by the properties. That is, if any property returns an untyped object
, it doesn't matter if the actual property value is of an observable type and has many properties, the binding will not be able to see any property in it.
Maybe I will create an even more dynamic version in the future, but if I do it, it will surely be another overload, as making the discovery really dynamic will affect the performance.
Stopping observation
It may look unusual, but we don't have a StopObserving
or similar method. The reason is that it would be hard to match the parameters, as two identical lambda expressions may end-up generating different delegates and so a remove/unsubscribe wouldn't work.
So, to make things easy, the Observe
method returns a delegate. You are free to ignore it if you don't plan to stop observing the object (it will naturally stop the observation if the object gets collected) but, if you want to stop observing the changes, it is enough to invoke such delegate.
I thought about making it return an Action
, but I decided to create a new delegate type, named UnsubscribeObserverAction
, only to make things clear.
Bidirectional binding?
As this is not really a binding, there's no bidirectional binding by default. Yet, you can observe object a.Text
and update object b.Text
, and also observe b.Text
and update a.Text
. Considering the objects don't keep changing the value they are receiving, the PropertyPathObserver
will not generate a notification when it sees that the value is the same (and actually the good implementation of the property themselves wouldn't do that).
Yet, by default the PropertyPathObserver
can't observe changes to DependencyProperties
. It simply looks for the INotifyPropertyChanged
. In the sample application I did provide a solution for the TextBox.Text
, but that's all for now. I really wanted that WPF's dependency properties had a very easy way to get change notifications (as the Universal Apps have). But that's not available and the work-around to make it work isn't my focus right now (another thing on my personal TODO list).
Exceptions
The PropertyPathObserver
doesn't deal with exceptions at all. So, if there's an exception, the application will probably crash.
You must ensure that you only use properties that don't throw and that, when executing the action on a property change, you either catch the exceptions or also guarantee that no exception will be thrown.
As an extra detail, if an object in the path can't be observed, it will also throw an exception. If in the same situation, the WPF binding simply reads the value once and never gets change notifications. If you want that behavior, you will need to register a SubscribingHandler that does nothing, but "lies" that it did a subscription (or you can simply skip observing the property and read its value directly).
PropertyObserver.RegisterSubscribingHandler
So, this is the method that you should use to register your own handler to deal with alternative property notifications.
In fact, the PropertyObserver
is the class used by the PropertyPathObserver
to get notification of each individual property in the path. For INotifyPropertyChanged
instances, it manages that a single event handler is registered to the PropertyChanged
event, independently if you want to observe only one property or if you want to observe many properties (or even create many observers to the same property).
The SubscribingHandlers
will be called every time the PropertyObserver.Observe
method is invoked. They should verify if they can do a subscription by analizing the instance and the property. If they can't, they must not throw exceptions, they must simply return null
. If a handler can do a subscription, then it should do it and return an UnsubscribeObserverAction
.
For example, the SubscribingHandler
to support the Text
property, is this one:
public UnsubscribeObserverAction _HandlerForTextBox_Text
(object instance, PropertyInfo property, Action action)
{
var textBox = instance as TextBoxBase;
if (textBox == null)
return null;
if (property.Name != "Text")
return null;
TextChangedEventHandler handler = (sender, args) => action();
textBox.TextChanged += handler;
return () => textBox.TextChanged -= handler;
}
As you can see, I immediatelly register to the TextChanged
event by doing the textBox.TextChanged += handler;
, and I return an action that will remove the handler when invoked. Remember that the () =>
means I am creating a lambda expression, and not executing the code immediatelly.
Simulating WPF's behavior for non-observable objects
If you want to simulate the WPF's behavior of only getting the property value once and never again instead of throwing exceptions for non-observable objects, you can use the following code:
public UnsubscribeObserverAction _HandlerForTextBox_Text
(object instance, PropertyInfo property, Action action)
{
if (instance is INotifyPropertyChanged)
return null;
return () => {};
}
Thread-safety
Observing different objects in parallel is thread-safe. But when registering many observers to the same object, well, it is not thread-safe. I don't really expect that to be done by many threads but, if you do that, you must ensure thread-safety.
Also registering a SubscribingHandler is not thread-safe. This is really expected to only happen when initializing the application, so it also shouldn't be a problem. (I know, I am going against some of my own principles with this code... maybe I will change it in the future).
From Value-Type to Nullable
Imagine that I am accessing this full path: sourceObject.Other.Value
.
That Value
property is a value-type (for example, double
).
By default, if you do this:
PropertyPathObserver.Observe
(
() => sourceObject.Other.Value,
(value) => Console.WriteLine(value)
);
The Observe
method will be accessing a non-nullable double
. If the result of source.Other
is null
, the valueChanged
action will be called with the default value, which as it wasn't provided, is zero. It will not be null
.
Yet, it is natural to think that when any item in the path is null
, the result should be null
. So, we need a nullable double in this case (type double?
or Nullable<double>
).
So, is it possible?
Well... when using the overloads that receive the path as a string
, we always specify the type of the result. Fortunately, even if the property is of type double
, you can say that you want the result to be of type double?
and it will work. This is not considered a type-conversion as double?
can naturally assign objects of type double
.
For the overload that receives the Expression
, that wasn't possible on the first version of the code/article. So, you may want to download the code again. Now there are two ways to make the conversion from non-nullable to nullable.
-
Specify the generic argument on the Observe
call as the nullable counter-part of the property type. That is, do the call like this:
PropertyPathObserver.Observe<double?>
(
() => sourceObject.Other.Value,
(value) => Console.WriteLine(value)
);
Even if we don't do it manually, this actually changes the expression to this:
() => (double?)sourceObject.Other.Value
And that's the reason it originally didn't work. The parser didn't accept any kind of cast/conversion. Now it accepts this kind of cast or to object
, but don't try any other type of cast as it will not work. The cast is not really executed. The PropertyPathObserver
actually doesn't run the cast, it only analyzes the expression.
- Call the
ObserveAsNullable
method instead of calling Observe
. This is a new method that I created only with the purpose of making the support from non-nullables to nullables easier. The advantage of this method over the previous one is that you don't need to specify the destination type, it is inferred by the compiler. So, you don't have the chance of writing the wrong type and, if the property changes from one type to another, your code will not be trying to force an invalid conversion.
How it works
So, how does the PropertyPathObserver
works?
I will try to explain it briefly but I will not dive into its actual implementation. The reason is simple: The code uses a lot of compiled expressions to get a good performance. Yet, expressions are quite confusing and hard to understand even for simple tasks.
So, let's imagine we have a baseObject
and we want to observe the properties "A.B.C.Text".
The observer will create a helper object, which includes the delegate to be invoked to notify the changes, the last value read and an array with information for each step of the path. Each item of the array includes things like what's the source object, a delegate to read the appropriate property really fast and a delegate to unsubscribe from change notifications.
So, for A.B.C.Text
we will have an array with four items.
The source for the first element in the array will be the baseObject
. We will immediately register on the baseObject
for changes of its A
property and we will store the unsubscribe delegate on the first item in the array.
Then, we will do a first read. If when reading A
we have a non-null result, we will store such result in the second item in the array, and we will also register for notifications of changes on its B
property (and store the unsubscribe delegate on the second item in the array). We actually keep going to B
and C
and Text
.
After we read the value from Text
, well, we will have the value to present for the first observation and to store on the helper object. If any value is null
in the middle of the path, it only means we need to invoke the notification delegate with null
.
Notice that the objects in the array are source object, the result from A, from B and from C. The Text
property isn't stored in the array and is part of the helper object.
When the first read is done, we will have the array correctly filled. If, for example, we change the value of baseObject.A.B
, the notification will be sent with the right index (1). So, we simply don't waste any time reading the base object to get A
. We will read the property B
on top of the already stored result of A
directly. If the value ends up being the same (some objects may notify a change that didn't really happen), we simply return. If there is a real change, then we execute the unsubscribe delegate of B (which will unsubscribe for the previous value of B), and if the new value is not null, we will subscribe for its change notifications. If this happens, we will also read the value of C. If not, we keep null and go to the next step anyways. That means that we will end-up unregistering from C notifications too if it changed or became null
.
If we had the strange situation where B changed, but the new value of B has the same value for C, then we will only unregister previous B / register a new B. There will be no action for C and so the method will return without generating a change notification.
Well... I think that's all. It is not really simple, it is not really hard, but the code actually makes it look much harder than it is.
Curiosity
When doing the performance tests with WPF, I discovered something very interesting:
The red rectangle is for when I ran the WPF binding test. The yellow is for the PropertyPathObserver, with INotifyPropertyChanged and the green with the optimized object.
What surprised me is the amount of garbage collections done when dealing with WPF bindings. I know I was changing property values a lot, what is not a common scenario, yet I never expected it to cause so many garbage collections.
I ended up running the test many times, and the PropertyPathObserver never causes that kind of issue, while WPF keeps doing it.
So, I believe the difference in performance is because of the excessive garbage generated by WPF. I simply don't know why it does it.
Version History
- Second update (version 3): November 8th, 2016. Inside the string overload of Observe, replaced a DeclaringType by a PropertyType. In the sample everything worked because both types were the same. Also, optimized the cache of delegates to only care about the declaring type, avoiding repetitive delegate caches in memory, saving both speed and memory, when the reflected type is different from the declaring type (it doesn't matter to the delegate);
- First update: November 5th, 2016. Added the "From Value-Type to Nullable" topic;
- Initial version: November 3rd, 2016.