Tuesday 11 October 2011

Linking Dependency Properties By Default

Problem

Let's say you're wanting to create a custom control which has two labels, header and context. Although this is a rather painstakingly obvious example, in fact it the problem itself can rear its head in many places.

By default, you want both the header and context to have the same font size. However, being the good developer that you are, you also want to make sure that the control is extendable to let them differ if required.

Question is, in WPF, how do you do it?

Answer
Well first, as you always do, I scoured the internet. However it appears that nobody has even asked this question, let alone answer it. I must admit that I'm surprised because it is a reasonably common requirement when developing control suites.

So given there wasn't anything out there, I went on to write my own. Lets start with our little example xaml of our custom control.

<UserControl x:Class="AllInTheXaml_DefaultBetweenTwoDPs.Example1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" 
               FontSize="{Binding HeaderFontSize, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" 
               Content="{Binding Header, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"/>
        <Label Grid.Column="1" 
               FontSize="{Binding FontSize, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" 
               Content="{Binding Context, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}"/>
    </Grid>
</UserControl>
 
Great, we have our dependency properties. But wait, how do we actually resolve the issue? Step in the great power of bindings! All you need to do is add a one-way binding between the FontSize and the HeaderFontSize.


using System.Windows;
using System.Windows.Data;

namespace AllInTheXaml_DefaultBetweenTwoDPs
{
    public partial class Example1
    {
        public static DependencyProperty HeaderProperty = DependencyProperty.Register("Header"
            typeof(string), typeof(Example1), new PropertyMetadata("Header"));
        public string Header
        {
            get { return (string)GetValue(HeaderProperty); }
            set { SetValue(HeaderProperty, value); }
        }

        public static DependencyProperty ContextProperty = DependencyProperty.Register("Context"
            typeof(string), typeof(Example1), new PropertyMetadata("Context"));
        public string Context
        {
            get { return (string)GetValue(ContextProperty); }
            set { SetValue(ContextProperty, value); }
        }

        public static DependencyProperty HeaderFontSizeProperty = DependencyProperty.Register("HeaderFontSize",  
            typeof(int), typeof(Example1), new PropertyMetadata(11));
        public int HeaderFontSize
        {
            get { return (int) GetValue(HeaderFontSizeProperty); }
            set { SetValue(HeaderFontSizeProperty, value); }
        }

        public Example1()
        {            
            InitializeComponent();
            BindingOperations.SetBinding(this, HeaderFontSizeProperty,  
                new Binding("FontSize") { Source = this, Mode = BindingMode.OneWay});
        }
    }
}
 
This works because any override of the value of the dependency property clears the binding to whatever you set. That way, this will only be used when there is no explict reference to bind or set the HeaderFontSize.

And here are some nice little windows to show this all in action

<Window x:Class="AllInTheXaml_DefaultBetweenTwoDPs.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AllInTheXaml_DefaultBetweenTwoDPs" 
    Title="Window1" Height="100" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <local:Example1 Grid.Row="0" Header="Left" Context="Right" FontSize="{Binding Text, ElementName=size}"/>        
            <TextBox Text="12" x:Name="size" Grid.Row="1"/>                            
    </Grid>
</Window>
 




 
 
<Window x:Class="AllInTheXaml_DefaultBetweenTwoDPs.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AllInTheXaml_DefaultBetweenTwoDPs" 
    Title="Window1" Height="100" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>        
        <local:Example1 Grid.Row="0" Header="Left" Context="Right"  
            HeaderFontSize="{Binding Text, ElementName=headerSize}" FontSize="{Binding Text, ElementName=size}"/>        
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <TextBox Text="30" x:Name="headerSize" Grid.Column="0"/>
            <TextBox Text="12" x:Name="size" Grid.Column="1"/>
        </Grid>                               
    </Grid>
</Window>

Enjoy!

No comments:

Post a Comment