Introduction
INotifyPropertyChanged
is a useful interface. It provides a mechanism whereby your classes can tell classes that depend on them (and particularly, WPF user interfaces) that they've changed and so something else needs to change as well. The implementation is pretty easy to read and accomplish, and unlike DependencyProperty
instances, you don't have thread affinity to worry about (at least in terms of exceptions thrown).
However, they come with two main problems:
- When calling
PropertyChanged
, you need to state the name of the property that changed. Without planning, this can easily result in numerous magic strings circulating about your code.
- Calculated properties go from being a convenient way to have a value that is always valid (and is semantically a property, even if it is never stored in a field) to an outright maintenance nightmare.
This article outlines a solution to these problems that tries to be - relative to other solutions - intuitive syntactically, easy to use (just reference the solution), and relatively performant (for instance, minimal reliance on reflection).
Previous Approaches
This solution is by no means the first to attempt to provide a convenient approach to the problem. For instance, the approach outlined by Emiel Jongerius in his blog post On Developing Pochet has provided me with a working solution for some time, and indeed highlights a number of important considerations that I might otherwise have overlooked. This has provided me with a great starting point, but comes with a number of important limitations:
- To use the core implementation, a class needs to inherit from
BindableObjectBase3
- which is fine until it isn't. C#, after all, does not allow multiple inheritance, so this is - from time to time - a big ask.
- It makes debugging somewhat painful, because every property needs to create - and destroy - a
PropertyTracker
instance whenever it is retrieved, and a lot of the code to prevent notification storms ends up being relatively unintuitive when debugging time.
- The syntax for usage - to me - is relatively unintuitive.
- It ostensibly has issues with thread safety.
These limitations by no means make the approach inappropriate; it works, and it works pretty well. However, they were sufficient to prompt me to attempt my own solution to the problem that avoids them (particular the first 3).
Terminology
One of the issues with describing an approach to this issue is that whilst it is easy to describe the relationship Cat.IsAlive
bears to Cat.Heart.IsBeating
(it is dependant upon it, though by no means exclusively), it is much harder to describe the converse. For the purposes of this article, I shall refer to this as a foundation relationship - i.e., the value of IsBeating
forms part of the foundation for the value of IsAlive
.
In addition, Cat
, as the bearer of IsAlive
, will be referred to as the dependant host (i.e., the entity that hosts the dependant property) of IsAlive
, whilst Heart
will be referred to as the foundation host. In all cases, this solution, assumes that there is a relationship between a specific dependant host and a specific foundation host, and that, for example, a foundation host could change at runtime (for instance, if Operations.HeartTransplant(PussInBoots, LionHeart)
were invoked).
The Approach
Whereas prior approaches have focused on maintaining a list of property changed handlers that need to be invoked when a foundation property changes, and then subscribing only to this handler, my approach side-steps this by maintaining the chain of event handlers itself. By which I mean:
- Prior to object construction, Dependencies are registered in a similar manner to the way
DependencyProperty
instances function. This is handled by the DependencyManager
class via one or more DependencyDelclaration
instances, and results in a ConcurrentBag<DependencyDeclaration>
corresponding to the type of the class.
- When the object is constructed, the list of registered dependencies is queried and used to construct event handlers that should be - at point of creation - attached to the
PropertyChanged
handler of the object.
- These handlers perform three functions:
- They confirm that the sender of the handler still bears the correct relationship to the dependant host. If the relationship fails, then they detach and return (and should get disposed of in due course)
- If they are an intermediate event handler (i.e., they were raised because e.g. of a heart transplant), they attach the subsequent event handler to the new value of the next entity in the chain. For example, after a heart transplant:
Cat.PropertyChanged
is raised, notifying that Heart
has changed.
- The old
Heart
now floats off into the ether (hence the requirement to validate or detach above)
- The new
Heart
doesn't know anything about the fact that it should notify Cat
if IsBeating
changes. We need to do something about this, so the event handler for the heart change identifies an appropriate event handler for responding to Heart.IsBeating
changes, and attaches it to the new heart
- Finally,
Cat.OnPropertyChanged
is called to indicate that the dependant property IsAlive
could now have changed.
This is quite a lot of work to do whenever a property changes, so to minimize the effort, all event handlers are built at class construction, and stored in a HandlerGenerator
class that allows them to interact with one another correctly. In particular, handlers can identify whether they are an intermediary handler or not, what the next handler is, and can attach and detach from the sender without forcing unwanted references to either Cat
or themselves to float around and longer than necessary.
Note also, that there is a minimal burden to implement anything in your models beyond registering dependencies. This is accomplished by placing the majority of the code into ISupportsDependencyManagerExtensions
as extension methods on ISupportsDependencyManager
. This way, a very simple implementation is sufficient.
Using the Code
Using the solution means adopting a couple of reasonably simple code patterns. These come in the form dependency registration, an additional line in your constructor, and a static constructor for derived classes. I'll cover each of these independently.
Dependency Registration
Registering a dependency involves using one of DependencyManager.
RegisterDependency
or DependencyManager.RegisterDependencies
. These return the DependencyDeclaration
s they are passed, either singularly or as an array. This is primarily to allow flexibility to register dependencies next to the properties they provide information about, typically as Private Static
members. Taking such an approach in some senses simulates using an Attribute
based solution (which is severely limited by attributes' inability to take lambda expressions as arguments).
Dependency registration also includes an optional flag for overrideBaseIdentifier
which specifies whether the dependencies should be added to or should replace the dependencies already present in any base class. This can be important when overriding virtual methods, as it is entirely possible that dependencies declared relating to a virtual method no longer apply in an override.
Let's look at a property definition before we examine how to declare a DependencyDeclaration
.
public class Cat : ISupportsDependencyManager
{
public bool IsAlive
{
get {
return this.Heart.IsBeating && this.IsBreathing;
}
}
private static DependencyDeclaration[] IsAliveDependencies =
DependencyManager.RegisterDependencies<Cat>(
new DependencyDeclaration<Cat, Cat>(cat=>cat.IsAlive, cat=>cat.IsBreathing),
new DependencyDeclaration<Cat, Heart>(cat=>cat.IsAlive, heart=>heart.IsBeating, cat=>cat.Heart)
);
public bool IsBreathing {
get { return _IsBreathing; }
set
{
_IsBreathing = value;
this.OnPropertyChanged(()=>this.IsBreathing);
}
}
private bool _IsBreathing;
}
There are several things to notice here:
IsBreathing
notifies via a refactor-friendly OnPropertyChanged
method. A simpler OnPropertyChanged(string PropertyName)
is a required member to implement ISupportsDependencyManager
, but the version that takes a lambda is provided as an extension method.
DependencyDeclaration
takes two type arguments. The first of these is used to identify the dependent host - i.e., the object that has the dependant property. The second identifies the foundation host type, i.e., the object to which the foundation property belongs. A dependant host must implement ISupportsDependencyManager
, whilst a foundation host must implement INotifyPropertyChanged
.
- It also takes three expression arguments. These are:
- An expression identifying, from an arbitrary
TDependantHost
instance, the dependant property.
- An expression identifying, from an arbitrary
TFoundationHost
instance, the foundation property.
- An expression identifying how to get from a
TDependantHost
instance to a TFoundationHost
one.
Of these, the third is most tricky. For it to function correctly, every property in the chain must also implement INotifyPropertyChanged
. This argument is not required if the dependency is internal, as in the case of IsBreathing
.
This method will be automatically called before the class is first accessed (at least, by any mechanism other than reflection), which will aggregate together all DependencyDeclaration
s for use in building class instances.
One final note regarding declaring dependencies. Depending on whether you make use of property getters to build expensive properties as they are needed (rather than on class construction), you may or may not wish to call .SetCascades()
on declarations after creating them. This flags that, rather than attaching just the most obvious event handler to Cat
, and then waiting until Heart
is changed (e.g. assigned from scratch) to assign a handler to it, the appropriate handler will be assigned for every member of the chain from the word go. This is important if Heart
exists already (e.g. private Heart _Heart = new Heart()
), because otherwise, unless a transplant takes place, it won't have the appropriate handler attached.
.SetCascades()
is a fluent method, returning the same DependencyDeclaration
with Cascades
set to true
.
The Constructor
The class constructor doesn't require a great deal of work, but does require one code pattern. This is, an initial call to this.AttachAllHandlers(this.PropertyChanged)
. Ideally, this should take place right at the start of the constructor, especially if you are not using .SetCascades()
, but as long as it happens before fields that are used in dependency declarations are assigned, it will still work.
Please note that AttachAllHandlers
called from a base class constructor directly will execute before the derived class constructor, so if you want to assign fields in your constructor for a derived class before calling AttachAllHandlers
, you may need to tweak the code somewhat.
The Static Constructor
A static
constructor is only required when inheriting from a class that already implements ISupportsDependencyManager
. It is required in order to ensure that dependencies registered against a base class are also imported - otherwise, to continue the cat
example, a dependency linking Cat.Heart.IsBeating
to Cat.IsAlive
would not automatically be applied to a SiameseCat
- after all, at the point when Cat
registers its dependencies, it has no way to know if it will ever be derived from, and even less way to know what those derivations will look like.
Consequently, a SiameseCat
requires a static
constructor that imports its base dependencies as follows:
class SiameseCat : Cat
{
static SiameseCat
{
DependencyManager.RegisterBaseDependencies<SiameseCat>();
}
}
Points of Interest
There are two particularly noteworthy elements in this code. The first is in how leveled event handlers are able to identify the "next" event handler to attach appropriately. Storing event handlers in an ordered list makes this possible, because thisLevel + 1
corresponds to nextLevel
. Obviously, when the event handlers are defined, the next level doesn't exist yet, but by the time they execute, it does.
Additionally, because all event handlers need to raise OnPropertyChanged
for the actual target object, there was a risk of a memory leak happening, with the event handlers maintaining a reference to target even after all other instances were eliminated. As such, WeakReference
is used in a couple of locations throughout the code to guarantee this isn't the case.
Finally, the use of Concurrent
classes should make this solution a little more thread safe than some of the prior approaches.
History
- 12/04/2015 : First draft article submitted - TJacobs
- 14/04/2015: Attached project correctly; apologies for the delay - TJacobs