Monday, January 28, 2008

Spending quality time to get to know MarkupExtensions

I've been playing with MarkupExtensions, and after a day of trial and mostly error, there was both delight and heartache. I'll give you the summary, as well as the long and rambly story.

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: