Click here to Skip to main content
Click here to Skip to main content

WPFSpark: 5 of n: FluidPivotPanel

, 19 Jan 2012
Rate this:
Please Sign up or sign in to vote.
A Windows Phone PivotControl in WPF.

wpfspark_new.png

Introduction

This is the fifth article in the WPFSpark series. Till now I have covered four controls in WPFSpark - SprocketControl, ToggleSwitch, FluidWrapPanel, and SparkWindow.

The previous articles in the WPFSpark series can be accessed here:

  1. WPFSpark: 1 of n: SprocketControl
  2. WPFSpark: 2 of n: ToggleSwitch
  3. WPFSpark: 3 of n: FluidWrapPanel
  4. WPFSpark: 4 of n: SparkWindow

In this article, I describe in detail the fifth control in this library - the FluidPivotPanel control.

Inspiration

FluidPivotPanel is inspired from the PivotControl of Windows Phone 7+. Metro UI continues to fascinate me. It is so smooth and sleek. FluidPivotPanel is my attempt at making a similar control for WPF.

FluidPivotPanel Demystified

The Basic Layout

The basic layout of the FluidPivotPanel comprises of a Panel which hosts all the Pivots. Each Pivot in turn comprises of a Header and a Content. The Pivot, whose header is active (i.e., currently selected), has its content displayed. The selected header will be the left most header, while the other headers will be arranged in a cyclic manner.

