Customizing the PivotViewer

For those of you who haven’t seen it yet, Microsoft released a very powerful data visualization control called the PivotViewer. It leverages the deep zoom feature to perform filtering and sorting of complex data objects allowing the user to navigate through dense data in an efficient (and fun) way. The only problem is that it is a closed source library and the control implementation doesn’t allow for templates or styling.

Luckily there are a few tricks you can play to make substantial changes to the appearance and behavior of the PivotViewer if you’re willing to work with the codebehind. Xpert360 Ltd. published a blog post titled Adventures with PivotViewer in which they explain how to effectively re-skin the PivotViewer. After rifling through the assemblies using Reflector I found that you can get access to much more of the core functionality by leveraging some of the classes exposed through the library’s internal namespace.

The PivotViewer control that they give you access to through the System.Windows.Pivot namespace is a wrapper around the CollectionViewer MVVM structure exposed in the Microsoft.Pivot.Internal.* namespaces. Almost all of the members that you would wish to be able to modify are exposed by obtaining references to the view, view model, and model. This can be achieved trivially by subclassing PivotViewer and overriding the OnApplyTemplate() function.

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

    Grid partContainer = (Grid)this.GetTemplateChild("PART_Container");

    CollectionViewerView cvv =
        ((CollectionViewerView)(partContainer).Children[0]);
    CollectionViewerViewModel cvvm =
        (CollectionViewerViewModel)ViewBehaviors.GetViewModel(cvv);
    CollectionViewerModel cvm =
        (CollectionViewerModel)ViewBehaviors.GetModel(cvv);
}

Once you obtain these three references, the sky is the limit. One example of what I was able to use these for was changing the default view. The data that I am displaying lends itself almost exclusively to the histogram view, but the default is the grid view. Whenever the sort category was changed automatically by drilling down through a single histogram column the view was reset to grid view. This resulted in an unpleasant user experience because it required mousing all the way across the screen to keep setting the view back to histogram mode.

I was again able to leverage Reflector to find that the views are initialized into a ViewSelectionState object exposed through a property on the model. Each view has an entry in the ViewDataObjects array on the ViewSelectionState, with the grid view data object in the first index and the histogram in the second. By reversing them in the OnApplyTemplate() function I was able to change the default behavior of the PivotViewer.

cvm.ViewSelectionState.ViewDataObjects =
    cvm.ViewSelectionState.ViewDataObjects
        .Reverse()
        .ToArray();

Another useful feature I was able to add was a GridSplitter next to the filter panel. Many of our values are long and were being trimmed by the filter panel, so by adding the GridSplitter I was able to give users access to the complete values as necessary. This improvement leveraged the view reference to modify the visual tree.

FrameworkElement filterPane =
    (FrameworkElement)cvv.FindName("PART_FilterPaneContainer");
Grid filterParent = (Grid)filterPane.Parent;

GridSplitter splitter = new GridSplitter();
splitter.SetValue(Grid.ColumnProperty, 0);
splitter.Style =
    (Style)App.Current.Resources["RightVerticalGridSplitterStyle"];
BindingOperations.SetBinding(splitter, GridSplitter.VisibilityProperty,
    new Binding("Opacity")
    {
        Source = filterPane,
        // Custom implementation converting 0 to false, other to true
        Converter = new VisibilityConverter()
    });
filterParent.Children.Add(splitter);

filterPane.Margin = new Thickness(
    filterPane.Margin.Left,
    filterPane.Margin.Top,
    filterPane.Margin.Right + splitter.Width,
    filterPane.Margin.Bottom);

Please leave a post if you come up with any other useful tweaks!


