Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Breaking Apart the Margin Property in Xaml for better Binding

Pete Brown - 08 May 2010

WARNING: This post doesn't have a solution. It's more like one of those old "In Search Of" episodes where you'd see a lot of talking, an interview with a 90 year old guy you can't understand, and some fuzzy photographs, but no closure.

When looking at twitter tonight, I stumbled across this:

image

I read it and thought "You should be able to break out margins using property element syntax, but I'd have to test to see if you could then use binding". Ok, so that was a long thought :)

Ideally, any solution would need to support:

  • Xaml-only binding setup, using the data context
  • Binding using a code-only object as a data source to set the margins, and change them at runtime
  • Binding using a slider (or similar) using element binding to change the margins
  • Binding individual values (left, top, right, bottom)

Margins in Xaml using the Converter

The Margin property (probably the Thickness type, actually) has a built-in type converter that converts the text we normally use to specify a margin:

<Grid Margin="15, 5, 25, 20">
            
</Grid>

or

<Grid Margin="15 5 25 20">
            
</Grid>

..into a Thickness object with the appropriate properties. There's another way to specify the margin, however.

Using Property-Element Syntax

That property string is just a convenience. The long-winded way of writing the same thing is this:

<Grid>
    <Grid.Margin>
        <Thickness Left="15"
                   Top="5"
                   Right="25"
                   Bottom="20" />
    </Grid.Margin>
</Grid>

NOTE: This only works in WPF. In Silverlight, there's an internal runtime check on the Thickness properties that throws an error if you try and set the individual properties from xaml.

That lets you break them out into separate properties, perhaps to make it easier to read. Nevertheless, since Thickness is a struct, not a DependencyObject, you won't be able to use binding to set those individual properties.

However, the Margin property itself is a dependency property. All we need to do is create something we *can* bind to, and use a custom ValueConverter to take care of the rest.

Using a Custom Value Converter

There are likely a thousand ways to solve this issue. Without trying them out, I imagine you could use a behavior, or attached property, or simply a property on the Window (Page) itself if you only need one of these, and perhaps end up with a working solution. However, I couldn't find a single solution that worked for binding.

Here's the xaml using the custom thickness and value converter. You can also hang a Thickness property off the CustomThickness class and do the value conversion in there, eliminating the external value converter.

<Grid>
    <Grid.Resources>
        <local:CustomThicknessValueConverter x:Key="CustomThicknessValueConverter" />
    </Grid.Resources>
        
    <Grid>
        <Grid.Margin>
            <Binding Converter="{StaticResource CustomThicknessValueConverter}">
                <Binding.Source>
                    <local:CustomThickness Left="15"
                                           Top="5"
                                           Right="25"
                                           Bottom="20" />
                </Binding.Source>
            </Binding>
        </Grid.Margin>
            
        <Rectangle Fill="CornflowerBlue" />
    </Grid>
        
</Grid>

Yep, that's pretty ugly. However, it does get us one step closer, and shows yet another way to handle the initial binding. However, there are a few problems. 1. The binding is one-time and never updates, and 2. if you try and use binding inside the CustomThickness properties themselves, WPF will balk as I derived CustomThickness from DependencyObject instead of FrameworkElement. That brings me to problem number 3. Deriving a custom margin class from FrameworkElement is just … wrong.

Here's the value converter

public class CustomThicknessValueConverter: IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Debug.WriteLine("Convert");

        CustomThickness custom = value as CustomThickness;

        if (custom != null)
        {
            return new Thickness(custom.Left, custom.Top, custom.Right, custom.Bottom);
        }
        else
        {
            return new Thickness(0, 0, 0, 0);
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

For reference here's the CustomThickness class with the added Thickness property, eliminating the need for the converter

public class CustomThickness : DependencyObject // FrameworkElement to be a binding target
{
    public CustomThickness()
        : base()
    {
    }

    public double Left
    {
        get { return (double)GetValue(LeftProperty); }
        set { SetValue(LeftProperty, value); }
    }

    public static readonly DependencyProperty LeftProperty =
        DependencyProperty.Register("Left", typeof(double), typeof(CustomThickness), new UIPropertyMetadata(0.0, OnPropertyChanged));


    private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        CustomThickness custom = d as CustomThickness;

        Thickness thickness = new Thickness(custom.Left, custom.Top, custom.Right, custom.Bottom);
        custom.Thickness = thickness;

        Debug.WriteLine("CustomThickness: Property Changed");
    }



    public double Top
    {
        get { return (double)GetValue(TopProperty); }
        set { SetValue(TopProperty, value); }
    }

    public static readonly DependencyProperty TopProperty =
        DependencyProperty.Register("Top", typeof(double), typeof(CustomThickness), new UIPropertyMetadata(0.0, OnPropertyChanged));



    public double Right
    {
        get { return (double)GetValue(RightProperty); }
        set { SetValue(RightProperty, value); }
    }

    public static readonly DependencyProperty RightProperty =
        DependencyProperty.Register("Right", typeof(double), typeof(CustomThickness), new UIPropertyMetadata(0.0, OnPropertyChanged));



    public double Bottom
    {
        get { return (double)GetValue(BottomProperty); }
        set { SetValue(BottomProperty, value); }
    }

    public static readonly DependencyProperty BottomProperty =
        DependencyProperty.Register("Bottom", typeof(double), typeof(CustomThickness), new UIPropertyMetadata(0.0, OnPropertyChanged));




    public Thickness Thickness
    {
        get { return (Thickness)GetValue(ThicknessProperty); }
        set { SetValue(ThicknessProperty, value); }
    }

    // Using a DependencyProperty as the backing store for Thickness.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ThicknessProperty =
        DependencyProperty.Register("Thickness", typeof(Thickness), typeof(CustomThickness), new UIPropertyMetadata());

        
}

 

 