The Components

  • IPivotHeader
  • This interface must be implemented by a control so that it can be used as a Header Item in the FluidPivotPanel. It defines a method SetActive (to activate/deactivate the Header Item) and an event HeaderSelected (which is fired when the Header is selected).

    This provides freedom to the control to choose which of the user inputs would raise the HeaderSelected events - whether by left mouse button, right mouse button, or interaction by stylus or even through touch.

    /// <summary>
    /// Interface for the PivotHeader
    /// </summary>
    public interface IPivotHeader
    {
        /// <summary>
        /// Activates/Deactivates the Pivot Header based on the 'isActive' flag.
        /// </summary>
        /// <param name="isActive">Flag to indicate whether
        ///        the Pivot Header should be Activated or Deactivated.</param>
        void SetActive(bool isActive);
        /// <summary>
        /// Event fired when the header is selected by the user
        /// </summary>
        event EventHandler HeaderSelected;
    }
  • IPivotContent
  • This interface must be implemented by a control so that it can be used as a Content Item in the FluidPivotPanel. It defines a method SetActive (to activate/deactivate the Content Item).

    /// <summary>
    /// Interface for the PivotContent
    /// </summary>
    public interface IPivotContent
    {
        /// <summary>
        /// Activates/Deactivates the Pivot Content based on the 'isActive' flag.
        /// </summary>
        /// <param name="isActive">Flag to indicate whether
        /// the Pivot Content should be Activated or Deactivated.</param>
        void SetActive(bool isActive);
    }
  • PivotHeaderControl
  • PivotHeaderControl derives from ContentControl, implements the IPivotHeader interface, and represents the Header Item as a text.

    /// <summary>
    /// Class which implements the IPivotHeader interface
    /// and represents the header item in text form.
    /// </summary>
    public class PivotHeaderControl : ContentControl, IPivotHeader
    {
        #region Dependency Properties
    
        ...
    
        #endregion
    
        #region Construction / Initialization
    
        /// <summary>
        /// Ctor
        /// </summary>
        public PivotHeaderControl()
        {
            // By default, the header will be inactive
            IsActive = false;
            this.Foreground = InactiveForeground;
            // This control will raise the HeaderSelected event on Mouse Left Button down
            this.MouseLeftButtonDown +=new MouseButtonEventHandler(OnMouseDown);
        }
    
        #endregion
    
        #region IPivotHeader Members
    
        /// <summary>
        /// Activates/Deactivates the Pivot Header based on the 'isActive' flag.
        /// </summary>
        /// <param name="isActive">Flag to indicate whether the Pivot
        ///   Header and Pivot Content should be Activated or Deactivated</param>
        public void SetActive(bool isActive)
        {
            IsActive = isActive;
        }
    
        public event EventHandler HeaderSelected;
    
        #endregion
    
        #region EventHandlers
    
        /// <summary>
        /// Handler for the mouse down event
        /// </summary>
        /// <param name="sender">Sender</param>
        /// <param name="e">Event Args</param>
        void OnMouseDown(object sender, MouseButtonEventArgs e)
        {
            if (HeaderSelected != null)
            {
                HeaderSelected(this, new EventArgs());
            }
        }
    
        #endregion
    }

    PivotHeaderControl Properties

    Dependency PropertyTypeDescriptionDefault Value
    ActiveForegroundBrushGets or sets the Foreground of the PivotHeaderControl when it is active. Brushes.Black
    InactiveForegroundBrushGets or sets the Foreground of the PivotHeaderControl when it is inactive. Brushes.DarkGray
    IsActiveBooleanGets or sets whether the PivotHeaderControl is currently active/inactive.False
  • PivotContentControl
  • PivotContentControl derives from ContentControl, implements the IPivotContent interface, and represents the Content of the PivotItem.

    /// <summary>
    /// Implementation of the IPivotContent interface
    /// </summary>
    public class PivotContentControl : ContentControl, IPivotContent
    {
        #region Fields
    
        Storyboard fadeInSB;
    
        #endregion
    
        #region DependencyProperties
    
        ...
    
        #endregion
    
        #region IPivotContent Members
    
        public void SetActive(bool isActive)
        {
            if (isActive)
            {
                this.Visibility = Visibility.Visible;
                if (AnimateContent)
                    fadeInSB.Begin();
            }
            else
            {
                this.Visibility = Visibility.Collapsed;
            }
        }
    
        #endregion
    }

    PivotContentControl Properties

    Dependency PropertyTypeDescriptionDefault Value
    AnimateContentBooleanGets or sets whether the PivotContentControl should be animated when it becomes active. True
  • PivotItem
  • PivotItem is a ContentControl which encapsulates the Pivot Header and Pivot Content to represent a single entity. Whenever the Pivot Header for a PivotItem is active, the corresponding Pivot Content also becomes active.

    /// <summary>
    /// Class which encapsulates the header and content
    /// for each Pivot item.
    /// </summary>
    public class PivotItem : ContentControl
    {
        #region Fields
    
        PivotPanel parent = null;
    
        #endregion
    
        #region Dependency Properties
    
        ...
    
        #endregion
    
        #region APIs
    
        /// <summary>
        /// Sets the parent PivotPanel of the Pivot Item
        /// </summary>
        /// <param name="panel">PivotPanel</param>
        public void SetParent(PivotPanel panel)
        {
            parent = panel;
        }
    
        /// <summary>
        /// Activates/Deactivates the Pivot Header and Pivot Content
        /// based on the 'isActive' flag.
        /// </summary>
        /// <param name="isActive">Flag to indicate whether the Pivot Header
        ///            and Pivot Content should be Activated or Decativated</param>
        public void SetActive(bool isActive)
        {
            if (PivotHeader != null)
            {
                IPivotHeader header = PivotHeader as IPivotHeader;
                if (header != null)
                    header.SetActive(isActive);
            }
    
            if (PivotContent != null)
            {
                IPivotContent content = PivotContent as IPivotContent;
                if (content != null)
                    content.SetActive(isActive);
                else
                    PivotContent.Visibility = isActive ? 
                      Visibility.Visible : Visibility.Collapsed;
            }
        }
    
        /// <summary>
        /// Initializes the PivotItem
        /// </summary>
        public void Initialize()
        {
            // Set the header as inactive
            if (PivotHeader != null)
            {
                IPivotHeader header = PivotHeader as IPivotHeader;
                if (header != null)
                    header.SetActive(false);
            }
    
            // Make the PivotContent invisible
            if (PivotContent != null)
            {
                ((FrameworkElement)PivotContent).Visibility = Visibility.Collapsed;
            }
        }
    
        #endregion
    }

    PivotItem Properties

    Dependency PropertyTypeDescriptionDefault Value
    PivotHeaderFrameworkElementGets or sets the Pivot Header Item for the PivotItem.null
    PivotContentFrameworkElementGets or sets the Pivot Content Item for the PivotItem.null
  • PivotHeaderPanel
  • The PivotHeaderPanel is responsible for hosting the PivotHeader items. It derives from Canvas. Whenever a PivotHeader is selected by the user, the PivotHeaderPanel moves the PivotHeader to the left most positions and moves the PivotHeader items before the selected PivotHeader item to the end of the list, thus maintaining a cyclic order of the PivotHeader items.

    /// <summary>
    /// Panel which contains all the headers
    /// </summary>
    public class PivotHeaderPanel : Canvas
    {
        #region Constants
    
        private const int ADD_FADE_IN_DURATION = 250;
        private const int UPDATE_FADE_IN_DURATION = 50;
        private const int TRANSITION_DURATION = 300;
    
        #endregion
    
        #region Events
    
        public event EventHandler HeaderSelected;
    
        #endregion
    
        #region Fields
    
        Storyboard addFadeInSB;
        Storyboard updateFadeInSB;
        List<UIElement> headerCollection = null;
        Queue<Object[]> animationQueue = null;
        bool isAnimationInProgress = false;
        object syncObject = new object();
        CubicEase easingFn = null;
    
        #endregion
    
        #region Construction / Initialization
    
        /// <summary>
        /// Ctor
        /// </summary>
        public PivotHeaderPanel()
        {
            // Define the storyboards
            DoubleAnimation addFadeInAnim = new DoubleAnimation(0.0, 1.0, 
              new Duration(TimeSpan.FromMilliseconds(ADD_FADE_IN_DURATION)));
            Storyboard.SetTargetProperty(addFadeInAnim, 
              new PropertyPath(UIElement.OpacityProperty));
            addFadeInSB = new Storyboard();
            addFadeInSB.Children.Add(addFadeInAnim);
    
            DoubleAnimation updateFadeInAnim = new DoubleAnimation(0.0, 1.0, 
              new Duration(TimeSpan.FromMilliseconds(UPDATE_FADE_IN_DURATION)));
            Storyboard.SetTargetProperty(updateFadeInAnim, 
              new PropertyPath(UIElement.OpacityProperty));
            updateFadeInSB = new Storyboard();
            updateFadeInSB.Children.Add(updateFadeInAnim);
    
            updateFadeInSB.Completed += new EventHandler(OnAnimationCompleted);
    
            headerCollection = new List<UIElement>();
    
            easingFn = new CubicEase();
            easingFn.EasingMode = EasingMode.EaseOut;
        }
    
        #endregion
    
        #region APIs
    
        /// <summary>
        /// Adds a child to the HeaderPanel
        /// </summary>
        /// <param name="child">Child to be added</param>
        public void AddChild(UIElement child)
        {
            if (child == null)
                return;
    
            lock (syncObject)
            {
                Dispatcher.BeginInvoke(new Action(() =>
                {
                    child.Opacity = 0;
                    // Get the Desired size of the child
                    child.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
    
                    // Check if the child needs to be added at the end or inserted in between
                    if ((Children.Count == 0) || 
                        (Children[Children.Count - 1] == headerCollection.Last()))
                    {
                        child.RenderTransform = CreateTransform(child);
                        Children.Add(child);
                        headerCollection.Add(child);
    
                        addFadeInSB.Begin((FrameworkElement)child);
                    }
                    else
                    {
                        var lastChild = Children[Children.Count - 1];
                        Children.Add(child);
                        int index = headerCollection.IndexOf(lastChild) + 1;
                        // Insert the new child after the last child in the header collection
                        if (index >= 1)
                        {
                            double newLocationX = ((TranslateTransform)(
                              ((TransformGroup)headerCollection[index].RenderTransform).Children[0])).X;
                            headerCollection.Insert(index, child);
                            child.RenderTransform = CreateTransform(new Point(newLocationX, 0.0));
    
                            InsertChild(child, index + 1);
                        }
                    }
    
                    // Subscribe to the HeaderSelected event and set Active property to false
                    IPivotHeader headerItem = child as IPivotHeader;
    
                    if (headerItem != null)
                    {
                        headerItem.HeaderSelected += new EventHandler(OnHeaderSelected);
                    }
                }));
            }
        }
    
        /// <summary>
        /// Checks if the given UIElement is already added to the Children collection.
        /// </summary>
        /// <param name="child">UIElement</param>
        /// <returns>true/false</returns>
        public bool Contains(UIElement child)
        {
            return Children.Contains(child);
        }
    
        /// <summary>
        /// Cycles 'count' elements to the left
        /// </summary>
        /// <param name="count">Number of elements to move</param>
        public void MoveForward(int count)
        {
            if ((isAnimationInProgress) || (count <= 0) || 
                          (count >= headerCollection.Count))
                return;
            else
                isAnimationInProgress = true;
    
            Dispatcher.BeginInvoke(new Action(() =>
            {
                // Create the animation queue so that the items removed from the beginning 
                // are added in the end in a sequential manner.
                animationQueue = new Queue<Object[]>();
    
                lock (animationQueue)
                {
                    for (int i = 0; i < count; i++)
                    {
                        animationQueue.Enqueue(new object[] { headerCollection[i], true });
                    }
                }
    
                // Get the total width of the first "count" children
                double distanceToMove = ((TranslateTransform)((
                   (TransformGroup)headerCollection[count].RenderTransform).Children[0])).X;
    
                // Calculate the new location of each child and create appropriate transition
                foreach (UIElement child in headerCollection)
                {
                    double oldTranslationX = ((TranslateTransform)(
                      ((TransformGroup)child.RenderTransform).Children[0])).X;
                    double newTranslationX = oldTranslationX - distanceToMove;
                    Storyboard transition = CreateTransition(child, 
                      new Point(newTranslationX, 0.0), 
                      TimeSpan.FromMilliseconds(TRANSITION_DURATION), easingFn);
                    // Process the animation queue once the last child's transition is completed
                    if (child == headerCollection.Last())
                    {
                        transition.Completed += (s, e) =>
                        {
                            ProcessAnimationQueue();
                        };
                    }
                    transition.Begin();
                }
            }));
        }
    
        /// <summary>
        /// Cycles 'count' elements to the right
        /// </summary>
        /// <param name="count">Number of elements to move</param>
        public void MoveBack(int count)
        {
            if (isAnimationInProgress)
                return;
            else
                isAnimationInProgress = true;
    
            Dispatcher.BeginInvoke(new Action(() =>
            {
                if ((count <= 0) || (count >= headerCollection.Count))
                    return;
    
                // Create the animation queue so that the items removed from the end 
                // are added at the beginning in a sequential manner.
                animationQueue = new Queue<Object[]>();
    
                lock (animationQueue)
                {
                    for (int i = headerCollection.Count - 1; i >= headerCollection.Count - count; i--)
                    {
                        animationQueue.Enqueue(new object[] { headerCollection[i], false });
                    }
                }
    
                // Get the total width of the last "count" number of children
                double distanceToMove = 
                  ((TranslateTransform)(((TransformGroup)
                    headerCollection[headerCollection.Count - 1].RenderTransform).Children[0])).X -
                    ((TranslateTransform)(((TransformGroup)
                    headerCollection[headerCollection.Count - count].RenderTransform).Children[0])).X +
                    headerCollection[headerCollection.Count - 1].DesiredSize.Width;
    
                // Calculate the new location of each child and create appropriate transition
                foreach (UIElement child in headerCollection)
                {
                    double oldTranslationX = ((TranslateTransform)(
                      ((TransformGroup)child.RenderTransform).Children[0])).X;
                    double newTranslationX = oldTranslationX + distanceToMove;
                    Storyboard transition = CreateTransition(child, 
                      new Point(newTranslationX, 0.0), 
                      TimeSpan.FromMilliseconds(TRANSITION_DURATION), null);
                    // Process the animation queue once the last
                    // child's transition is completed
                    if (child == headerCollection.Last())
                    {
                        transition.Completed += (s, e) =>
                        {
                            ProcessAnimationQueue();
                        };
                    }
                    transition.Begin();
                }
            }));
        }
    
        /// <summary>
        /// Removes all the children from the header
        /// </summary>
        public void ClearHeader()
        {
            foreach (UIElement item in headerCollection)
            {
                IPivotHeader headerItem = item as IPivotHeader;
    
                if (headerItem != null)
                {
                    // Unsubscribe
                    headerItem.HeaderSelected -= OnHeaderSelected;
                }
            }
    
            headerCollection.Clear();
            Children.Clear();
        }
    
        /// <summary>
        /// Resets the location of the header items so that the 
        /// first child that was added is moved to the beginning.
        /// </summary>
        internal void Reset()
        {
            if (Children.Count > 0)
            {
                OnHeaderSelected(Children[0], null);
            }
        }
    
        #endregion
    
        #region Helpers
    
        /// <summary>
        /// Inserts the child at the specified index.
        /// </summary>
        /// <param name="child">Child to be inserted</param>
        /// <param name="index">Index at where
        ///       insertion should be performed</param>
        private void InsertChild(UIElement child, int index)
        {
            // Move all the children after the 'index' to the right to accommodate the new child
            for (int i = index; i < headerCollection.Count; i++)
            {
                double oldTranslationX = ((TranslateTransform)(
                  ((TransformGroup)headerCollection[i].RenderTransform).Children[0])).X;
                double newTranslationX = oldTranslationX + child.DesiredSize.Width;
                headerCollection[i].RenderTransform = CreateTransform(new Point(newTranslationX, 0.0));
            }
    
            addFadeInSB.Begin((FrameworkElement)child);
        }
    
        /// <summary>
        /// Appends the child at the beginning or the end based on the isDirectionForward flag
        /// </summary>
        /// <param name="child">Child to be appended</param>
        /// <param name="isDirectionForward">Flag to indicate
        ///    whether the items has to be added at the end or at the beginning</param>
        private void AppendChild(UIElement child, bool isDirectionForward)
        {
            Dispatcher.BeginInvoke(new Action(() =>
            {
                child.Opacity = 0;
                child.RenderTransform = CreateTransform(child, isDirectionForward);
                headerCollection.Remove(child);
                if (isDirectionForward)
                    headerCollection.Add(child);
                else
                    headerCollection.Insert(0, child);
    
                updateFadeInSB.Begin((FrameworkElement)child);
            }));
        }
    
        /// <summary>
        /// Handles the completed event of each animation in the Animation Queue
        /// </summary>
        /// <param name="sender">Sender</param>
        /// <param name="e">EventArgs</param>
        private void OnAnimationCompleted(object sender, EventArgs e)
        {
            lock (animationQueue)
            {
                if (animationQueue.Count > 0)
                    animationQueue.Dequeue();
            }
    
            ProcessAnimationQueue();
        }
    
        /// <summary>
        /// Process the animation for the next element in the Animation Queue
        /// </summary>
        private void ProcessAnimationQueue()
        {
            lock (animationQueue)
            {
                if (animationQueue.Count > 0)
                {
                    Object[] next = animationQueue.Peek();
                    UIElement child = (UIElement)next[0];
                    bool isDirectionForward = (bool)next[1];
                    AppendChild(child, isDirectionForward);
                }
                else
                {
                    isAnimationInProgress = false;
                }
            }
        }
    
        /// <summary>
        /// Gets the position available before the first child
        /// </summary>
        /// <returns>Distance on the X-axis</returns>
        private double GetFirstChildPosition()
        {
            double transX = 0.0;
    
            // Get the first child in the headerCollection
            UIElement firstChild = headerCollection.FirstOrDefault();
            if (firstChild != null)
                transX = ((TranslateTransform)(((TransformGroup)
                  firstChild.RenderTransform).Children[0])).X;
    
            return transX;
        }
    
        /// <summary>
        /// Gets the position available after the last child
        /// </summary>
        /// <returns>Distance on the X-axis</returns>
        private double GetNextAvailablePosition()
        {
            double transX = 0.0;
            // Get the last child in the headerCollection 
            UIElement lastChild = headerCollection.LastOrDefault();
            // Add the X-Location of the child +
            //      its Desired width to get the next child's position
            if (lastChild != null)
                transX = ((TranslateTransform)(((TransformGroup)
                  lastChild.RenderTransform).Children[0])).X + lastChild.DesiredSize.Width;
    
            return transX;
        }
    
        /// <summary>
        /// Creates a translation transform for the child so that it can be placed
        /// at the beginning or the end.
        /// </summary>
        /// <param name="child">Item to be translated</param>
        /// <param name="isDirectionForward">Flag to indicate
        ///    whether the items has to be added at the end or at the beginning</param>
        /// <returns>TransformGroup</returns>
        private TransformGroup CreateTransform(UIElement child, bool isDirectionForward = true)
        {
            if (child == null)
                return null;
    
            double transX = 0.0;
            if (isDirectionForward)
                transX = GetNextAvailablePosition();
            else
                // All the children have moved forward to make space for the children to be
                // added in the beginning of the header collection. So calculate the 
                // child's location by subtracting its width from the first child's location
                transX = GetFirstChildPosition() - child.DesiredSize.Width;
    
            TranslateTransform translation = new TranslateTransform();
            translation.X = transX;
            translation.Y = 0.0;
    
            TransformGroup transform = new TransformGroup();
            transform.Children.Add(translation);
    
            return transform;
        }
    
        /// <summary>
        /// Creates a translation transform
        /// </summary>
        /// <param name="translation">Translation value</param>
        /// <returns>TransformGroup</returns>
        private TransformGroup CreateTransform(Point translation)
        {
            TranslateTransform translateTransform = new TranslateTransform();
            translateTransform.X = translation.X;
            translateTransform.Y = translation.Y;
    
            TransformGroup transform = new TransformGroup();
            transform.Children.Add(translateTransform);
    
            return transform;
        }
    
        /// <summary>
        /// Creates the animation for translating the element
        /// to a new location
        /// </summary>
        /// <param name="element">Item to be translated</param>
        /// <param name="translation">Translation value</param>
        /// <param name="period">Translation duration</param>
        /// <param name="easing">Easing function</param>
        /// <returns>Storyboard</returns>
        private Storyboard CreateTransition(UIElement element, 
          Point translation, TimeSpan period, EasingFunctionBase easing)
        {
            Duration duration = new Duration(period);
    
            // Animate X
            DoubleAnimation translateAnimationX = new DoubleAnimation();
            translateAnimationX.To = translation.X;
            translateAnimationX.Duration = duration;
            if (easing != null)
                translateAnimationX.EasingFunction = easing;
    
            Storyboard.SetTarget(translateAnimationX, element);
            Storyboard.SetTargetProperty(translateAnimationX,
                new PropertyPath("(UIElement.RenderTransform).(
                    TransformGroup.Children)[0].(TranslateTransform.X)"));
    
            Storyboard sb = new Storyboard();
            sb.Children.Add(translateAnimationX);
    
            return sb;
        }
    
        #endregion
    
        #region EventHandlers
    
        /// <summary>
        /// Handles the HeaderSelected event
        /// </summary>
        /// <param name="sender">Sender</param>
        /// <param name="e">EventArgs</param>
        void OnHeaderSelected(object sender, EventArgs e)
        {
            if ((isAnimationInProgress) || (headerCollection == null) || 
                          (headerCollection.Count == 0))
                return;
    
            UIElement child = sender as UIElement;
            if (child != null)
            {
                // Check if the header selected is not the first header
                int index = headerCollection.IndexOf(child);
                if (index > 0)
                {
                    // Move the selected header to the left most position
                    MoveForward(index);
                    // Raise the HeaderSelected event
                    if (HeaderSelected != null)
                        HeaderSelected(child, new EventArgs());
                }
            }
        }
    
        #endregion        
    }
  • PivotPanel
  • PivotPanel is the outermost panel of the FluidPivotPanel which hosts the PivotHeaderPanel and the PivotContent items. It derives from Canvas and contains a Grid which has two rows. The top row has a height defined by the dependency property HeaderHeight and hosts the PivotHeaderPanel. The bottom row takes up the remaining space and hosts the PivotContent items.

    PivotPanel also implements the INotifiableParent interface in order to be notified when children are added to it via XAML. In order to understand the role of the INotifiableParent interface in detail, check out my blog article Get notified when a child is added to a custom panel via XAML.

    /// <summary>
    /// The main Panel which contains the Pivot Items
    /// </summary>
    [ContentProperty("NotifiableChildren")]
    public class PivotPanel : Canvas, INotifiableParent
    {
        #region Fields
    
        private Grid rootGrid;
        private PivotHeaderPanel headerPanel;
        private List<PivotItem> pivotItems = null;
        private PivotItem currPivotItem = null;
        NotifiableUIElementCollection notifiableChildren = null;
    
        #endregion
    
        #region Dependency Properties
    
        ...
    
        #endregion
    
        #region Properties
    
        /// <summary>
        /// Property used to set the Content Property for the FluidWrapPanel
        /// </summary>
        public NotifiableUIElementCollection NotifiableChildren
        {
            get
            {
                return notifiableChildren;
            }
        }
    
        #endregion
    
        #region Construction / Initialization
    
        public PivotPanel()
        {
            notifiableChildren = new NotifiableUIElementCollection(this, this);
    
            // Create the root grid that will hold the header panel and the contents
            rootGrid = new Grid();
    
            RowDefinition rd = new RowDefinition();
            rd.Height = HeaderHeight;
            rootGrid.RowDefinitions.Add(rd);
    
            rd = new RowDefinition();
            rd.Height = new GridLength(1, GridUnitType.Star);
            rootGrid.RowDefinitions.Add(rd);
    
            Binding backgroundBinding = new Binding();
            backgroundBinding.Source = this.Background;
            rootGrid.SetBinding(Grid.BackgroundProperty, backgroundBinding);
    
            rootGrid.Width = this.ActualWidth;
            rootGrid.Height = this.ActualHeight;
    
            rootGrid.HorizontalAlignment = HorizontalAlignment.Stretch;
            rootGrid.VerticalAlignment = VerticalAlignment.Stretch;
    
            // Create the header panel
            headerPanel = new PivotHeaderPanel();
            headerPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
            headerPanel.VerticalAlignment = VerticalAlignment.Stretch;
            headerPanel.HeaderSelected += new EventHandler(OnHeaderSelected);
            rootGrid.Children.Add(headerPanel);
    
            this.Children.Add(rootGrid);
    
            pivotItems = new List<PivotItem>();
    
            this.SizeChanged += (s, e) =>
                {
                    if (rootGrid != null)
                    {
                        rootGrid.Width = this.ActualWidth;
                        rootGrid.Height = this.ActualHeight;
                    }
                };
        }
    
        #endregion
    
        #region APIs
    
        /// <summary>
        /// Adds a PivotItem to the PivotPanel's Children collection
        /// </summary>
        /// <param name="item">PivotItem</param>
        public int AddChild(PivotItem item)
        {
            if (pivotItems == null)
                pivotItems = new List<PivotItem>();
    
            pivotItems.Add(item);
    
            item.SetParent(this);
    
            if (item.PivotHeader != null)
                headerPanel.AddChild(item.PivotHeader as UIElement);
    
            if (item.PivotContent != null)
            {
                Grid.SetRow(item.PivotContent as UIElement, 1);
                // Set the item to its initial state
                item.Initialize();
                rootGrid.Children.Add(item.PivotContent as UIElement);
            }
    
            return pivotItems.Count - 1;
        }
    
        /// <summary>
        /// Adds the newly assigned PivotHeader of the PivotItem to the PivotPanel
        /// </summary>
        /// <param name="item">PivotItem</param>
        internal void UpdatePivotItemHeader(PivotItem item)
        {
            if ((pivotItems.Contains(item)) && (item.PivotHeader != null) 
                 && (!headerPanel.Contains((UIElement)item.PivotHeader)))
            {
                headerPanel.AddChild(item.PivotHeader as UIElement);
                // Activate the First Pivot Item.
                ActivateFirstPivotItem();
            }
        }
    
        /// <summary>
        /// Adds the newly assigned PivotContent of the PivotItem to the PivotPanel
        /// </summary>
        /// <param name="item">PivotItem</param>
        internal void UpdatePivotItemContent(PivotItem item)
        {
            if ((pivotItems.Contains(item)) && (item.PivotContent != null) 
                   && (!rootGrid.Children.Contains((UIElement)item.PivotContent)))
            {
                Grid.SetRow(item.PivotContent as UIElement, 1);
                rootGrid.Children.Add(item.PivotContent as UIElement);
                // Activate the First Pivot Item.
                ActivateFirstPivotItem();
            }
        }
    
        /// <summary>
        /// Adds a list of PivotItems to the PivotPanel's Children collection
        /// </summary>
        /// <param name="items">List of PivotItems</param>
        public void AddItems(List<PivotItem> items)
        {
            if (items == null)
                return;
    
            foreach (PivotItem item in items)
            {
                AddChild(item);
            }
    
            ActivateFirstPivotItem();
        }
    
        /// <summary>
        /// Sets the DataContext for the PivotContent of each of the PivotItems.
        /// </summary>
        /// <param name="context">Data Context</param>
        public void SetDataContext(object context)
        {
            if ((pivotItems == null) || (pivotItems.Count == 0))
                return;
    
            foreach (PivotItem item in pivotItems)
            {
                item.PivotContent.DataContext = context;
            }
        }
    
        /// <summary>
        /// Resets the location of the header items so that the 
        /// first child that was added is moved to the beginning.
        /// </summary>
        public void Reset()
        {
            if (headerPanel != null)
                headerPanel.Reset();
        }
    
        #endregion
    
        #region Event Handlers
    
        /// <summary>
        /// Handles the event raised when a header item is selected
        /// </summary>
        /// <param name="sender">Header item</param>
        /// <param name="e">Event Args</param>
        void OnHeaderSelected(object sender, EventArgs e)
        {
            FrameworkElement headerItem = sender as FrameworkElement;
            if (headerItem == null)
                return;
    
            // Find the PivotItem whose header was selected
            PivotItem pItem = pivotItems.Where(p => 
              p.PivotHeader == headerItem).FirstOrDefault();
    
            if ((pItem != null) && (pItem != currPivotItem))
            {
                if (currPivotItem != null)
                {
                    currPivotItem.SetActive(false);
                }
    
                pItem.SetActive(true);
                currPivotItem = pItem;
            }
        }
    
        #endregion
    
        #region Helpers
    
        /// <summary>
        /// Sets the First Pivot item as active
        /// </summary>
        private void ActivateFirstPivotItem()
        {
            // Set the first item as active
            if ((pivotItems != null) && (pivotItems.Count > 0))
            {
                pivotItems.First().SetActive(true);
                currPivotItem = pivotItems.First();
            }
        }
    
        /// <summary>
        /// Removes all the Pivot Items from the Children collection
        /// </summary>
        void ClearItemsSource()
        {
            if ((pivotItems == null) || (pivotItems.Count == 0))
                return;
    
            if (headerPanel != null)
                headerPanel.ClearHeader();
    
            if (rootGrid != null)
            {
                foreach (PivotItem item in pivotItems)
                {
                    rootGrid.Children.Remove(item.PivotContent);
                }
            }
    
            pivotItems.Clear();
        }
    
        #endregion
    
        #region INotifiableParent Members
    
        /// <summary>
        /// Adds the child to the Panel through XAML
        /// </summary>
        /// <param name="child">Child to be added</param>
        /// <returns>Index of the child in the collection</returns>
        public int AddChild(UIElement child)
        {
            PivotItem pItem = child as PivotItem;
            if (pItem != null)
            {
                return AddChild(pItem);
            }
    
            return -1;
        }
    
        #endregion
    }

    PivotPanel Properties

    Dependency PropertyTypeDescriptionDefault Value
    ContentBackgroundBrushGets or sets the Background of the area of PivotPanel which hosts the PivotContent items.null
    HeaderBackgroundBrushGets or sets the Background of the area of PivotPanel which hosts the PivotHeaderPanel.null
    HeaderHeightGridLengthGets or sets the Height of the first row of the grid in the PivotPanel which hosts the PivotHeaderPanel.0.1*
    ItemsSourceObservableCollection<PivotItem>Bindable property for specifying an ObservableCollection of PivotItems.null

FluidPivotPanel - Relating the Components

Here is the rough class diagram which explains the relationship between the above components.

Using FluidPivotPanel in your Application Through XAML

Through XAML, you first need to add PivotPanel to your layout. Within the PivotPanel, you can declare the PivotItems. Within each PivotItem, you have to set the PivotHeader and PivotContent properties. If you are adding a PivotContentControl as the PivotContent of the PivotItem and you do not want it to be animated when it is active, you can set the AnimateContent property of PivotContentControl to false.

xmlns:wpfspark="clr-namespace:WPFSpark;assembly=WPFSpark"

...

<wpfspark:PivotPanel x:Name="RootPanel"
                     HeaderHeight="70"
                     HorizontalAlignment="Stretch">
    <wpfspark:PivotItem>
        <wpfspark:PivotItem.PivotHeader>
            <wpfspark:PivotHeaderControl FontFamily="Segoe WP"
                 FontSize="28"
                 ActiveForeground="White"
                 InactiveForeground="#444444">Item One</wpfspark:PivotHeaderControl>
        </wpfspark:PivotItem.PivotHeader>
        <wpfspark:PivotItem.PivotContent>
            <wpfspark:PivotContentControl>
                <Border Background="LightBlue"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch">
                    <TextBlock FontFamily="Segoe WP"
                               FontSize="48"
                               Foreground="White"
                               Text="Item One Content"></TextBlock>
                </Border>
            </wpfspark:PivotContentControl>
        </wpfspark:PivotItem.PivotContent>
    </wpfspark:PivotItem>
    <wpfspark:PivotItem>
        <wpfspark:PivotItem.PivotHeader>
            <wpfspark:PivotHeaderControl FontFamily="Segoe WP"
                     FontSize="28"
                     ActiveForeground="White"
                     InactiveForeground="#444444">Item Two</wpfspark:PivotHeaderControl>
        </wpfspark:PivotItem.PivotHeader>
        <wpfspark:PivotItem.PivotContent>
            <wpfspark:PivotContentControl>
                <Border Background="LightGoldenrodYellow"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch">
                    <TextBlock FontFamily="Segoe WP"
                               FontSize="48"
                               Foreground="Black"
                               Text="Item Two Content"></TextBlock>
                </Border>
            </wpfspark:PivotContentControl>
        </wpfspark:PivotItem.PivotContent>
    </wpfspark:PivotItem>
    <wpfspark:PivotItem>
        <wpfspark:PivotItem.PivotHeader>
            <wpfspark:PivotHeaderControl FontFamily="Segoe WP"
                 FontSize="28"
                 ActiveForeground="White"
                 InactiveForeground="#444444">Item Three</wpfspark:PivotHeaderControl>
        </wpfspark:PivotItem.PivotHeader>
        <wpfspark:PivotItem.PivotContent>
            <wpfspark:PivotContentControl>
                <Border Background="LightSeaGreen"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch">
                    <TextBlock FontFamily="Segoe WP"
                               FontSize="48"
                               Foreground="Black"
                               Text="Item Three Content"></TextBlock>
                </Border>
            </wpfspark:PivotContentControl>
        </wpfspark:PivotItem.PivotContent>
    </wpfspark:PivotItem>
</wpfspark:PivotPanel>

You can also create a common style for the PivotHeaderControl to avoid the repetition of the declaration of their common properties like FontSize, FontFamily, ActiveForeground, InactiveForeground, etc. Here is an example of how you can achieve that:

<Window x:Class="TestWPFSpark.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wpfspark="clr-namespace:WPFSpark;assembly=WPFSpark"
        Title="MainWindow"
        Height="350"
        Width="525">
    <Window.Resources>
        <Style x:Key="PivotHeaderStyle"
               TargetType="wpfspark:PivotHeaderControl">
            <Setter Property="FontFamily"
                    Value="Courier New"></Setter>
            <Setter Property="FontFamily"
                    Value="Courier New"></Setter>
            <Setter Property="FontSize"
                    Value="32"></Setter>
            <Setter Property="ActiveForeground"
                    Value="LawnGreen"></Setter>
            <Setter Property="InactiveForeground"
                    Value="#444444"></Setter>
            <Setter Property="Margin"
                    Value="20,10"></Setter>
        </Style>
    </Window.Resources>
    <Grid Background="Black">
        <wpfspark:PivotPanel x:Name="RootPivotPanel"
                             HeaderHeight="70"
                             HorizontalAlignment="Stretch"
                             HeaderBackground="Black">
            <wpfspark:PivotItem>
                <wpfspark:PivotItem.PivotHeader>
                    <wpfspark:PivotHeaderControl 
                      Style="{StaticResource PivotHeaderStyle}">Item One
                    </wpfspark:PivotHeaderControl>
                </wpfspark:PivotItem.PivotHeader>
                <wpfspark:PivotItem.PivotContent>
                    <wpfspark:PivotContentControl>
                        <Border Background="LightBlue"
                                HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch">
                            <TextBlock FontFamily="Segoe WP"
                                       FontWeight="Light"
                                       FontSize="48"
                                       Foreground="White"
                                       Text="Item One Content"></TextBlock>
                        </Border>
                    </wpfspark:PivotContentControl>
                </wpfspark:PivotItem.PivotContent>
            </wpfspark:PivotItem>
            <wpfspark:PivotItem>
                <wpfspark:PivotItem.PivotHeader>
                    <wpfspark:PivotHeaderControl 
                      Style="{StaticResource PivotHeaderStyle}">Item Two
                    </wpfspark:PivotHeaderControl>
                </wpfspark:PivotItem.PivotHeader>
                <wpfspark:PivotItem.PivotContent>
                    <wpfspark:PivotContentControl>
                        <Border Background="LightGoldenrodYellow"
                                HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch">
                            <TextBlock FontFamily="Segoe WP"
                                       FontWeight="Light"
                                       FontSize="48"
                                       Foreground="Black"
                                       Text="Item Two Content"></TextBlock>
                        </Border>
                    </wpfspark:PivotContentControl>
                </wpfspark:PivotItem.PivotContent>
            </wpfspark:PivotItem>
            <wpfspark:PivotItem>
                <wpfspark:PivotItem.PivotHeader>
                    <wpfspark:PivotHeaderControl 
                       Style="{StaticResource PivotHeaderStyle}">Item Three
                    </wpfspark:PivotHeaderControl>
                </wpfspark:PivotItem.PivotHeader>
                <wpfspark:PivotItem.PivotContent>
                    <wpfspark:PivotContentControl>
                        <Border Background="LightSeaGreen"
                                HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch">
                            <TextBlock FontFamily="Segoe WP"
                                       FontWeight="Light"
                                       FontSize="48"
                                       Foreground="Black"
                                       Text="Item Three Content"></TextBlock>
                        </Border>
                    </wpfspark:PivotContentControl>
                </wpfspark:PivotItem.PivotContent>
            </wpfspark:PivotItem>
        </wpfspark:PivotPanel>
    </Grid>
</Window

Using FluidPivotPanel in your Application Through Code

Through code, you have to declare an ObservableCollection of PivotItems and add PivotItems to it. Once done, you can set the ItemsSource property of the PivotPanel to this ObservableCollection.

ObservableCollection<PivotItem> items = new ObservableCollection<PivotItem>();
for (int i = 0; i < colors.Count(); i++)
{
    PivotHeaderControl tb = new PivotHeaderControl();
    tb.FontFamily = new FontFamily("Segoe WP");
    tb.FontWeight = FontWeights.Light;
    tb.ActiveForeground = Brushes.White;
    tb.InactiveForeground = new SolidColorBrush(Color.FromRgb(48,48,48));
    tb.FontSize = 64;
    tb.Content = colors[i];
    tb.Margin = new Thickness(20, 0, 0, 0);

    PivotContentControl pci = new PivotContentControl();
    ListBox lb = new ListBox() { FontFamily = new FontFamily("Segoe WP"), 
                                 FontSize = 32, 
                                 FontWeight = FontWeights.Light, 
                                 Foreground = Brushes.Gray, 
                                 Background = Brushes.Black, 
                                 BorderThickness = new Thickness(0),
                               };
    lb.ItemTemplate = (DataTemplate)this.Resources["ListBoxItemTemplate"];
    lb.ItemsSource = data[i];
    ScrollViewer.SetHorizontalScrollBarVisibility(lb, ScrollBarVisibility.Disabled);
    lb.HorizontalAlignment = HorizontalAlignment.Stretch;
    lb.VerticalAlignment = VerticalAlignment.Stretch;
    lb.Margin = new Thickness(30,10,10,10);
    pci.Content = lb;

    PivotItem pi = new PivotItem { PivotHeader = tb, PivotContent = pci };
    items.Add(pi);
}

// Set the ItemsSource property of the PivotPanel
RootPivotPanel.ItemsSource = items;

History

  • December 21, 2011: WPFSpark v1.0 released.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

About the Author

Ratish Philip
Software Developer NA
India India
Ratish Philip is a software developer with 8 years of experience. He loves programming in C#, WPF & Silverlight.
 
He is currently exploring the depths of Windows 8 programming.
 
Creating enriched user experiences is what appeals to him the most.
 
Occasionally expresses his creativity through pencil sketching too!
 
Ratish's personal blog: wpfspark.wordpress.com

Comments and Discussions

 
QuestionHelp for customizing PinmemberSidharth Penta23-Dec-13 3:57 
GeneralMy vote of 5 PinmemberiJam_j6-Apr-13 14:36 
QuestionItemTemplate and Collection<Object> Pinmemberguipasmoi30-Dec-12 9:19 
QuestionCool Controls! 5! Pinmemberhennsamc29-Sep-12 10:27 
AnswerRe: Cool Controls! 5! Pinmemberhennsamc29-Sep-12 10:32 
SuggestionTouch Pinmemberdamylen20-Jan-12 9:02 
GeneralMy vote of 5 PinmemberJohn Schroedl17-Jan-12 7:51 
GeneralRe: My vote of 5 PinmemberRatish Philip17-Jan-12 23:00 
GeneralVery Nice PinmemberBradyEvans13-Jan-12 4:23 
GeneralRe: Very Nice PinmemberRatish Philip13-Jan-12 4:35 
QuestionCool PinmvpDave Kerr12-Jan-12 21:16 
AnswerRe: Cool PinmemberRatish Philip12-Jan-12 21:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140718.1 | Last Updated 20 Jan 2012
Article Copyright 2011 by Ratish Philip
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid