WPF - The Zoom Decorator: Part 3

|

Despite what the title says, this post is not about Decorators in WPF, the previous post in this series explains why decorators are not on the menu; the post before that gives the background to what we're going to delve into in this post.

Now that's out the way, on the with the show. In this post we're going to take the simple XAML we defined for zooming and turn that into a reusable Zoom control.

Migrating Loose XAML into a Control

Our first step is to migrate the loose XAML into a control, first lets define a class called Zoom that inherits from ContentControl.

namespace PaulJ.Windows.Controls
{
    using System;
    using System.Windows.Controls;

    public class Zoom : ContentControl
    {
        static Zoom()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(Zoom),
                new FrameworkPropertyMetadata(typeof(Zoom)));
        }
    }
}

The interesting part here is in the code for the static constructor; note that it overrides the metadata property for the controls DefaultStyle. What this says is that our new control's type is the resource key for it's style. What this enables us to do is define a style for our control without having to provide a resource key name for it, we can just specify the type - like you would for a Button or a TextBox.

For example, the following would replace the style for a Button:

<Style TargetType="{x:Type Button}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">

               <!-- Control Template XAML -->

            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Whatever you specify for the content of the control template will be how the button looks; as an x:Key value has not been specified the Type value will be used as the key - this will effectively change all buttons in scope. If we specify an x:Key value then only the Buttons that specify a Style property bound to that key would show the content specified in the new control template.

<Button /> <!-- Good -->
<Button Style="{StaticResource myButtonStyle}" /> <!-- Bad -->

If we did not override the metadata in the static constructor for our control, we would have to use the syntax shown for the second button in all the places we want to use our control; which would be a little tedious and would not give the same experience as using the built in controls.

So now we have a resource key defined and we know what we want our control to look like (the loose XAML already written), the question now is: where to put it, where do we specify the default style for our control? There are two things that we need to do to provide our a Default style: First, add attribute to our assembly and second, add a generic resource dictionary.

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None,
    ResourceDictionaryLocation.SourceAssembly)]

The ThemeInfoAttribute class is an assembly level attribute, and in the example above we are saying that we have no theme level specific resources (we're not replacing Aero or Luna here!) and we have a generic dictionary located within the source assembly i.e. our control assembly.

With this code in place, WPF is now going to look for a resource dictionary in the following location at run time:

pack://application:,,,/Themes/Generic.xaml

That resource dictionary is as close to System Scope as we can get with our assembly; without defining any new themes (something you will probably never do, unless you're writing an operating system or intend to replace Aero!).

With that file in place, we now have somewhere to dump our controls XAML. We only need to make a couple of minor adjustments to migrate the loose XAML in to a control template, I've highlighted some of important the changes:

<Style TargetType="{x:Type local:Zoom}">
<Setter Property="Template">
   <Setter.Value>
     <ControlTemplate TargetType="{x:Type local:Zoom}">
       <Grid Height="{TemplateBinding Height}"
             Width="{TemplateBinding Width}">
         <Border ...>
           <ContentControl ClipToBounds="True">
             <ContentPresenter ...>
                <ContentPresenter.RenderTransform>
                  <ScaleTransform ScaleX="{Binding Path=Value,
                              ElementName=PART_ZoomSlider}"
                                  ScaleY="{Binding Path=Value,
                              ElementName=PART_ZoomSlider}"/>
          ...
          
          <Slider x:Name="PART_ZoomSlider" ... />
          ...

</Style>

The key changes are the TargetType association to our control, the use of TemplateBinding rather than fixed values, meaning the values will be obtained at run time from the templated parent (which will be an instance of our control). Finally, I renamed the slider so that it uses a standard naming convention for template parts; other than that the template remains pretty much unchanged.

We now have a working control identical to the loose XAML version in functionality, but it can now be used like this:

<Page x:Class="PaulJ.Windows.MainPage"
   xmlns=..."
   xmlns:x="..."
   xmlns:z="clr-namespace:PaulJ.Windows.Controls;assembly=...">
    <Grid>
        OrangeZoom.JPG<z:Zoom>
            <Rectangle
                Height="150"
                Width="150"
                Fill="Orange" />
        </z:Zoom>
    </Grid>
</Page>

Fixing the ClipToBounds

There is a small problem with our current implementation: when we zoom to full-size we loose the border; the image below demonstrates:

ZoomMissingBorder.jpg

This is because the ClipToBounds property is applied to the Border, so while the contained element (a rectangle in this example) does not leak outside the bounds of the Border control, there is no room left for the control to draw the border lines. The simplest way to fix that is to introduce a child control to the Border that acts as the clipping container:

<Border BorderBrush="{TemplateBinding BorderBrush}"
        BorderThickness="{TemplateBinding BorderThickness}">
  <ContentControl ClipToBounds="True">
ZoomBorderFixed.JPG    <ContentPresenter ... />
  </ContentControl>
</Border>

I used a ContentControl for this purpose as it does not have any visual appearance of it's own, meaning it's lightweight, but it does participate in layout of the visual tree - so it is perfect for our needs. No more Border issues.

Adding some Knobs and Dials

Currently all the slider behaviour is static, you'd have to completely replace the template just to make the zoom go from 0 to 500 percent (the default is 200 percent), or to start at 25 percent instead of a hundred. You also would be unable to change any of these values for an animation effect or in even in code without jumping through lots of hoops.

So lets expose three core properties Value, Minimum and Maximum; after we've done that you'll easily see how you could add more flexibility to your version of the control.

The process is pretty straight forward: define a Dependency Property for each value we wish to represent, and then update the control template to use a TemplateBinding for that value:

public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
    "Value",
    typeof(double),
    typeof(Zoom),
    new UIPropertyMetadata(1.0));

This creates a Dependency property to represent the Value of the Zoom control, and then we expose the dependency property value by using a standard .NET property:

[Bindable(true), Category("Behavior")]
public double Value
{
    get { return (double)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

By doing with we make the dependency property accessible both from code and from XAML. Note that I've also added a couple of attributes to the property so that it is located sensibly in the property window of any UI designers, such as Expression Blend or Visual Studio, and I've also said that the value can be safely used for data binding.

You just repeat that process for the other properties and then we're done. The following link is WPF Browser Application (XBAP) that uses the new Zoom control's various different properties:

PaulJ.Windows.ZoomApplication.xbap 

I'm also making all the source code available for you to do with as you wish. All that I ask is that if you do anything cool with it then please send me a link or an email:

Download the Source

There are various improvements that could be made, such as adding animation to the zooming effect or adding the ability to move the zoom slider into different positions within the control. Also feel free to make these changes and then drop me a line.

Obviously if you find any bugs or have any problems with the code then please let me know.

Enjoy!

No comments: