摘要:Silverlight DataContext Changed Event
One known issue with Silverlight is that the DataContext
bound to a control may change, but there is no readily available change event. Unlike WPF, you don't have an explicit event to register with in order to track changes. This becomes a problem in controls like the DataGrid
control which reuses the same control instances for each page. Even though fresh data is bound, if your control isn't aware that the data context changed, it will keep stale content.
If you search online you'll find the solution is simple: you create a dependency property that is actually based on the data context (call it a "dummy" property) and then register for changes to that property. I was glad to find the solution but wanted something a little more reusable (remember, I like the DRY principle: don't repeat yourself, so when I find myself writing the same line of code more than once I have to go back and refactor).
The solution? I was able to find something that I think works well and involves an interface and a static class.
First, I want to identify when a control should be aware of changes to DataContext
and also provide a method to call when this happens. That was easy enough. I created IDataContextChangedHandler
and defined it like this:
1.
public
interface
IDataContextChangedHandler<T> where T: FrameworkElement
2.
{
3.
void
DataContextChanged(T sender, DependencyPropertyChangedEventArgs e);
4.
}
As you can see, it is a simple interface. A method is called with the sender (which will presumably be the control itself) and the arguments for a dependency property changed event. It is typed to T, of course.
Next, I used generics to create a base class that manages the "fake" dependency property:
01.
public
static
class
DataContextChangedHelper<T> where T: FrameworkElement, IDataContextChangedHandler<T>
02.
{
03.
private
const
string
INTERNAL_CONTEXT =
"InternalDataContext"
;
04.
05.
public
static
readonly
DependencyProperty InternalDataContextProperty =
06.
DependencyProperty.Register(INTERNAL_CONTEXT,
07.
typeof
(Object),
08.
typeof
(T),
09.
new
PropertyMetadata(_DataContextChanged));
10.
11.
private
static
void
_DataContextChanged(
object
sender, DependencyPropertyChangedEventArgs e)
12.
{
13.
T control = (T)sender;
14.
control.DataContextChanged(control, e);
15.
}
16.
17.
public
static
void
Bind(T control)
18.
{
19.
control.SetBinding(InternalDataContextProperty,
new
Binding());
20.
}
21.
}
As you can see, the class does a few things and works for any framework element, which is a "basic building block" that supports binding. It is typed to the FrameworkElement
but also requires that the target implements IDataContextChangedHandler
. It creates a dependency property. Because the data context can be any object, the type of the dependency is object
, but the type of the parent is the framework element itself ("T"). When something happens to the property, it will invoke _DataContextChanged
.
The event handler is sent the control that raised the event as well as the arguments for the old and new properties in the data context. We simply cast the sender back to its original type of T. Then, because we know it implements IDataContextChangedHandler
, we can simply call DataContextChanged
.
Finally, there is a static call to bind the control itself.
Now let's put the pieces together. Let's say you have a control that makes a gauge based on a data value, and you want to put the control in the grid. You need to know when the DataContext
changes, because you will update your gauge. The control will look like this:
01.
public
partial
class
Gauge : IDataContextChangedHandler<Gauge>
02.
{
03.
public
Gauge()
04.
{
05.
InitializeComponent();
06.
DataContextChangedHelper<Gauge>.Bind(
this
);
07.
}
08.
09.
public
void
DataContextChanged(Gauge sender, DependencyPropertyChangedEventArgs e)
10.
{
11.
if
(e.NewValue !=
null
)
12.
{
13.
int
gaugeLevel = (
int
)e.NewLevel;
14.
_UpdateImage(gaugeLevel);
15.
}
16.
}
17.
}
And there you have it - to register for the data context changing, we simply implemented IDataContextChangedHandler
and then registered by calling Bind
in our constructor.