Imagine that we retrieve a list of POCO (Plain Old CLR Objects) and we find our self in need to track changes made to those objects.
To illustrate this we use a very simple example like the Customer class:
public class Customer
{
public string CustomerID { get; set; }
}
Standard CLR properties has no built-in support for interception so the preferred way of doing this is to implement the INotifyPropertyChanged interface.
This interface allows us to notify subscribers that the property has changed and its widely used when two-way data binding is required.
If we bind, say a textbox to a property that does not support change notification, we end up with a situation where changes made to the textbox is automatically reflected in the property, but not the other way around.
The data binding mechanism (WinForms and WPF), relies heavily on this interface to do its magic.
The example below shows our modified Customer object with support for change notification.
public class Customer : INotifyPropertyChanged
{
private string _customerId;
public string CustomerID
{
get { return _customerId; }
set
{
_customerId = value;
OnPropertyChanged("CustomerID");
}
}
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Although the interface is quite easy to implement it also results in a lot of tidious code for us to write. Every property setter need that call to OnPropertyChanged passing along the property name.
Sure we could create a base class that implemented this interface and by that simplify it to some extent, but that takes our classes away from the POCO principle.
AOP to the rescue
I'd say that we let our objects remain POCO objects and implement the INotifyPropertyChanged interface at runtime instead.
You might wonder how that can be done and there are actually two ways of achieving that.
We can find a lot of documentation on Aspect Oriented Programming that is more or less difficult to understand, but let us try to sum it up in one sentence.
AOP is about adding new aspects to an object without the object ever knowing about it.
The Aspect in our case is implementing the INotifyPropertyChanged interface so that changes made to the Customer object are intercepted by the caller.
And we want to do this at runtime.
There are actually two ways of doing that and we can either use a proxy to the actual customer object or we can use IL injection to modify the customer object so that it implements what we need, in this case the INotifyPropertyChanged interface.
Lets start of simple with the proxy approach.
The Proxy
A proxy is an object that sits between the caller and the actual target object.
The proxy class will at least implement the target interface, but can also implement additional interfaces which is the key to our solution.
Creating a proxy object can be done in two different ways:
- Create a proxy by sub classing the target object.
This requires all members to be declared as virtual members so that they can be overridden in the proxy type. - Create a proxy that implements the same interface that the actual target object does.
This requires the members to be declared in an interface implemented by the target object.
We are going to stick to the second approach here as we will see later, provides us with a little more flexibility.
So we extract the interface from our Customer object.
public interface ICustomer
{
string CustomerID { get; set; }
}
public class Customer : ICustomer
{
public string CustomerID { get; set; }
}
Now the customer implements a contract that also can be implemented by our customer proxy type.
As mentioned before the proxy class can implement additional interfaces and that means that the proxy type can implement the INotifyPropertyChanged interface.
The figure below shows that the textbox now binds to the CustomerProxy and since it implements INotifyPropertyChanged, a two-way communication now exists between the
CustomerProxy and the textbox (client).
So any changes to the CustomerID exposed by the CustomerProxy will now be reflected in the textbox.
And this actually brings us to the downside of the proxy approach.
For all this to work, changes to the CustomerID (that we want to be reflected in the textbox), must be made through the CustomerProxy.
If we have some logic inside the Customer object that internally updates the CustomerID property, the changes are not reflected in the textbox.
Why? Simply because we are not invoking the Customer proxy that is resposible for the change notification.
In the case that the proxied objects contains logic that may present a problem if the code actually updates the properties.
IL Injection.
This approach takes you far beyond the topics you might encounter in your average "Teach yourself C# in 21 days" book.
What we are talking about here is modifying the Customer class in such a way that it implements the INotifyPropertyChanged interface.
The modification is done by injecting the IL code that normally would be the result of implementing the interface in the first place.
The injection can be done at runtime or after the assembly has been compiled.
We are going concentrate on runtime modification here and that involves the following steps.
1. Load the assembly from disc (not into the application domain)
2. Modify the assembly (IL injection)
3. Save the assembly back to disc or load it as an byte array into the application domain without touching the original assembly.
There is only one library that I know of that actually allows us to do this, and that is Mono.Cecil.
This is a very powerful library that pretty much lets us do whatever we want with our assembly.
An interesting problem with this approach is how to get access to the target assembly before it is loaded into the application domain.
As we already know, once an assembly is loaded into the application domain, there is not much we can do about that. It can not be unloaded nor modified.
So we need to catch it BEFORE Fusion resolves the assembly and loads it into the application domain.
Again this presents us with two possible solutions.
- Make sure that Fusion is unable to resolve the assembly and handle the AppDomain.AssemblyResolve event.
This can be done by placing the assembly in a subfolder - Make sure that the assembly is not referenced directly
Load the assembly our self and make the necessary changes to it before it gets loaded into the application domain.
IMHO, depending on Fusion being unable to locate the assembly seems somewhat “hacky”.
If the assembly is not referenced by the application itself, it can be loaded dynamically by an Inversion of Control container and we can hook into the load process and do our magic before the container starts to register services from that assembly.
There is also another that presents a challenge when doing IL injection and that is how to enable debugging in our modified/weaved assemblies.
If we modify the IL, the symbol information (pdb) also needs to be updated. If we don’t handle that properly, we will not be able to set a breakpoint inside a class that at runtime has been modified.
Mono.Cecil provides some support here and from what I have tested so far, we should be able to actually debug weaved assemblies.
Conclusion
As we can see there are many decisions that needs to be made as to how we are going to inject new behavior into our objects and I would say it depends a lot of what kind of behavior we want to add.
Initially I wanted to create some kind of state/change tracker that hooks up the PropertyChanged event and keeps track of changes made to an object graph.
My gut feeling here is to go with the Proxy implementation as it allows us to keep everything i C# (No IL injection) and it allows proxies to be installed in a lazy fashion as we navigate down the object graph.
It is also somewhat simpler to add support for new interfaces, for example the IDataErrorInfo interface if we should want to give some visual feedback when validation errors occur.
No comments:
Post a Comment