[WPF] Creating parameterized styles with attached properties

Very poorPoorAverageGoodExcellent (5 votes) 
Loading ... Loading ...

Today I’d like to share a trick that I used quite often in the past few months. Let’s assume that in order to improve the look of your application, you created custom styles for the standard controls:

OK, I’m not a designer… but it will serve the purpose well enough to illustrate my point ;). These styles are very simple, they’re just the default styles of CheckBox and RadioButton in which I only changed the templates to replace the BulletChromes with these awesome blue tick marks. Here’s the code:

        <Style x:Key="{x:Type CheckBox}" TargetType="{x:Type CheckBox}">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="Background" Value="{StaticResource CheckBoxFillNormal}"/>
            <Setter Property="BorderBrush" Value="{StaticResource CheckBoxStroke}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="FocusVisualStyle" Value="{StaticResource EmptyCheckBoxFocusVisual}"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type CheckBox}">
                        <BulletDecorator Background="Transparent"
                                         SnapsToDevicePixels="true">
                            <BulletDecorator.Bullet>
                                <Border BorderBrush="{TemplateBinding BorderBrush}"
                                        Background="{TemplateBinding Background}"
                                        BorderThickness="1"
                                        Width="11" Height="11" Margin="0,1,0,0">
                                    <Grid>
                                        <Path Name="TickMark"
                                              Fill="Blue"
                                              Data="M0,4 5,9 9,0 4,5"
                                              Visibility="Hidden" />
                                        <Rectangle Name="IndeterminateMark"
                                                   Fill="Blue"
                                                   Width="7" Height="7"
                                                   HorizontalAlignment="Center"
                                                   VerticalAlignment="Center"
                                                   Visibility="Hidden" />
                                    </Grid>
                                </Border>
                            </BulletDecorator.Bullet>
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              Margin="{TemplateBinding Padding}"
                                              RecognizesAccessKey="True"
                                              SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </BulletDecorator>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasContent" Value="true">
                                <Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
                                <Setter Property="Padding" Value="4,0,0,0"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="TickMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="IsChecked" Value="{x:Null}">
                                <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="{x:Type RadioButton}" TargetType="{x:Type RadioButton}">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
            <Setter Property="Background" Value="#F4F4F4"/>
            <Setter Property="BorderBrush" Value="{StaticResource CheckBoxStroke}"/>
            <Setter Property="BorderThickness" Value="1"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type RadioButton}">
                        <BulletDecorator Background="Transparent">
                            <BulletDecorator.Bullet>
                                <Grid VerticalAlignment="Center" Margin="0,1,0,0">
                                    <Ellipse Width="11" Height="11"
                                             Stroke="{TemplateBinding BorderBrush}"
                                             StrokeThickness="1"
                                             Fill="{TemplateBinding Background}" />
                                    <Ellipse Name="TickMark"
                                             Width="7" Height="7"
                                             Fill="Blue"
                                             Visibility="Hidden" />
                                    <Ellipse Name="IndeterminateMark"
                                             Width="3" Height="3"
                                             Fill="Blue"
                                             Visibility="Hidden" />
                                </Grid>
                            </BulletDecorator.Bullet>
                            <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                              Margin="{TemplateBinding Padding}"
                                              RecognizesAccessKey="True"
                                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </BulletDecorator>
                        <ControlTemplate.Triggers>
                            <Trigger Property="HasContent" Value="true">
                                <Setter Property="FocusVisualStyle" Value="{StaticResource CheckRadioFocusVisual}"/>
                                <Setter Property="Padding" Value="4,0,0,0"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                            </Trigger>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="TickMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="IsChecked" Value="{x:Null}">
                                <Setter TargetName="IndeterminateMark" Property="Visibility" Value="Visible" />
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

OK, so you now have beautiful controls that are going to make the app a big success, management is happy, everything is for the best… until you realize that in another view of the application, the controls need to have the same style, but with green tick marks!

