Hacking PivotViewer : Pan

March 27, 2012 2 Comments

Hacking PivotViewer Series

Continuing on with the Hacking PivotViewer series, in this post we will look at accessing and modifying the current pan position.  As I mentioned in the first post, Hacking PivotViewer : Zoom, there is always more than one way to attack some of these problems.  In doing the research for this post, I came across a new approach that will allow access to internal structures without the need for using elevated permissions.  This post will explore this new approach as well as build a new API for panning within the PivotViewer.

Before we get into panning within the PivotViewer, let’s take a look at this new approach.  In the first post of this series, I discussed two approaches for getting access to the internals of the PivotViewer: visual tree hacking and using reflection.  While reflection was a cleaner approach, it requires elevated permissions to access information about a class marked as internal.  The problem with the visual tree approach is there needs to be part of the UI which indirectly exposes access to what you are trying to do.  In the case of zooming, the zoom slider allowed us to build a zoom API by modifying its current value.  However, the PivotViewer does not expose a UI control that will allow us to accomplish the same thing for panning.

Using the second approach, reflection, will require us to set the elevated trust permission on our application in order to use the API.  While this is possible, it can certainly be a hurdle for some public implementations.  So how do we overcome this situation?  Well it turns out that binding to a property of an internal class does not fire off a security warning.  That’s right boys and girls.  As long as the class you are binding to supports INotifyPropertyChanged, we can bind to it and build an API around it.  Of course, the best way to explain this is to show it in action.

To get things started, we are going to begin with the project from Hacking PivotViewer : Zoom.  You can download that project here: HackingPivotViewer_Zoom.zip

Just as we did with our zoom API, we are going to build our API with dependency properties and defining bindings by overriding the OnApplyTemplate method.  For this project we need to create 3 dependency properties in our CustomPivotViewer class.  The first will be the ZoomPanState.  We will bind this property to the ZoomPanState object in the CollectionViewerViewModel.  This is actually the object that controls the pan and the zoom.  We will access the CollectionViewerViewModel through the visual tree as we did in the previous post.

The next two properties are the PanOffset and the TargetPanOffset.  The PanOffset property binds to the Offset of the ZoomPanState.  This is a Point object that defines the current pan offset.  The TargetPanOffset binds to the TargetOffset property.  Since PivotViewer uses zoom animations for everything, modifying the Offset property isn’t what you want to change.  If you modify the TargetOffset, then the PivotViewer will create a smooth transition from its current Offset to the new TargetOffset.  It is also important to note that we will create a two way binding for the PanTargetOffset.  If you don’t, you will spend a while banging your head into your keyboard trying to figure out why values aren’t updating (or maybe that was just me).

public static readonly DependencyProperty ZoomPanStateProperty 
    = DependencyProperty.Register("ZoomPanState", 
                                typeof(object), 
                                typeof(CustomPivotViewer), null);

public object ZoomPanState
{
    get { return GetValue(ZoomPanStateProperty); }
    set { SetValue(ZoomPanStateProperty, value); }
}

public static readonly DependencyProperty PanOffsetProperty 
    = DependencyProperty.Register("PanOffset", 
                                typeof(Point), 
                                typeof(CustomPivotViewer), null);

public Point PanOffset
{
    get { return (Point)GetValue(PanOffsetProperty); }
    set { SetValue(PanOffsetProperty, value); }
}

public static readonly DependencyProperty TargetPanOffsetProperty 
    = DependencyProperty.Register("TargetPanOffset", 
                                typeof(Point), 
                                typeof(CustomPivotViewer), null);

public Point TargetPanOffset
{
    get { return (Point)GetValue(TargetPanOffsetProperty); }
    set { SetValue(TargetPanOffsetProperty, value); }
}