14 responses to “Customizing the PivotViewer

  • PivotNewbie

    This is awesome. I have implemented it and it works with one exception… I cannot bind the opacity because VisibilityConverter() does not resolve. Can you explain what you are doing here and how to implement this portion?

    My apologies for the newbie question. The fact that you omitted this makes me feel that most developers would know just how to fix this.

    • codethaumaturge

      VisibilityConverter is a custom implementation of the IValueConverter interface which provides Convert(…) and ConvertBack(…) functions so that a binding can map values from a source type or format to a target type or format. In this case I’m converting an integer value to a Visibility enumeration value since our source is the Opacity property on the filter pane. This case doesn’t use the ConvertBack, so you can just stub that out, and an implementation of Convert would look something like this:


      public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
      {
      object result = Visibility.Visible;
      if (value != null && value is int && (int)value == 0)
      result = Visibility.Collapsed;
      return result;
      }

      You can extend this converter to accept all kinds of data types according to how you want to use it. I’ve also found it very handy to pass in a toggle flag as the converter parameter.

      • PivotNewbie

        I had searched on the web and found a different visibility converter which eliminated the errors, but did not actually make the splitter invisible when the filter panel is faded out. I have now replaced it with your code, and it still does not vanish when the panel fades.
        This is quite discouraging because it is so close to being great, but is, at this moment, a total fail.

    • Code Thaumaturge

      I have a new post titled One VisibilityConverter to Rule Them All which might clear this up for you. Try updating your VisibilityConverter with that code and see if it clears up your issue.

  • PivotNewbie

    Also, I get the following error from the filterPane.Margin portion of the code.
    Error HRESULT E_FAIL has been returned from a call to a COM component.

    • codethaumaturge

      That sounds to me like it’s an issue with the binding which gets evaluated when you add the splitter to the visual tree. Do you still get the error if you comment out the line setting the margin?

      • PivotNewbie

        Yes. You are correct. When I comment out that particular piece of code, the error goes away. However, I can see what the code is supposed to do, and it would be nice if it worked. Do you have any ideas as to why it is failing in my case?

    • Code Thaumaturge

      Try replacing splitter.Width with a constant value and see if that gets rid of the error. If it doesn’t, try replacing all of the values with constant values. That will tell you whether it’s reading the splitter property, reading the current frame margin, or assigning the margin that is causing the issue.

      If that’s still giving you trouble try taking out the visibility binding.

      As a side note, here is the style definition for the GridSplitter that I used:


      <Style x:Key="RightVerticalGridSplitterStyle"
      TargetType="sdk:GridSplitter">
      <Setter Property="Width"
      Value="{StaticResource GridSplitterThickness}" />
      <Setter Property="HorizontalAlignment"
      Value="Right" />
      <Setter Property="VerticalAlignment"
      Value="Stretch" />
      </Style>

  • Joris

    To be honest, some things here are quite silly, why not just bind the opacity, rather than the visibility. In any case, your code has the wrong pane, the container doesn’t go invisible, the filterpane does. Also, Opacity is never an int so your converter will never work.

    If you do not want to set the gridsplitter width in the
    style (or use a style at all), just set the width in code (otherwise width will be NaN and you cannot add this in the binding)

    So here’s the working code:

    Grid filterPaneContainer = (Grid)_cvv.FindName("PART_FilterPaneContainer");
    Grid filterParent = (Grid)filterPaneContainer.Parent;

    FilterPaneView filterPane = _cvv.FindName("PART_FilterPane") as FilterPaneView;

    GridSplitter splitter = new GridSplitter();
    splitter.SetValue(Grid.ColumnProperty, 0);
    splitter.Width = 16;
    BindingOperations.SetBinding(splitter, GridSplitter.OpacityProperty, new Binding("Opacity") { Source = filterPane });
    filterParent.Children.Add(splitter);

    filterPaneContainer.Margin = new Thickness(filterPaneContainer.Margin.Left, filterPaneContainer.Margin.Top, filterPaneContainer.Margin.Right + splitter.Width, filterPaneContainer.Margin.Bottom);

    • Code Thaumaturge

      The reason I bind to Visibility instead of Opacity is that you can still interact with a 0 opacity GridSplitter. When no pivot collection is loaded, try hovering over the GridSplitter. You will see the horizontal drag cursor appear, and you can proceed to slide the splitter around. Setting the Visibility to collapsed prevents this behavior.

      Also, when I copy the your code exactly I see the GridSplitter at all times. The container is the one that goes transparent. Without referencing Reflector again, my recollection is that the FilterPane contains all of the meat (Search box and filter controls) and the FilterPaneContainer contains the shadowed border and the FilterPane. The container handles the UI transitions and the pane handles the content.

      • Joris

        I understand the reason for that Visibility binding, makes good sense. An alternative is to capture mousemove on the splitter to make it visible, just like the pane.

        As for the FilterPane, it’s the only one that goes invisible, the container keeps its visibility. (just inspect the visibility property of the container at runtime, it never changes.) Perhaps a version difference? The reason I posted this is because your code simply didn’t work for me.

      • Code Thaumaturge

        I was wondering if there might be a version discrepancy as well. The version I’m using is 1.0.6079.0. It sounds like PivotViewer will be included in the upcoming Silverlight 5 API, so it will be interesting to see if anything changes. It’s one of the hazards of modifying internals of 3rd party objects.

        A couple interesting things also mentioned in that blog post linked above is that we should be able to bind to local data collections and render XAML to the tiles.

  • PivotNewbie

    I think there may indeed be a version difference as Joris’ code worked great for me, whereas the original did not. That would also explain why the suggested fixes also did not work. Thanks, Joris, for providing a working solution.

    I would be very interested in seeing how to make the splitter appear only on mouseover. That seems like it would provide the most polished appearance.

    • PivotNewbie

      By the way, I was able to modify the width of the Info Panel using the following code:
      Grid partContainer = (Grid)this.GetTemplateChild(“PART_Container”);
      CollectionViewerView cvv = ((CollectionViewerView)(partContainer).Children[0]);
      Grid container = cvv.Content as Grid;
      Grid viewerGrid = container.Children[1] as Grid;

      // Adjust the width of the info panel
      InfoPaneView infoPanelView = viewerGrid.Children[3] as InfoPaneView;
      infoPanelView.Width = 210;

      However, I was unable to figure out how to add a splitter to this panel. Any ideas?

Leave a reply to PivotNewbie Cancel reply