The first solution that comes to mind is to duplicate the style, and replace blue with green in the copy. But since you’re a good developer who cares about best practices, you know that duplicate code is evil: if you ever need to make changes to the style of the blue CheckBox, you will also have to modify the green one… and perhaps the red one, and the black one, etc. Clearly it would soon become unmanageable. So we need to refactor, but how? Ideally we would pass parameters to the style, but a style is not a method that you can call with various parameters…

What we need is an extra property that controls the color of the tick marks, so we can bind to this property in the template. A possible approach is to create custom controls that inherit CheckBox and RadioButton, with an extra TickBrush property… but personnally I don’t really like this approach, I always prefer to use the built-in controls when they can fit the bill.

Anyway, there is an easier solution: we just need to create a ThemeProperties class with an attached property of type Brush:

    public static class ThemeProperties
    {
        public static Brush GetTickBrush(DependencyObject obj)
        {
            return (Brush)obj.GetValue(TickBrushProperty);
        }

        public static void SetTickBrush(DependencyObject obj, Brush value)
        {
            obj.SetValue(TickBrushProperty, value);
        }

        public static readonly DependencyProperty TickBrushProperty =
            DependencyProperty.RegisterAttached(
                "TickBrush",
                typeof(Brush),
                typeof(ThemeProperties),
                new FrameworkPropertyMetadata(Brushes.Black));
    }

We change the templates a bit to replace the hard-coded color with a binding to this property:

                                ...

                                <!-- CheckBox -->
                                        <Path Name="TickMark"
                                              Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                              Data="M0,4 5,9 9,0 4,5"
                                              Visibility="Hidden" />
                                        <Rectangle Name="IndeterminateMark"
                                                   Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                                   Width="7" Height="7"
                                                   HorizontalAlignment="Center"
                                                   VerticalAlignment="Center"
                                                   Visibility="Hidden" />

                                ...

                                <!-- RadioButton -->
                                    <Ellipse Name="TickMark"
                                             Width="7" Height="7"
                                             Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                             Visibility="Hidden" />
                                    <Ellipse Name="IndeterminateMark"
                                             Width="3" Height="3"
                                             Fill="{TemplateBinding my:ThemeProperties.TickBrush}"
                                             Visibility="Hidden" />

And when we use the controls, we set the property to the desired tick color:

<CheckBox Content="Checked" IsChecked="True" my:ThemeProperties.TickBrush="Blue" />

So we can now have controls that share the same style, but have different colors for the tick mark:

Isn’t it great? However there is a small problem left: since controls on the same view all use the same tick color, it’s not very convenient to repeat the color on each one. It would be nice to be able to specify the color just once, on the root of the view… Well, as it happens, dependency properties have a nice feature that allows to do exactly that: value inheritance. We just need to specify the Inherits flag in the declaration of the TickBrush property:

        public static readonly DependencyProperty TickBrushProperty =
            DependencyProperty.RegisterAttached(
                "TickBrush",
                typeof(Brush),
                typeof(ThemeProperties),
                new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.Inherits));

With this flag, the property becomes “ambient”: we only need to specify its value on a parent control, and all descendant controls will automatically inherit the value. So if you need a view where all the checkboxes and radiobuttons are red, just set the TickBrush property to Red on the root element of the view.

Obviously this concept can be extended to other cases: actually, every time an element of the template must change based on arbitrary criteria, this technique can be used. It can be a good alternative to duplicating a template when you only need to change a small part of it.

kick it on DotNetKicks.com

[WPF] Markup extensions and templates

Very poorPoorAverageGoodExcellent (2 votes) 
Loading ... Loading ...

Note : This post follows the one about a a markup extension that can update its target, and reuses the same code.

You may have noticed that using a custom markup extension in a template sometimes lead to unexpected results… In this post I’ll explain what the problem is, and how to create a markup extensions that behaves correctly in a template.

The problem

Let’s take the example from the previous post : a markup extension which gives the state of network connectivity, and updates its target when the network is connected or disconnected :

<CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />

Now let’s put the same CheckBox in a ControlTemplate :

<ControlTemplate x:Key="test">
  <CheckBox IsChecked="{my:NetworkAvailable}" Content="Network is available" />
</ControlTemplate>

And let’s create a control which uses this template :