The next thing we need to do is to populate these values.  As I mentioned above, this will be done via binding.  By using Reflector, I was able to determine the structure of the CollectionViewViewModel class.  The property we are interested in is the ZoomPanState.  We will create a binding to our ZoomPanState dependency property.  The two offest properties are mapped to properties of the ZoomPanState.  It’s important to remember that this is an internal class, and therefore we shouldn’t be able to access the members.  However, by using a little binding and understanding the structure of the class in question, we are able to expose access to the class and it’s properties.

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

    Grid templateChild = (Grid)base.GetTemplateChild("PART_Container");

    var coll = templateChild.Children[2] as Control;
    var viewModel = coll.Resources["ViewModel"];

    var bind = new Binding("ZoomPanState");
    bind.Source = viewModel;
    SetBinding(ZoomPanStateProperty, bind);

    bind = new Binding("Offset");
    bind.Mode = BindingMode.TwoWay;
    bind.Source = ZoomPanState;
    SetBinding(PanOffsetProperty, bind);

    bind = new Binding("TargetOffset");
    bind.Mode = BindingMode.TwoWay;
    bind.Source = ZoomPanState;
    SetBinding(TargetPanOffsetProperty, bind);

}

With that in mind, we will create a Pan method on our CustomPivotViewer that has two parameters: x and y.  These parameters are going to work as you would expect.  The x parameter will handle horizontal offsets and the y will handle the vertical.  The method simply sets the PanTargetOffset to a new Point based on the current PanTargetOffset adjusted with the new values.

public void Pan(double x, double y)
{
    TargetPanOffset = new Point(PanOffset.X + x, PanOffset.Y + y);
}

There are a couple of things to notice about the pan values.  The values are based off of the total area of the collection based at its current zoom level.  So the more you zoom in, the larger your canvas is.  The second thing is that PivotViewer will do some bounds checking for you and not let you zoom pass the edge of the collection.  Therefore, it’s not something you need to worry about writing code for.  As of this writing, I haven’t located the current size of the collection.  This would come in handy when you want to scale the amount you pan based on the current size. But alas, we can’t have everything that we want. Smile

Now let’s create a UI to test the new API.  We will replace the header we added in the last post with a new one to examine the pan API.  It consists of the current x and y offsets and four buttons (left, top, right, bottom).

<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="Offset X : "/>
    <TextBlock Text="{Binding PanOffset.X, 
                    ElementName=pViewer, 
                    StringFormat='0.0'}"/>
    <TextBlock Text="   Offset Y : "/>
    <TextBlock Text="{Binding PanOffset.Y, 
                    ElementName=pViewer, 
                    StringFormat='0.0'}"
                Margin="0,0,20,0"/>
    <Button Content="Left" Width="40" Height="25" 
        x:Name="btnLeft" Click="btnLeft_Click"/>
    <Button Content="Up" Width="40" Height="25" 
        x:Name="btnUp" Click="btnUp_Click"/>
    <Button Content="Right" Width="40" Height="25" 
        x:Name="btnRight" Click="btnRight_Click"/>
    <Button Content="Down" Width="40" Height="25" 
        x:Name="btnDown" Click="btnDown_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>

The click event handlers make use of our new Pan method to set the offset in the correct directions.

private void btnDown_Click(object sender, RoutedEventArgs e)
{
    pViewer.Pan(0,100);
}

private void btnRight_Click(object sender, RoutedEventArgs e)
{
    pViewer.Pan(100,0);
}

private void btnUp_Click(object sender, RoutedEventArgs e)
{
    pViewer.Pan(0,-100);
}

private void btnLeft_Click(object sender, RoutedEventArgs e)
{
    pViewer.Pan(-100,0);
}

Running the project your application should look like this.

image

When you are zoomed out panning will not work, so don’t get discouraged.  This is because the size of the collection canvas is equal to that of the available space.  So trying to pan will hit the bounds checking that PivotViewer does.  Once you zoom in, you should be able to pan around using the four buttons we added.  You will also notice that PivotViewer makes nice smooth transitions when you pan.  This is a result of using the TargetOffset and allowing PivotViewer to handle the actual transitions.

Well there you have it.  We can now zoom and pan the PivotViewer.  You can download the code for this project here: HackingPivotViewer_Pan.zip

This series will continue as more customizations are created for the new PivotViewer.  Make sure to check back for future updates.

Series Navigation<< Hacking PivotViewer : Zoom

Comments (2)

Trackback URL | Comments RSS Feed

  1. Juliana says:

    Hi Tony,

    Is it possible to something similar to switch between selected item to the right or to the left from current selected item?

  2. Tony Champion says:

    You just gave me the subject for my next Hacking PivotViewer post. :)

    Yes, you should be able to using the same techniques. I’ll need to do some research to determine the best way to expose it.

Leave a Reply