Click here to Skip to main content
15,867,453 members
Articles / Programming Languages / C#

PropertyPathObserver - A New Way of Creating Bindings - Portable

Rate me:
Please Sign up or sign in to vote.
4.93/5 (29 votes)
8 Nov 2016CPOL14 min read 17.6K   223   29   9
This article presents the PropertyPathObserver class, which allows for faster bindings and more.

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:

Image 1

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

C#
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:

C#
PropertyPathObserver.Observe<string>
(
  localVariableOrInstanceField, "FirstProperty.SecondProperty.Text",
  (value) => textBlock.Text = value
);
C#
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:

C#
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:

C#
public UnsubscribeObserverAction _HandlerForTextBox_Text
  (object instance, PropertyInfo property, Action action)
{
  if (instance is INotifyPropertyChanged)
    return null;

  // We didn't do anything, but this result will "lie" that we did.
  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:

C#
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.

  1. Specify the generic argument on the Observe call as the nullable counter-part of the property type. That is, do the call like this:

    C#
    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:

    C#
    () => (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.

  2. 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:

Image 2

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Microsoft
United States United States
I started to program computers when I was 11 years old, as a hobbyist, programming in AMOS Basic and Blitz Basic for Amiga.
At 12 I had my first try with assembler, but it was too difficult at the time. Then, in the same year, I learned C and, after learning C, I was finally able to learn assembler (for Motorola 680x0).
Not sure, but probably between 12 and 13, I started to learn C++. I always programmed "in an object oriented way", but using function pointers instead of virtual methods.

At 15 I started to learn Pascal at school and to use Delphi. At 16 I started my first internship (using Delphi). At 18 I started to work professionally using C++ and since then I've developed my programming skills as a professional developer in C++ and C#, generally creating libraries that help other developers do their work easier, faster and with less errors.

Want more info or simply want to contact me?
Take a look at: http://paulozemek.azurewebsites.net/
Or e-mail me at: paulozemek@outlook.com

Codeproject MVP 2012, 2015 & 2016
Microsoft MVP 2013-2014 (in October 2014 I started working at Microsoft, so I can't be a Microsoft MVP anymore).

Comments and Discussions

 
GeneralMy vote of 5 Pin
Jon McKee10-Dec-16 18:05
professionalJon McKee10-Dec-16 18:05 
GeneralRe: My vote of 5 Pin
Paulo Zemek12-Dec-16 6:13
mvaPaulo Zemek12-Dec-16 6:13 
QuestionI did something like this a while back Pin
Sacha Barber6-Nov-16 2:56
Sacha Barber6-Nov-16 2:56 
AnswerRe: I did something like this a while back Pin
Paulo Zemek6-Nov-16 5:39
mvaPaulo Zemek6-Nov-16 5:39 
GeneralRe: I did something like this a while back Pin
Sacha Barber6-Nov-16 10:15
Sacha Barber6-Nov-16 10:15 
QuestionExcellent!!!! Pin
CandyJoin5-Nov-16 8:07
CandyJoin5-Nov-16 8:07 
AnswerRe: Excellent!!!! Pin
Paulo Zemek5-Nov-16 9:03
mvaPaulo Zemek5-Nov-16 9:03 
GeneralMy vote of 5 Pin
Fred POINDRON3-Nov-16 23:38
Fred POINDRON3-Nov-16 23:38 
GeneralRe: My vote of 5 Pin
Paulo Zemek3-Nov-16 23:41
mvaPaulo Zemek3-Nov-16 23:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.