Data-binding PivotItem.Visibility : an helper

Posted by & filed under Dev', Sources.

A common quirk of Windows Phone’s Pivot control API is the lack of support of the Visibility property.
The property is held by the Pivot class, but toggling it to Visible or Collapsed does not change the visibility of the PivotItem. In fact the only way to hide a PivotItem is to remove it from its parent Items collection.

This is unfortunate because it is a common use-case to have a boolean property in your ViewModel that would be perfect to toggle the visibility of the PivotItem rather than writing code-behind or implementing a collection in your viewmodel for this scenario, polluting the viewmodel with unnecessary logic.

Here is a little extension class adding an attached property IsDisplayed to PivotItem that you can bind that will dynamically add/remove the PivotItem from its parent.

Source code :

/* PivotItemEx.cs
* ==============
* Extensions to Microsoft.Phone.Controls.PivotItem
* Pierre BELIN, 2013-14 <pierre@ree7.fr>
*
* Licensed under the MS-PL
*/
using Microsoft.Phone.Controls;
using System.Collections.Generic;
using System.Windows;
namespace ree7.Utils.Controls
{
/// <summary>
/// Extensions to Microsoft.Phone.Controls.PivotItem
/// </summary>
public static class PivotItemEx
{
/// <summary>
/// Holds metadata to properly restore the PivotItems
/// </summary>
private class PivotItemMetadata
{
public Pivot Parent { get; set; }
public int Position { get; set; }
}
private static List<Pivot> managedPivots = new List<Pivot>();
private static Dictionary<PivotItem, PivotItemMetadata> disabledItems = new Dictionary<PivotItem, PivotItemMetadata>();
public static bool GetIsDisplayed(DependencyObject obj)
{
return (bool)obj.GetValue(IsDisplayedProperty);
}
public static void SetIsDisplayed(DependencyObject obj, bool value)
{
obj.SetValue(IsDisplayedProperty, value);
}
// Using a DependencyProperty as the backing store for IsDisplayed. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsDisplayedProperty =
DependencyProperty.RegisterAttached("IsDisplayed", typeof(bool), typeof(PivotItemEx), new PropertyMetadata(true, OnIsDisplayedChanged));
private static void OnIsDisplayedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs a)
{
bool value = (bool)a.NewValue;
// Only act when value turns false by removing the PivotItem from its parent
if (value == false)
{
PivotItem source = obj as PivotItem;
if (obj == null) return;
Pivot pivot = source.Parent as Pivot;
if (pivot == null) return;
// Store the pivot metadata
PivotItemMetadata meta = new PivotItemMetadata();
meta.Parent = pivot;
meta.Position = pivot.Items.IndexOf(source);
disabledItems[source] = meta;
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
pivot.Items.Remove(source);
});
}
else
{
PivotItem source = obj as PivotItem;
if (obj == null) return;
if (disabledItems.ContainsKey(source))
{
PivotItemMetadata meta = disabledItems[source];
Pivot parent = meta.Parent;
disabledItems.Remove(source);
if (CheckPivotStillUsed(parent) == false)
{
UnsubscribePivot(parent);
}
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
if (meta.Position > parent.Items.Count)
{
parent.Items.Add(source);
}
else
{
parent.Items.Insert(meta.Position, source);
}
});
}
}
}
private static void OnManagedPivotUnloaded(object sender, RoutedEventArgs e)
{
UnsubscribePivot((Pivot)sender);
}
private static bool CheckPivotStillUsed(Pivot p)
{
foreach (var meta in disabledItems.Values)
{
if (meta.Parent == p) return true;
}
return false;
}
private static void SubscribePivot(Pivot p)
{
if (managedPivots.Contains(p) == false)
{
managedPivots.Add(p);
p.Unloaded += OnManagedPivotUnloaded;
}
}
private static void UnsubscribePivot(Pivot p)
{
if (managedPivots.Contains(p))
{
managedPivots.Remove(p);
p.Unloaded -= OnManagedPivotUnloaded;
List<PivotItem> keysToRemove = new List<PivotItem>();
foreach (var pair in disabledItems)
{
if (pair.Value.Parent == p) keysToRemove.Add(pair.Key);
}
foreach (var key in keysToRemove) disabledItems.Remove(key);
}
}
}
}

How to use it :

Declare a boolean property in your ViewModel that triggers the NotifyPropertyChanged event on change (here with MVVM Light) :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
#region public bool HasRelatedVideos
private bool _HasRelatedVideos;
public bool HasRelatedVideos
{
  get
  {
    return _HasRelatedVideos;
  }
  set
  {
    _HasRelatedVideos = value;
    RaisePropertyChanged(() => HasRelatedVideos);
  }
}
#endregion

In your XAML file, declare a reference to the namespace of the PivotItemEx :

1
xmlns:uc="clr-namespace:ree7.Utils.Controls"

And finally declare and bind the attached property in the PivotItem :

1
2
<controls:PivotItem Header="{Binding l.star_related_videos, Source={StaticResource LocaleHelper}}"
            uc:PivotItemEx.IsDisplayed="{Binding HasRelatedVideos}">

One Response to “Data-binding PivotItem.Visibility : an helper”

  1. Przemek

    Hi Pierre,
    Thanks for publish this code. It is very useful to me.
    I used your code in WP8 project and I have some problems with memory leaks. When I add your extension to view then GC are not collect this view.
    I try to find what is wrong. I see that method SubscribePivot isn’t used at all. Is this correct ?

    Thanks in advance for your explanation.
    Regards
    Przemek