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:
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?