<Control Template="{StaticResource test}" />

If we disconnect from the network, we notice that the CheckBox is not automatically updated by the NetworkAvailableExtension, whereas it was working fine when we used it outside the template…

Explanation and solution

The markup expression is evaluated when it is encountered by the XAML parser : in that case, when the template is parsed. But at this time, the CheckBox control is not created yet, so the ProvideValue method can’t access it… When a markup extension is evaluated inside a template, the TargetObject is actually an instance of System.Windows.SharedDp, an internal WPF class.

For the markup extension to be able to access its target, it has to be evaluated when the template is applied : we need to defer its evaluation until this time. It’s actually pretty simple, we just need to return the markup extension itself from ProvideValue : this way, it will be evaluated again when the actual target control is created.

To check if the extension is evaluated for the template or for a “real” control, we just need to test whether the type of the TargetObject is System.Windows.SharedDp. So the code of the ProvideValue method becomes :

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (target != null)
            {
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;
                _targetObject = target.TargetObject;
                _targetProperty = target.TargetProperty;
            }

            return ProvideValueInternal(serviceProvider);
        }

Cool, it’s now fixed, the CheckBox is updated when the network connectivity changes :)

Last, but not least

OK, we have a solution that apparently works fine, but let’s not count our chickens before they’re hatched… What if we now want to use our ControlTemplate on several controls ?

<Control Template="{StaticResource test}" />
<Control Template="{StaticResource test}" />

Now let’s run the application and unplug the network cable : the second CheckBox is updated, but the first one is not…

The reason for this is simple : there are two CheckBox controls, but only one instance of NetworkAvailableExtension, shared between all instances of the template. Now, NetworkAvailableExtension can only reference one target object, so only the last one for which ProvideValue has been called is kept…

So we need to keep track of not one target object, but a collection of target objects, which will all be update by the UpdateValue method. Here’s the final code of the UpdatableMarkupExtension base class :

    public abstract class UpdatableMarkupExtension : MarkupExtension
    {
        private List<object> _targetObjects = new List<object>();
        private object _targetProperty;

        protected IEnumerable<object> TargetObjects
        {
            get { return _targetObjects; }
        }

        protected object TargetProperty
        {
            get { return _targetProperty; }
        }

        public sealed override object ProvideValue(IServiceProvider serviceProvider)
        {
            // Retrieve target information
            IProvideValueTarget target = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;

            if (target != null && target.TargetObject != null)
            {
                // In a template the TargetObject is a SharedDp (internal WPF class)
                // In that case, the markup extension itself is returned to be re-evaluated later
                if (target.TargetObject.GetType().FullName == "System.Windows.SharedDp")
                    return this;

                // Save target information for later updates
                _targetObjects.Add(target.TargetObject);
                _targetProperty = target.TargetProperty;
            }

            // Delegate the work to the derived class
            return ProvideValueInternal(serviceProvider);
        }

        protected virtual void UpdateValue(object value)
        {
            if (_targetObjects.Count > 0)
            {
                // Update the target property of each target object
                foreach (var target in _targetObjects)
                {
                    if (_targetProperty is DependencyProperty)
                    {
                        DependencyObject obj = target as DependencyObject;
                        DependencyProperty prop = _targetProperty as DependencyProperty;

                        Action updateAction = () => obj.SetValue(prop, value);

                        // Check whether the target object can be accessed from the
                        // current thread, and use Dispatcher.Invoke if it can't

                        if (obj.CheckAccess())
                            updateAction();
                        else
                            obj.Dispatcher.Invoke(updateAction);
                    }
                    else // _targetProperty is PropertyInfo
                    {
                        PropertyInfo prop = _targetProperty as PropertyInfo;
                        prop.SetValue(target, value, null);
                    }
                }
            }
        }

        protected abstract object ProvideValueInternal(IServiceProvider serviceProvider);
    }

The UpdatableMarkupExtension is now fully functional… until proved otherwise ;). This class makes a good starting point for any markup extension that needs to update its target, without having to worry about the low-level aspects of tracking and updating target objects.

css.php