(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) |
Chris Sells put an interesting XAML conundrum to me yesterday. It was one of those problems to which the intuitive reaction is "That must be dead simple" and which then takes about a day to figure out... The problem was essentially this:
How do I modify the appearance of all the buttons on a XAML page at once, programmatically?
This seems like it should be straightforward because, on the face of it, XAML’s styling mechanism does precisely this kind of thing. Consider this example:
<FlowPanel xmlns="http://schemas.microsoft.com/2003/xaml" xmlns:def="Definition"> <FlowPanel.Resources> <Style> <Button Background="Red" ID="Button1"/> <Style.VisualTriggers> <PropertyTrigger Property="IsMouseOver" Value="true"> <Set PropertyPath="Background" Value="limegreen"/> </PropertyTrigger> </Style.VisualTriggers> </Style> </FlowPanel.Resources> <Button ID="Button2">Hello</Button> <Button ID="Button3">there</Button> <Button ID="Button4">world!</Button> </FlowPanel>
The first child of that <Style>
element tells Avalon that
buttons should have a red background by default. The second child, <Style.VisualTriggers>
,
indicates that the background should be set to lime green if the button’s IsMouseOver
property becomes true, i.e., it causes the button to go green when you move the
mouse over it.
So we appear to have everything we need – not only do styles provide a mechanism for applying properties consistently across the entire page, that mechanism appears to support dynamic property changes.
Unfortunately, the extent of the support for dynamic behaviour appears to be
surprisingly limited: as far as I can tell, styles and their associated
triggers are essentially fixed at page load time. If you attempt to modify a
style at runtime, you will get an InvalidOperationException
, which
turns out to be because the Style
object has set an internal flag
indicating that it is ‘sealed’. (This is not the same thing as the C# sealed
keyword, by the way. It is just an internal flag that the Style
maintains, and once it is set, it refuses to allow further updates.)
It seems like the Style
and its associated triggers are read by
Avalon when it builds its internal data structures to represent the UI, and
then not used again. Styles do not remain ‘wired up’ which is presumably why
they refuse to let you modify them. (It would be more irritating if they didn’t
throw an exception, and instead just silently failed.) This is evidently not
the mechanism we are looking for. Move along.
Fortunately, XAML is designed to support dynamic user interfaces which can directly reflect changes made to objects. It’s just that styles aren’t the mechanism we're supposed to use in order to do that. Instead, data binding seems to be the way to go here. We can use this syntax:
<ButtonBackground="*Bind(Path=Background;BindType=OneWay)">Bound</Button>
This indicates that the button’s Background
should be bound to the
Background
property of the data source. But what is the data source
here? We specify that by adding the following child to the FlowPanel
:
<FlowPanel.DataContext> <Bind> <Bind.DataSource> <ObjectDataSource TypeName="MyStyleSource,MyAssembly"/> </Bind.DataSource> </Bind> </FlowPanel.DataContext>
This indicates that an instance of a class called MyStyleSource
,
defined in a component called MyAssembly
, should be created and
used as a data source. All we have to do is write that class and give it a
property called Background
of the appropriate type. (MSAvalon.Windows.Media.Brush
,
in this case.) Recall for a moment the original goal here: to be able to modify
the appearance of the buttons dynamically. For this to work, Avalon will need
some way of detecting when our custom data source object changes. It will
expect us to implement the IPropertyChange
interface in order to
indicate when such change occur. (This performs the same role as property
change events in Windows Forms binding, except a single event is used to notify
about any property changes, rather than having one event per property.) The
data source will look like this:
using MSAvalon.ComponentModel; using System.ComponentModel; public class MyStyleSource : IPropertyChange { private Brush bg = Brushes.Blue; public Brush Background { get { return bg; } set { if (!bg.Equals(value)) { bg = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Background")); } } } } public event PropertyChangedEventHandler PropertyChanged; }
We are now where we want to be. We have told Avalon to create an instance of our
custom data source, and to bind its Background
property to our
button’s Background
. All we have to do now is retrieve the data
source and modify its Background
property in some code on our
panel’s codebehind:
StyleSource ss = (MyStyleSource) this.DataContext; ss.Background = Brushes.Yellow;
This reads the panel’s DataContext
property. We set this earlier
(in the XAML) to be our custom data source. It then updates the Background
property, which will raise the PropertyChanged
event. This event
will be handled by Avalon, which will update any components bound to the
property, thus causing our button’s background colour to change.
However, I’m not really happy with this as it stands. It forces me to declare all my buttons like this:
<Button Background="*Bind(Path=Background;BindType=OneWay)">Click me</Button>
If I forget to put that property binding in there, the button won’t change when it should. Worse, if I want to change anything (e.g. bind to a different source object, or bind extra properties), I need to change every single button in my XAML! You’d think that this would be a job for styles. The obvious solution would be to put this at the top of the file:
<FlowPanel.Resources> <Style> <ButtonBackground="*Bind(Path=Background;BindType=OneWay)"/> </Style> </FlowPanel.Resources>
Unfortunately that turns out not to work, and nor does the more verbose complex
property syntax for specifying a bind here. The reason seems to be that
bindings can only be applied to DependencyObjects
, and although Button
is a dependency object, we’re not really creating a Button
here,
despite how the syntax looks. It seems that the contents of a Style
element are parsed using a completely different mechanism from the one used for
the rest of the XAML file. As far as I can tell, this syntax populates a Style
with a list of things it will do to any Button
it is applied to,
and that list doesn’t seem to have a provision for storing the fact that it
should apply a binding. (Which you may or may not regard as a problem. I have
to admit that it never occurred to me to ask if this was a useful thing to do.
I like a challenge...)
This is as far as I have got. I can’t see a way of automatically assigning a binding to a particular property on every instance of a given type. And while this isn’t a show stopper, it’s a bit disappointing. But I may well have missed something – if I have, please let me know. (The fact that I haven’t written a comment facility for my blog engine yet makes this harder, admittedly, but feel free to email me.)