(1 item) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(6 items) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(4 items) |
|
(2 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(2 items) |
|
(5 items) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(1 item) |
|
(3 items) |
|
(1 item) |
|
(1 item) |
|
(2 items) |
|
(8 items) |
|
(2 items) |
|
(7 items) |
|
(2 items) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(1 item) |
|
(2 items) |
|
(4 items) |
|
(1 item) |
|
(5 items) |
|
(1 item) |
|
(3 items) |
|
(2 items) |
|
(2 items) |
|
(8 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(6 items) |
|
(1 item) |
|
(2 items) |
|
(5 items) |
|
(5 items) |
|
(7 items) |
|
(3 items) |
|
(7 items) |
|
(16 items) |
|
(10 items) |
|
(27 items) |
|
(15 items) |
|
(15 items) |
|
(13 items) |
|
(16 items) |
|
(15 items) |
There's a common misconception that data binding has to have something to do with databases or at least data access classes
such as .NET's DataSet
. In fact this is not the case - you can bind any property of a
Windows Forms control to any property of any object. Consider this
ridiculously simple class:
public class Person { public string FirstName { get { return firstName; } set { firstName = value; } } private string firstName; public string LastName { get { return lastName; } set { lastName = value; } } private string lastName; }
Windows Forms is quite happy to bind to this. Here's an excerpt from a form:
private Person p; public Form1() { InitializeComponent(); p = new Person(); p.FirstName = "Ian"; p.LastName = "Griffiths"; txtFirstName.DataBindings.Add("Text", p, "FirstName"); txtLastName.DataBindings.Add("Text", p, "LastName"); }
When the form opens, the two text boxes show the values contained in the Person
object. Moreover,
if the user types new values into either of these text fields, the relevant property of the Person
object
will be updated automatically. You don't need anything beyond the code shown above to achieve this.
You can even bind to arrays of objects - the DataGrid
will happily present an array of such
objects as a table. Again, no special coding is required. This all works thanks to the magic of reflection - the data binding
infrastructure can discover what's in your object and adapt its behaviour accordingly. Sadly, the design-time support in
.NET version 1.1 for singular objects is not so great, but that's being addressed in VS.NET 2005 - the DataConnector
system lets you use any old object as a data source in the designer.
There is one snag. Although changes made by the user are propagated automatically into the object, the converse is
not true. With the Person
class as defined above, if some code changes one of the properties after the
initial binding, the UI will not reflect that change.
There's a perfectly straightforward way to deal with this. All you have to do is add so-called property change notification
events to your class. These are simple events that are raised when properties are changed, and which are named after the
property they apply to. For example, the change notification event for our Person
class's FirstName
property would be called FirstNameChanged
. These events all have to use the standard
System.EventHandler
delegate type.
Here's the relevant event definition for the FirstName
property, along with the necessary changes
to the set
handler to raise the event.
public string FirstName { get { return firstName; } set { if (firstName != value) { firstName = value; if (FirstNameChanged != null) FirstNameChanged(this, EventArgs.Empty); } } } private string firstName; public event EventHandler FirstNameChanged;
This works like a charm - if your code changes these properties, the UI will now be updated automatically. This works because the data binding infrastructure goes looking for these event handlers. If you bind a control to some data source, it uses reflection to find out whether the data source has a notification change event for the property being bound to.
It's straightforward, and has worked since .NET 1.0. The main problem is it's a bit tedious to do this for all your properties.
.NET 2.0 (aka Whidbey) simplifies things a little by introducing a new interface called IPropertyChange
, defined in the
System.ComponentModel
namespace. (The Avalon previews have also shown an IPropertyChange
interface defined in the MSAvalon.ComponentModel
namespace. Since these interfaces appear to be identical,
presumably Avalon will move over to using the one in System.ComponentModel
at some point.)
IPropertyChange
is a very simple interface. It has just one member, an event called
PropertyChanged
, of type PropertyChangedEventHandler
. The idea is that a data source can
raise this event whenever any property changes. This simplifies things because you only need the one event, rather than
one event per property, we now need just one event for the entire class.
public class Person : IPropertyChange { public string FirstName { get { return firstName; } set { if (firstName != value) { firstName = value; OnPropertyChanged("FirstName"); } } } private string firstName; public string LastName { get { return lastName; } set { if (lastName != value) { lastName = value; OnPropertyChanged("LastName"); } } } private string lastName; public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }
So now all we need is for each property set
handler to check whether the property value has really changed,
and then call the helper function to raise the event. Still mildly tedious, but better than the v1.x approach.
This is all very well if you're using Whidbey, but as I write this (October 2004) that's still in beta, and will be for some time yet. So what can you do today?
There's nothing stopping you from defining your own version of IPropertyChange
of course - it really is a very
simple interface. The main problem of course is that the data binding architecture in .NET 1.x won't use it. However,
it's quite possible to make it do so with the .NET TypeDescriptor
infrastructure.
The TypeDescriptor
system is very powerful, and yet a lot of people don't seem to know about it.
(Obviously not enough people have read my book. :-) ) It essentially provides a level of indirection over reflection, and is
used by both Windows Forms data binding, and the design-time architecture. (And also the PropertyGrid
, whether you're
using that at design time or runtime.) So earlier, when I said that Windows Forms data binding is using reflection, it's not using
it directly. It's using it via the TypeDescriptor
system.
So what does that mean in practice? In short, it enables us to pretend our class has features that are not in fact present.
For example, a class might have no properties at all, but could nonetheless tell the TypeDescriptor
infrastructure to report the presence
of properties. Visual Studio .NET uses this at design time to modify the set of properties that components and controls appear
to have. (For example, it makes sure that the Properties panel will always show a Name
property whether the component
in question has one or not.)
I'm going to use this mechanism to make it look like our data source raises property change notifications even though it doesn't
support the V1.x-style events. This will enable classes to use V2.0-style property change notifications on today's .NET framework.
(At the time of writing, today's .NET framework is V1.1. If VS.NET 2005 has shipped by the time you're reading this, then you don't need
to use this technique, because the framework already supports IPropertyChange
intrinsically.)
The technique I will use is to write an adapter class for objects that use V2.0-style change notifications. This adapter will
raise V1.x-compatible events. Or more accurately, it will appear to raise those kinds of events to anything
that uses the TypeDescriptor
infrastructure. In use it will look something like this:
p = new Person(); p.FirstName = "Ian"; p.LastName = "Griffiths"; object ds = PropertyChangeAdapter.Wrap(p); txtFirstName.DataBindings.Add("Text", ds, "FirstName"); txtLastName.DataBindings.Add("Text", ds, "LastName");
The Person
class is as above - providing a single PropertyChanged
event.
Notice that we don't bind the controls directly to the underlying data source - instead we bind to the wrapper returned
by PropertyChangeAdapter.Wrap
method. That wrapper performs the necessary TypeDescriptor
magic to pretend
that it has exactly the same set of properties as the object it wraps, and to generate change notifications when the source object
raises its PropertyChanged
event.
So it's pretty simply to use - just wrap the source object using one line of code and bind to the result. Data binding now works exactly as you'd expect - changes typed in by the user are pushed into the object as before, but now, and changes made to the underlying object will result in the UI being updated.
If you don't like this wrapping approach, my PropertyChangeAdapter
can
also be used as a base class - if your data source simply derives from PropertyChangeAdapter
and implements
IMyPropertyChange
, you don't even need the explicit wrapping call - you can just use the same binding setup code
we saw at the start. (I chose to support the explicit wrapping approach as well as inheritance because
.NET only permits single inheritance, and you may already have a reason to be deriving from something else.)
So how does it all work, you ask? Sadly, it ends up being a bit messy, mainly because hooking into the TypeDescriptor
infrastructure
is a bit of an all-or-nothing proposition. We end up with a lot of boilerplate. Conceptually it's pretty simple though. The
PropertyChangeAdapter
class implements ICustomTypeDescriptor
. The TypeDescriptor
class looks for
this interface - this is what enables us to fake up members. When the adapter is asked for property descriptors, it returns its own
special customized PropertyDescriptor
objects. These are nearly identical to the ones the TypeDescriptor
would normally return, except we have modified them to raise change notifications. And that's it.
So let's look at the code. First, we need a definition of IPropertyChange
. I've actually chosen to call the
interface IMyPropertyChange
, rather than IPropertyChange
, simply because using the same name as
a forthcoming V2.0 interface seems foolhardy - if I had used the same name, we'd get compiler errors when trying to compile under
V2.0, because it would complain that my definition clashes with the system definition. Using a different name avoids this, although
it obviously means changes are required when you do eventually want to migrate over to the V2.0 mechanism. (This
code will work fine on both v1.x and v2.0, it's just that it fails to take advantage of V2.0's support for IPropertyChange
.) Here
it is, along with the relevant delegate and event type definitions:
public delegate void MyPropertyChangedEventHandler(object sender, MyPropertyChangedEventArgs e); public class MyPropertyChangedEventArgs : EventArgs { public MyPropertyChangedEventArgs(string propertyName) { propName = propertyName; } private readonly string propName; public string PropertyName { get { return propName; } } } public interface IMyPropertyChange { event MyPropertyChangedEventHandler PropertyChanged; }
Now we need our adapter class. This is going to derive from ICustomTypeDescriptor
so that it
can choose what properties it seems to expose. It also needs to provide suitable constructors for use either as
a wrapper or as a base class:
public class PropertyChangeAdapter : ICustomTypeDescriptor { public static object Wrap(IMyPropertyChange o) { return new PropertyChangeAdapter(o); } private IMyPropertyChange targetObject; // Used when data sources derive from us. protected PropertyChangeAdapter() { IMyPropertyChange pc = (IMyPropertyChange) this; Init(pc); } // Used when we are in wrapper mode. private PropertyChangeAdapter(IMyPropertyChange target) { Init(target); } private void Init(IMyPropertyChange target) { targetObject = target; targetObject.PropertyChanged += new MyPropertyChangedEventHandler(targetObject_PropertyChanged); GeneratePropertyDescriptorWrappers(); } ...
The initialization code hooks the underlying data source's PropertyChanged
event. Remember, this is how the
underlying data source wants to raise its events. It is the job of the adapter to convert that into V1.x-style events. Before we see
the actual handler function though, we need to see how this class is going to fake it. The heart of this class is the fake
PropertyDescriptor
objects that it will return to the TypeDescriptor
. Here's where we create them:
... private PropertyDescriptor[] wrappedProps; private void GeneratePropertyDescriptorWrappers() { PropertyDescriptorCollection originalProps = TypeDescriptor.GetProperties(targetObject, true); wrappedProps = new PropertyDescriptor[originalProps.Count]; for (int i = 0; i < wrappedProps.Length; ++i) { wrappedProps[i] = PropDescriptorFactory.WrapProp(originalProps[i]); } } ...
Of course we want to expose the exact same set of properties as the underlying object, we merely want to raise change
notifications slightly differently. So this starts by asking the TypeDescriptor
for the real property descriptors.
(The significance of passing true
as the final parameter there is that we are asking the TypeDescriptor
to show us the real descriptors, rather than the faked up descriptors - this is important in the case where the data source has derived
from us - we want the TypeDescriptor
to ignore our ICustomTypeDescriptor
implementation for the
moment.) Then we simply call a helper function, PropDescriptorFactory.WrapProp
to create the custom type
descriptors. So where does that helper come from?
At this point, the code gets a little more complicated than would be ideal. All we really need is a nested class that derives
from PropertyDescriptor
providing the custom behaviour we require. Unfortunately, PropertyDescriptor
is
an abstract base class with a large number of abstract methods, and is consequently pretty tedious to derive from. Especially since
we want something that is very nearly standard behaviour, with one minor event handling tweak.
Fortunately the .NET framework provides a class called SimplePropertyDescriptor
that does most of the work for
us. The one snag is that it's a protected nested class of TypeConverter
for some reason. This obliges us to derive a
class from TypeConverter
in order to derive from SimplePropertyDescriptor
! It's messy, but less effort
than writing a PropertyDescriptor
from scratch. Here it is, along with a couple of internal helper functions:
private class PropDescriptorFactory : TypeConverter { // Create a custom property descriptor based on an original descriptor. internal static PropertyDescriptor WrapProp(PropertyDescriptor originalProperty) { return new CustomPropertyDescriptor(originalProperty); } // Cause a custom property descriptor to raise a change notification internal static void RaisePropChange(object target, PropertyDescriptor prop) { CustomPropertyDescriptor pd = (CustomPropertyDescriptor) prop; pd.RaiseChange(target); } protected class CustomPropertyDescriptor : TypeConverter.SimplePropertyDescriptor { public CustomPropertyDescriptor(PropertyDescriptor originalProperty) : base (originalProperty.ComponentType, originalProperty.Name, originalProperty.PropertyType) { this.originalProperty = originalProperty; } PropertyDescriptor originalProperty; public override object GetValue(object target) { PropertyChangeAdapter adapter = (PropertyChangeAdapter) target; return originalProperty.GetValue(adapter.targetObject); } public override void SetValue(object target, object value) { PropertyChangeAdapter adapter = (PropertyChangeAdapter) target; originalProperty.SetValue(adapter.targetObject, value); RaiseChange(target); } public void RaiseChange(object target) { OnValueChanged(target, EventArgs.Empty); } } }
The SimplePropertyDescriptor
is an abstract class, but unlike PropertyDescriptor
, we only need to
override GetValue
and SetValue
. For GetValue
, we just defer to the original descriptor
for the real property, and we defer to the underlying data source object. SetValue
is almost the same, except we make
sure to raise the change notification - this is done by the RaiseChange
method, which simply calls through to the
base class's OnValueChanged
. The reason for putting this inside a separate RaiseChange
method
rather than calling it directly is that there's another place we want to get to be able to raise these events from, and the base class's
OnValueChanged
method is protected. Adding the RaiseChange
method allows the helper
function PropDescriptorFactory.RaisePropChange
to raise the event too.
Of course we must make sure that this custom property descriptor is what the outside world sees. This is very simple - it just
involves a suitable implementation of the ICustomTypeDescriptor.GetProperties
method:
public PropertyDescriptorCollection GetProperties() { return new PropertyDescriptorCollection(wrappedProps); }
And now there's just one thing left to do. We need the code that handles those PropertyChanged
events
from the underlying data source:
private void targetObject_PropertyChanged(object sender, MyPropertyChangedEventArgs e) { PropertyDescriptor pd = GetProperties()[e.PropertyName]; PropDescriptorFactory.RaisePropChange(this, pd); }
This simply locates the appropriate custom property descriptor for the property that just changed, and then uses the
PropDescriptorFactory.RaisePropChange
helper function we saw earlier to raise the change notification
event. This ensures that anything that was using the TypeDescriptor
infrastructure to monitor the data
source (e.g. the Windows Forms data binding infrastructure) will now be aware of the change.
Actually there s one other thing left to do. There are 11 other methods on ICustomTypeDescriptor
. It
seems that in this particular context you'll get away with just returning null
from all of them. But strictly
speaking, it would be better to defer to TypeDescriptor
for all of them. E.g., this kind of thing:
public TypeConverter GetConverter() { return TypeDescriptor.GetConverter(targetObject, true); } public EventDescriptorCollection GetEvents(Attribute[] attributes) { return TypeDescriptor.GetEvents(targetObject, attributes, true); } ...etc.
They're all just single-line implementations like that, and as such, not very fun to read, so I'll leave you to imagine the rest.
[Update: In Beta 2 of Whidbey, the IPropertyChange
was renamed to INotifyPropertyChanged
.]