Thursday, December 13, 2007

Zoom multiple controls from the same slider in XAML

Recently we've been playing around with XAML and WPF at work, and a common situation we run across is wanting to zoom different Page's without having separate zoom functionality. One slider should be enough to zoom any Page we hook up.

The basics start with an attached DependencyProperty called Zoom.
public static class AttachedProperties
{
public static readonly DependencyProperty ZoomProperty
= DependencyProperty.RegisterAttached("Zoom", typeof(double), typeof(UIElement),
new FrameworkPropertyMetadata(1.0,
FrameworkPropertyMetadataOptions.AffectsRender, null /* PropChangedCallback */,
new CoerceValueCallback((obj, value) => (double)value < 0.0 ? 0.0 : (double)value)));

public static double GetZoom(DependencyObject obj)
{
return (double)obj.GetValue(ZoomProperty);
}

public static void SetZoom(DependencyObject obj, object value)
{
double val = Double.Parse(value.ToString());
obj.SetValue(ZoomProperty, val);
}
}
Now we have an attached property we can connect to any UI Element! Notice that I suffixed 'Zoom' with '-Property' for the static variable, yet gave RegisterAttached just 'Zoom'. This is by convention (coincidentally so are the Get/Set pair below the static value).
<Page x:Class="ZoomTest.Page1" x:Name="page" xmlns="...">
xmlns:local="clr-namespace:ZoomTest"
local:AttachedProperties.Zoom="0.8">
<Grid />
</Page>
Now we have a Page that starts with a default zoom property of 80%. However, this does not do us much good. We don't actually tell the page how to zoom anywhere. This is where Binding comes into play.
<Page x:Class="ZoomTest.Page1" x:Name="page" xmlns="...">
xmlns:local="clr-namespace:ZoomTest"
local:AttachedProperties.Zoom="0.8">
<Grid LayoutTransform="{Binding Zoom, ElementName=page}">
</Grid>
</Page>
Now anything inside of the Grid will get a LayoutTransform based on the Zoom property of the Page! Unfortunately the LayoutTransform requires an actual Transform and not a double. Now we could have made the Zoom property a ScaleTransform, but that makes it less useful as an attached property. However, all is not lost:
public class ZoomConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Transform))
throw new InvalidOperationException();

double val = Double.Parse(value.ToString()) / 100.0;
return new ScaleTransform(val, val);
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
Now we just have to hook the converter up to the data binding, and viola! We have a Page who's Grid content zooms in and out with respect to a given scale.
<Page x:Class="ZoomTest.Page1" x:Name="page" xmlns="...">
xmlns:local="clr-namespace:ZoomTest"
local:AttachedProperties.Zoom="0.8">
<Page.Resources><local:ZoomConverter x:Key="zoomConverter" /></Page.Resources>
<Grid LayoutTransform="{Binding Zoom, ElementName=page, Converter={StaticResource zoomConverter}}">
</Grid>
</Page>
At this point all we have to do is wire up a Slider (two step process wraaa)!
<Window x:Class="GeometryTest.Window1" x:Name="window"
xmlns="..." local="clr-namespace:ZoomTest"
Title="Zoom Test" Height="480" Width="640">
<DockPanel LastChildFill="True">
<ToolBarTray DockPanel.Dock="Top">
<ToolBar>
<Label>Zoom:</Label><Slider x:Name="zoomSlider" Minimum="1" Maximum="100" Value="50" />
</ToolBar>
</ToolBarTray>
<Frame Source="Page1.xaml" LoadCompleted="ContentFrame_LoadCompleted" />
</DockPanel>
</Window>
...
void ContentFrame_LoadCompleted(object sender, NavigationEventArgs e)
{
Binding binding = new Binding();
binding.Source = zoomSlider;
binding.Path = new PropertyPath("Value");
(ContentFrame.Content as UIElement).SetBinding(AttachedProperties.ZoomProperty, binding);
}
Now, changing the zoom value with the Slider will cause the Grid to zoom in and out! Bonus points for adding a ScrollViewer to the Page to let you actually see the change in size of your Grid.