Working with PivotViewer and the CxmlCollectionSource

Microsoft’s PivotViewer control is an amazing tool for visualizing data and creating a unique UI for your application.  The one issue everyone seems to have with it, however, is its lack of styling capability.  In fact, working with PivotViewer in Expression Blend yields little in the way of styling or templating.  There are only a few color settings one can make, and some generic overall changes.   Replacing the filter panel or info panel is not currently supported.

What to do?

Since we cannot style the filter panel, we need to hide it and put our own in.  Hiding the panel is relatively simple and straight-forward.  Simply put, you need to subclass the PivotViewer and override the OnApplyTemplates() method, then walk the tree until you find the filter panel.  Fortunately, tools like Snoop or Mole can help you determine where the filter panel resides.  Below is a snippet of code showing where the filter panel resides in the Silverlight 5 version of the PivotViewer.

   1: public override void OnApplyTemplate()
   2: {
   3:     base.OnApplyTemplate();
   4:
   5:     Grid partContainer = (Grid)this.GetTemplateChild("PART_Container");
   6:     CollectionViewerView cvv = ((CollectionViewerView)(partContainer).Children[2]);
   7:     Grid container = cvv.Content as Grid;
   8:     Border viewerBorder = container.Children[1] as Border;
   9:     Grid viewerGrid = viewerBorder.Child as Grid;
  10:
  11:     // Filter Panel
  12:     viewerGrid.Children[2].Visibility = System.Windows.Visibility.Collapsed;
  13: }

NOTE: This is very brittle code and will most likely fail when Microsoft releases a new version of the PivotViewer.

 

Now that the filter panel is gone, we need to create our own.  If you’re manually creating the facets and data in code, then you should have no trouble binding a new Filter control to your data structures.  But what if you’re using the CxmlCollectionSource and loading your data from a cxml external file?

As anyone who’s worked with the CxmlCollectionSource can tell you, the facets and their filter values aren’t readily available for consumption.  In order to build up a collection of facets and filters, you to perform a little preprocessing.  The Items collection off the CxmlCollectionSource is the dataset you’re working with, however, getting values and properties for the items isn’t immediately apparent.

My solution was to create a small class to hold the facet information I required:

   1: public class Facet
   2: {
   3:     public string Category { get; set; }
   4:     public string Value { get; set; }
   5:     public bool IsChecked { get; set; }
   6: }

In order to iterate through the Items collection, you need to wait until the CxmlCollectionSource finishes processing the .cxml document.  Since it occurs asynchronously, you can start your processing in the event handler for the StateChanged event.  That can look something like this (I’m not implementing an MVVM pattern for simplicity’s sake.  This is just part of the code-behind for the MainPage.xaml).

   1: void OnLoaded(object sender, RoutedEventArgs e)
   2: {
   3:     if (_cxml == null)
   4:     {
   5:         _cxml = new CxmlCollectionSource(new Uri(HtmlPage.Document.DocumentUri, "/Data/PivotViewerData.cxml"));
   6:         _cxml.StateChanged += CxmlStateChanged;
   7:     }
   8:
   9:     return;
  10: }
  11:
  12: void CxmlStateChanged(object sender, CxmlCollectionStateChangedEventArgs e)
  13: {
  14:     if (e != null && e.NewState == CxmlCollectionState.Loaded)
  15:     {
  16:         MyPivot.PivotProperties = _cxml.ItemProperties.ToList();
  17:         MyPivot.ItemTemplates = _cxml.ItemTemplates;
  18:         MyPivot.ItemsSource = _cxml.Items;
  19:
  20:         ProcessFacets();
  21:
  22:         this.DataContext = _facetData;
  23:     }
  24:
  25:     _cxml.StateChanged -= CxmlStateChanged;
  26: }

The next step is to iterate over the Items of the CxmlCollectionSource and build a dictionary of Facets and their properties.  The Items collection is a collection of PivotViewerItems that do not contain the actual values of the data elements.  Instead, you need to call a method, GetPropertyValue(id) in order to get the list of properties that particular data elements contains.

The ProcessFacets() method below simply iterates over each item in the data set and determines which properties it contains that are filterable and what the acceptable values are for each.  For example if your data has two filterable properties, such as “Ingredients” and “Serving Size”, these two facets could have a wide range of acceptable values for each data element.  The call to GetPropertyValue for “Ingredients” for an particular item may return “Oil” and “Vinegar”, while another item may return “Milk”, “Oil” and “Peanuts.”  The dictionary built by ProcessFacets will create a Key for “Ingredients” and add a List of four strings representing the four unique values.

I think it may be clearer just to read the code below:

   1: private void ProcessFacets()
   2: {
   3:     // Iterate through all the PivotViewerItems.
   4:     foreach (var item in _cxml.Items)
   5:     {
   6:         // Determine what Propeties it has that are filterable.
   7:         var facets = from f in item.Properties
   8:                      where
   9:                          (PivotViewerPropertyOptions.CanFilter ==
  10:                           (f.Options & PivotViewerPropertyOptions.CanFilter))
  11:                      select f;
  12:
  13:         // For each facet, get its values.
  14:         foreach (var f in facets)
  15:         {
  16:             // create a new facet if it doesn't exist.
  17:             if (!_facetData.Facets.ContainsKey(f.Id))
  18:                 _facetData.Facets.Add(f.Id, new List<Facet>());
  19:
  20:             // get the facet values.
  21:             var props = item.GetPropertyValue(f.Id);
  22:
  23:             // there can be more than one value per facet.
  24:             foreach (var p in props)
  25:             {
  26:                 // don't add the same one more than once.
  27:                 if (!_facetData.Facets[f.Id].Any(o => o.Value.Equals(p)))
  28:                     _facetData.Facets[f.Id].Add(new Facet()
  29:                                                     {
  30:                                                         Category = f.Id,
  31:                                                         Value = p.ToString(),
  32:                                                         IsChecked = false
  33:                                                     });
  34:             }
  35:         }
  36:     }
  37: }

So, there you have it.

Armed with this data, you can now create your own filter panel and programmatically control the PivotViewer independently of the built-in panel.  All you would need to do from this point on is write the Xaml for your Filter view and bind the Facet data to it.  Finally, call SetViewerState on the PivotViewer and pass in the filter string built by your new view.  Note, however, that the filter string is a mini-language of a sorts and will require some processing in code to generate the appropriate parameter.  To get you started, I iterate over the dictionary like so:

   1: string[] filters = _facetData.Values
   2:   .SelectMany(facet => facet.Where(prop => prop.IsChecked))
   3:   .Select(prop => string.Format("{0}=EQ.{1}", prop.Category, prop.Value))
   4:   .ToArray();
   5:
   6: string filterString = string.Join("&", filters.ToArray());
   7:
   8: MyPivotViewer.SetViewerState(filters.Length > 0 ? filterString : "");

The PivotViewer is an extremely powerful tool to have in your arsenal, but it is not very customizable.  With a small hack and some understanding of the data set, some of its limitations can be overcome.   I hope this post gets you on the road to creating your own custom PivotViewer applications.

Stay Informed

Sign up for the latest blogs, events, and insights.

We deliver solutions that accelerate the value of Azure.
Ready to experience the full power of Microsoft Azure?

Atmosera is thrilled to announce that we have been named GitHub AI Partner of the Year.

X