Follow Me on Instagram Subscribe via RSS Feed

Hacking PivotViewer : Zoom

March 20, 2012

Hacking PivotViewer Series

If you have been following my blog, you know that there are quite a few posts here on PivotViewer.  If you haven’t seem them, you can find them all here: PivotViewer posts.  The Silverlight 5 PivotViewer provides a laundry list of new features and enhancements, but there is still several areas that simply weren’t addressed for one reason or another.

So that means it’s time to start taking matters in our own hands and start poking around under the hood.  This is the first post in a new series, Hacking PivotViewer.  This series will be a little different from some of the others, as it won’t be sequential.  As I have time to tackle different road blocks, I will post the results to this series.  Guess that means it’s time to get started.


Methods of Hacking

If you use Reflector on our good friend PivotViewer, you can quickly start to see what makes it tick.  Unfortunately, most of the good stuff is labeled as “internal”, which means that we don’t have access to it.  Correction, we are not supposed to have access to it.

When you run into this sort of situation, there are two different approaches you can take.  The first is to use reflection.  With reflection, you can gain access to an objects properties and methods (including private properties).  This includes accessing internal objects as well.  However, in Silverlight you have to run the application in ‘elevated trust’ mode in order to access internal classes.  Otherwise you will receive a security violation error.

The second approach is to hack the visual tree.  By gaining access to various parts of the visual tree, you can create hook points that will allow you to extend the PivotViewer.  This was done quite a bit for the original PivotViewer.  In fact, you can take a look at my CodePlex project that is centered around just that : PivotViewer Lessons.

If given the choice, I would rather use reflection.  You could create a public API that directly accessed the inner workings of PivotViewer.  Unfortunately, the security requirements make that a non-starter for many projects.  With that in mind, whenever possible, I will use the visual tree method.  Fortunately, zooming is one area we can do this in.

Exposing a Zoom API

When tackling a problem like this, there are always several approaches that you can take.  In fact, my first approach was scrapped in favor of the approach in this post.  PivotViewer has some internal view models that we could use reflection to get access to and modify.  However, the zoom slider control presents us an interesting entry point into the same view models.

By modifying the slider value we can change the zoom level and that is really all we need.  The API for this project will consist of 3 values : minimum, maximum, and value.  We will expose the min/max values simply to be a bit more informed. 

The first thing we need to do is to get a PivotViewer project up and running.  To speed things along, we will start with the project in my PivotViewer Basics: Client-Side Collections post.  You can download that project here: PVB01_ClientSideCreations.zip.

Now that we have a working sample project, we can get to work.  In order to encapsulate the code, we are going to create a new class the inherits the PivotViewer.  The first thing we are going to do is to add 3 dependency properties.  The reason we are using dependency properties is to open up binding possibilities.  If you would prefer, you could create simple properties in place of these.

public class CustomPivotViewer : PivotViewer
{
    public static readonly DependencyProperty ZoomMinProperty 
        = DependencyProperty.Register("ZoomMinimum", 
                                        typeof(double), 
                                        typeof(CustomPivotViewer), 
                                        null);

    public double ZoomMinimum
    {
        get { return (double)GetValue(ZoomMinProperty); }
        set { SetValue(ZoomMinProperty, value); }
    }

    public static readonly DependencyProperty ZoomMaxProperty 
        = DependencyProperty.Register("ZoomMaximum", 
                                        typeof(double), 
                                        typeof(CustomPivotViewer), 
                                        null);

    public double ZoomMaximum
    {
        get { return (double)GetValue(ZoomMaxProperty); }
        set { SetValue(ZoomMaxProperty, value); }
    }

    public static readonly DependencyProperty ZoomValueProperty 
        = DependencyProperty.Register("ZoomValue", 
                                        typeof(double), 
                                        typeof(CustomPivotViewer), 
                                        null);

    public double ZoomValue
    {
        get { return (double)GetValue(ZoomValueProperty); }
        set { SetValue(ZoomValueProperty, value); }
    } 
}

When a control’s UI gets generated in Silverlight, a method OnApplyTemplate is called.  You can override this method and this creates a great place to tie into elements of the visual tree.  The reason is, prior to this point, the visual tree elements do not exist. 

Let’s add an override of the OnApplyTemplate method.  Once the base control has created the visual tree, we can now find the zoom slider.  This is accomplished by using the GetTemplateChild method.  The method will return the Slider control that is used for zooming.  We will bind the Minimum, Maximum, and Value properites of the slider control to our 3 new dependency properties.

 

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    Slider templateChild1 
         = (Slider)base.GetTemplateChild("PART_ZoomSlider");
    var binding = new Binding("Minimum");
    binding.Source = templateChild1;
    SetBinding(ZoomMinProperty, binding);
    binding = new Binding("Maximum");
    binding.Source = templateChild1;
    SetBinding(ZoomMaxProperty, binding);
    binding = new Binding("Value");
    binding.Source = templateChild1;
    binding.Mode = BindingMode.TwoWay; 
    SetBinding(ZoomValueProperty, binding);

}