So, let's help Paul Jenkins out, and school me as well. Any clever solutions?

       
posted by Pete Brown on Saturday, May 8, 2010
filed under:        

6 comments for “Breaking Apart the Margin Property in Xaml for better Binding”

  1. Andrew Chisholmsays:
    One solution I've used for binding margins is a standard converter that uses scale multipliers for each dimension, admittedly it doesn't solve your problem because it only really lets you bind to one value but it usually does the trick for what I need, including binding a slider to a thickness e.g.

    <custom:ThicknessScaleConverter TopMultiplier="2" LeftMultiplier="1" RightMultiplier="2" BottomMultiplier="1" />

    This means if you bound to a property with a value of 5 the thickness would be "5, 10, 10, 5".
  2. Neal Borellisays:
    I think you can just use a IMultiValueConverter to solve this problem if you want to bind the individual values. For example, something like this should work:

    /// <summary>
    /// Converts one or more double values into a <see cref="Thickness" />.
    /// </summary>
    [ValueConversion(typeof(object), typeof(Thickness))]
    public class ThicknessConverter: IValueConverter, IMultiValueConverter
    {
    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
    return new Thickness((Double) value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
    throw new NotImplementedException();
    }

    #endregion

    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
    switch(values.Length)
    {
    case 0:
    return null;
    case 1:
    return new Thickness((Double) values[0]);
    case 2:
    case 3:
    return new Thickness((Double) values[0], (Double) values[1], (Double) values[0], (Double) values[1]);
    default:
    return new Thickness((Double) values[0], (Double) values[1], (Double) values[2], (Double) values[3]);
    }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
    throw new NotImplementedException();
    }

    #endregion
    }

    You would like it like this:

    In resource add:
    <local:ThicknessConverter x:Key="ThicknessConverter" />

    You can then use it as follows:

    <Border>
    <Border.Thickness>
    <MultiBinding Converter="{StaticResource ThicknessConverter}"
    <Binding Path="BorderWidth" />
    <Binding Path="BorderHeight" />
    </MultiBinding>
    </Border.Thickness>
    </Border>

    You could also use a JScript or Lambda converter to solve this in a more general way, but these solutions tend to be WPF only and will not work in SilverLight. For example, with the JScript converter, you could do something like this:

    <MultiBinding Converter="{StaticResource JScript}"
    ConverterParameter="new System.Windows.Thickness(values[0]*0.1/2,values[1]*0.1/2,0,0)">
    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="ActualWidth" />
    <Binding RelativeSource="{RelativeSource TemplatedParent}" Path="ActualHeight" />
    </MultiBinding>

    See http://www.11011.net/wpf-binding-expressions for more details on the JScript converter.
  3. Dan Puzeysays:
    I've used in the past something slightly more generic than Josh's solution. Basically, *two* attached properties: one for the property to be bound, and one for the value. This then has the advantage of allowing you to bind any (single) unbindable property on a XAML element. You can only easily support one-way bindings but for display purposes that's typically enough.

    I did start on a version that took lists of property names/values in xaml, but it was ugly markup and I didn't have enough use for it to continue...
  4. Bruce Piersonsays:
    Why not just declare a double value as a resource?? This works for me...

    <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib">

    <sys:Double x:Key="ContentBorderMargin">5</sys:Double>

    <Style x:Key="ContentBorderStyle" TargetType="{x:Type Border}">
    <Setter Property="Margin">
    <Setter.Value>
    <Thickness
    Left="{StaticResource ContentBorderMargin}"
    Top="{StaticResource ContentBorderMargin}"
    Right="0"
    Bottom="0" />
    </Setter.Value>
    </Setter>
    </Style>
    </ResourceDictionary>

Comment on this Post

Remember me