The summary
- MarkupExtensions are super simple and somewhat cool to create.
- Xaml has limited support for generics.
- MarkupExtensions do not allow you to specify arrays in the {MyMarkupExtension NamedParameter=Value, NamedParameter2=Value} syntax
- MarkupExtension doesn't derive from DependencyObject, therefore it is difficult create your own custom properties that work with databinding
- Custom MarkupExtensions work better in Visual Studio when they are defined in a different assembly than you are using them.
The whole story
I thought I'd do a markup extension that allowed you to do some simple math operations, avoiding using the Transform classes to achieve the same result.
At first it seemed really straight forward; I followed the great guide here to get started.
My idea was to let you choose an operation and a list of array elements. For example, if you wanted to bind the width of a border to half of myCanvas, you would specify like so:
<Border Width="{local:DoubleMath Operation=Multiply,
Operands={Binding ElementName=myCanvas,
Path=ActualWidth},.5}" Background="Red"/>
Unfortunately, there are several problems with this. The named parameters (Operands and Operation) correspond to properties on your MarkupExtension class. If the named parameter (property) corresponds to a list, then you need to use the x:Array syntax (as it would theoretically confuse the NamedParameter=Value parsing).
This is completely achievable, but it means you'd have to write it out in longhand, thus removing one of the advantages of using a MarkupExtension in the first place.
<Border Background="Red">
<Border.Width>
<local:DoubleMath Operation="Multiply">
<local:DoubleMath.Operands>
<Binding ElementName="myCanvas"
Path="ActualWidth"/>
<sys:Double>.5</sys:Double>
</local:DoubleMath.Operands>
</local:DoubleMath>
</Border.Width>
</Border>
You really don't need a list of parameters for a math class, as you should theoretically be able to nest them. So I created two properties, FirstArg and SecondArg. My class at this point looks like this:
public class DoubleMath : MarkupExtension {
public Operator Operator { // multiply, add, subtract, divide
get { ... }
set { ... }
}
public double FirstArg {
get { ... }
set { ... }
}
public double SecondArg {
get { ... }
set { ... }
}
public override object ProvideValue(IServiceProvider serviceProvider) { ... }
}
<Border Width="{local:DoubleMath Operation=Multiply,
FirstArg={Binding ElementName=myCanvas,
Path=ActualWidth},SecondArg=.5}}" Background="Red"/>
... but it's not very interesting as the runtime chokes when trying to assign a binding to FirstArg, as Binding is not a double. I change the return value to object and give it another go.
public object FirstArg {
get { ... }
set { ... }
}
The Xaml parser is smart enough to know what's going on; it attempts to hook up the binding for you - and errors out that you can't assign a Binding to a property that isn't registered as a dependency property.
So I register a dependency property, using the handy "Insert Snippet" WinFx 3.0 context menu item in Visual Studio 2008.
public double FirstArgument {
get { return (double)GetValue(FirstArgumentProperty); }
set { SetValue(FirstArgumentProperty, value); }
}
public static readonly DependencyProperty FirstArgumentProperty =
DependencyProperty.Register("FirstArgument",
typeof(double), typeof(DoubleMath), new UIPropertyMetadata
(double.NaN, new PropertyChangedCallback(ArgumentChanged)));
Unfortunately, when I built and ran, DependencyProperty.Register failed because my MarkupExtension doesn't inherit from DependencyObject. At this point I tried several ideas: AttachedProperties, changing the return type to Binding, containing my own dependency object behind the scenes that would serve as the databinding.
The databindings weren't initializing, so I simplified the problem. I used my contained DependencyObject class and placed it in the my Grid.Resources section. It seems that the DependencyObject needs to have a FrameworkElement as a parent to help it initialize its bindings. ...so back to the drawing board.
Finally, with a heavy heart, I abandoned my attempt at a markup extension and changed my class to derive from FrameworkElement. It worked like a charm:
<Grid Name="grid" Background="Yellow">
<local:DoubleMathExpression
Name="fiftyPercentWidthCalculator"
Operation="Add"
FirstArgument="1">
<local:DoubleMathExpression
Operation="Multiply"
FirstArgument="{Binding ElementName=grid, Path=ActualWidth}"
SecondArgument=".5"/>
</local:DoubleMathExpression>
<Border Background="Red"
Width="{Binding Path=Value, ElementName=fiftyPercentWidthCalculator}"/>
</Grid>
The main gotcha with this method is that it has to be used as an invisible element stuffed somewhere in one of your panels.
Maybe using a Transform isn't so bad after all. ;-)
[ XamlMath.cs ]
No comments:
Post a Comment