The last thing we are going to add is a method to allow us to increment the zoom value.  You can accomplish this my changing the ZoomValue directly, but this way we can handle increments as well.  The method does a little bounds checking and then adds the increment value to Value of the zoom slider.  Positive values will zoom in and negative values will zoom out.

public void Zoom(int val)
{
    var newVal = ZoomValue + val;
    ZoomValue = Math.Min(ZoomMaximum, 
        Math.Max(ZoomMinimum, newVal));
}

Armed with our new custom PivotViewer, let’s modify MainPage.xaml to use the it instead of the original version.

<conv:CustomPivotViewer x:Name="pViewer" Grid.Row="1">
    <pivot:PivotViewer.PivotProperties>
        <pivot:PivotViewerNumericProperty 
    Id="Value" 
    Options="None" 
    Binding="{Binding Value}"/>
        <pivot:PivotViewerStringProperty 
    Id="Data1" 
    Options="CanFilter,CanSearchText" 
                     
    Binding="{Binding Data1}"/>
        <pivot:PivotViewerStringProperty 
    Id="Color" 
    Options="CanFilter,CanSearchText" 
    Binding="{Binding Color}"/>
        <pivot:PivotViewerDateTimeProperty 
    Id="Stamp" 
    Options="CanFilter" 
    Binding="{Binding Stamp}"/>
    </pivot:PivotViewer.PivotProperties>
</conv:CustomPivotViewer>

And that is really all there is to is.  Of course, you probably want to make sure this actually works, right?  Well let’s do just that.  Below is a modification of the MainPage that adds a stack panel above the CustomPivotViewer.  It will display the min, max, and value of the current zoom.  It also contains two buttons, “In” and “Out”.  Care to guess what these are going to do?

<Grid x:Name="LayoutRoot" Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Orientation="Horizontal" Height="50">
        <StackPanel.Resources>
          <Style TargetType="TextBlock">
             <Setter Property="VerticalAlignment" Value="Center"/>
          </Style>
        </StackPanel.Resources>
        <TextBlock Text="Zoom Minimum : "/>
        <TextBlock Text="{Binding ZoomMinimum, 
                            ElementName=pViewer, 
                            StringFormat='0.0'}"/>
        <TextBlock Text="   Maximum : "/>
        <TextBlock Text="{Binding ZoomMaximum, 
                            ElementName=pViewer, 
                            StringFormat='0.0'}"/>
        <TextBlock Text="   Value : "/>
        <TextBlock Text="{Binding ZoomValue, 
                            ElementName=pViewer, 
                            StringFormat='0.0'}" Width="30"/>
        <Button Content="In" Width="30" Height="25" 
                x:Name="btnIn" Click="btnIn_Click"/>
        <Button Content="Out" Width="30" Height="25" 
                x:Name="btnOut" Click="btnOut_Click"/>
    </StackPanel>
    <conv:CustomPivotViewer x:Name="pViewer" Grid.Row="1">
        <pivot:PivotViewer.PivotProperties>
            <pivot:PivotViewerNumericProperty 
        Id="Value" 
        Options="None" 
        Binding="{Binding Value}"/>
            <pivot:PivotViewerStringProperty 
        Id="Data1" 
        Options="CanFilter,CanSearchText" 
                     
        Binding="{Binding Data1}"/>
            <pivot:PivotViewerStringProperty 
        Id="Color" 
        Options="CanFilter,CanSearchText" 
        Binding="{Binding Color}"/>
            <pivot:PivotViewerDateTimeProperty 
        Id="Stamp" 
        Options="CanFilter" 
        Binding="{Binding Stamp}"/>
        </pivot:PivotViewer.PivotProperties>
    </conv:CustomPivotViewer>

</Grid>

The last thing we need to do is to implement the Click handlers for the two buttons.  Both simply call our Zoom method we created to increment the zoom level.

private void btnIn_Click(object sender, RoutedEventArgs e)
{
    pViewer.Zoom(1);
}

private void btnOut_Click(object sender, RoutedEventArgs e)
{
    pViewer.Zoom(-1);
}

Now if we run the project, you should get something that looks like this:

image

The buttons will allow you to zoom in and out.  PivotViewer handles all of the nice smooth transitions for you and everything.  If you want to zoom out to the default view, you just need to set ZoomValue to 1.0.

You will start to see more of these hacks over the next few months.  I am working on a Silverlight 5 version of PivotViewer Lessons to help centralize all of these changes.

You can download the completed project here : HackingPV_Zoom.zip

Series NavigationHacking PivotViewer : Pan >>

Leave a Reply