Friday 16 December 2011

WP7 MVVM Panorama SelectedIndex Binding

The Panorama control doesn't support SelectedItem or SelectedIndex binding which is important for maintaining state on re-activation after tombstoning and may be required for navigation. To solve this problem, I've created a Panorama Behavior called TrackablePonaramaBehavior which can be attached to a panorama and offers a dependency property for binding to the selected index. The System.Windows.Interactivity library needs referencing to use it:

using System;
using Microsoft.Phone.Controls;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace Demo.Behaviors
{
    public class TrackablePanoramaBehavior : Behavior<Panorama>
    {
        private Panorama _panarama = null;
        private bool _updatedFromUI = false;

        // DP for binding index
        public static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register("SelectedIndex", typeof(int), typeof(TrackablePanoramaBehavior),
            new PropertyMetadata(0, new PropertyChangedCallback(SelectedIndexPropertyChanged)));

        // Index changed by view model
        private static void SelectedIndexPropertyChanged(DependencyObject dpObj, DependencyPropertyChangedEventArgs change)
        {
            if(change.NewValue.GetType() != typeof(int) || dpObj.GetType() != typeof(TrackablePanoramaBehavior))
                return;

            TrackablePanoramaBehavior track = (TrackablePanoramaBehavior)dpObj;

            // If this flag is not checked, the panorama smooth transition is overridden
            if (!track._updatedFromUI)
            {
                Panorama pan = track._panarama;

                int index = (int)change.NewValue;

                if (pan.Items.Count > index)
                {
                    pan.DefaultItem = pan.Items[(int)change.NewValue];
                }
            }

            track._updatedFromUI = false;
        }

        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            this._panarama = base.AssociatedObject as Panorama;
            this._panarama.SelectionChanged += _panarama_SelectionChanged;
        }       

        protected override void OnDetaching()
        {
            base.OnDetaching();

            if(this._panarama != null)
                this._panarama.SelectionChanged += _panarama_SelectionChanged;
        }

        // Index changed by UI
        private void _panarama_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            _updatedFromUI = true;
            SelectedIndex = _panarama.SelectedIndex;
        }
    }
}


To implement this in the View, the following XAML is used (I chopped most xmlns out to make it more concise):

<phone:PhoneApplicationPage

    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

    DataContext="{Binding Main, Source={StaticResource Locator}}">
  
    <!--LayoutRoot contains the root grid where all other page content is placed-->
    <Grid x:Name="LayoutRoot">
        <controls:Panorama>
            <i:Interaction.Behaviors>
                <track:TrackablePanoramaBehavior SelectedIndex="{Binding Path=SelectedIndex, Mode=TwoWay}" />
            </i:Interaction.Behaviors>


And simply bind to the view model:

public int SelectedIndex
{
    get { return this._selectedIndex; }
    set
    {
        if (this._selectedIndex != value)
        {
            this._selectedIndex = value;

            base.RaisePropertyChanged("SelectedIndex");
        }
    }
}

9 comments:

  1. Thanks, great for tombstoning! Did not found another article that explains how to do this.

    ReplyDelete
  2. I'm getting an error that the name doesn't exist in the namespace, but it really does. If you could help me it would be great.

    ReplyDelete
  3. Which name? Where? In the xaml? Have you git an xmlns un for the behavior ns?

    ReplyDelete
  4. Hi, I've got a observable collection with 4 ViewModels which is binded to my ItemSource. Any idea why your code is only being triggered for the first one when the page is initialized, but the SelectedIndex is not triggered when I swip to the next panorama item?

    ReplyDelete
  5. Is the selected index bound to the parent vm that holds the obs collection? Can you post a code snippet of the vm?

    ReplyDelete
  6. Hi Geoff, thanks for taking the time to reply! Problem sorted anyway! Stupid bug with panorama control on wp8 where SelectionChange doesn't get triggered when object type is PanoramaItem. Ridiculous!! Spent hours on this to eventually find this article: http://stackoverflow.com/questions/14260701/windows-phone-8-panorama-selectionchanged-databinding. Great code btw. Many thanks.

    ReplyDelete