Visually Located

XAML and GIS

Getting and Setting the SeletedIndex (visible section) of the Hub, now with Binding!

In my last post I talked about how you can restore the last visible section of the Hub. In that post I talked about the lack of the SelectedIndex and SelectedItem properties in the Hub control. These properties were in the Panorama control. Not having these properties means setting the visible section from a view model requires access to the Hub. This is not ideal. When functionality is not available, create it!

When you want to add functionality to a control there are two basic solutions.

  1. Extend the control by creating a new one.
  2. Extend the control with attached properties

The first solution is generally accomplished by inheriting from the control itself. The second is most often solved with a behavior. Whenever possible I prefer option 1 over option 2. The downside to option 1 is adding more and more functionality trying to come up with a good name for your control.

Extending existing controls is really easy. There [usually] is not a need to create a new style for the control. We can easily add new dependency properties to the control.

public class SelectionHub : Hub
{
    public int SelectedIndex
    {
        get { return (int)GetValue(SelectedIndexProperty); }
        set { SetValue(SelectedIndexProperty, value); }
    }
 
    // Using a DependencyProperty as the backing store for SelectedIndex.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedIndexProperty =
        DependencyProperty.Register(
        "SelectedIndex", 
        typeof(int), 
        typeof(SelectionHub), 
        new PropertyMetadata(0, OnSelectedIndexChanged));
 
    private static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // TODO
    }
}

When extending a control, you’ll want to override the OnApplyTemplate method to plug in your custom functionality. For adding the ability to add set or get the functionality, we’ll want to listen to when the section changes. In the last post I described how you can use the SelectionsInViewChanged event to be notified when the visible sections change. An odd thing about this event that Atley Hunter found is that it will not fire when the Hub has two sections. If we want a solution to work for all hubs, we need another event to hook into. If we view the style of the Hub control, we’ll see that is has a ScrollViewer control that aids moving content.

<Canvas Grid.RowSpan="2">
...
</Canvas>
<ScrollViewer x:Name="ScrollViewer" HorizontalScrollMode="Auto" HorizontalSnapPointsType="None" HorizontalAlignment="Left" HorizontalScrollBarVisibility="Hidden" Grid.RowSpan="2" Template="{StaticResource ScrollViewerScrollBarlessTemplate}" VerticalScrollBarVisibility="Disabled" VerticalScrollMode="Disabled" ZoomMode="Disabled">
    <ItemsStackPanel  x:Name="Panel" CacheLength="6" Orientation="{TemplateBinding Orientation}"/>
</ScrollViewer>
<Canvas Grid.Row="0">
...
</Canvas>

The ScrollViewer has the ViewChanged event that we can hook into to tell when the visible section changes! In the event we can what the current selected index is by checking the first index of the SectionsIdView list.

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
 
    var scroller= GetTemplateChild("ScrollViewer") as ScrollViewer;
    if (scroller == null) return;
 
    scroller.ViewChanged += ScrollerOnViewChanged;
}
 
private void ScrollerOnViewChanged(object sender, ScrollViewerViewChangedEventArgs scrollViewerViewChangedEventArgs)
{
    _settingIndex = true;
    SelectedIndex = Sections.IndexOf(SectionsInView[0]);
    _settingIndex = false;
}

When the SelectedIndex changes, we want to set the visible section. The SelectedIndex can change from binding, from code behind, or from the user swiping. the _settingIndex property above is to prevent trying to change the visible section when setting the SelectedIndex when swiping.

private static void OnSelectedIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var hub = d as SelectionHub;
    if (hub == null) return;
 
    // do not try to set the section when the user is swiping
    if (hub._settingIndex) return;
 
    // No sections?!?
    if (hub.Sections.Count == 0) return;
 
    var section = hub.Sections[hub.SelectedIndex];
    hub.ScrollToSection(section);
}

Using this new hub control is simple

<controls:SelectionHub SelectedIndex="{Binding SeletedSectionIndex}"
                       Header="application name" 
                       Background="{ThemeResource HubBackgroundImageBrush}">
    <!-- sections -->
</controls:SelectionHub>

If you prefer option 2 for extending controls, then can easily be converted to a behavior. First, add a reference to the Behaviors SDK.

BehaviorReference

The key difference is subscribing to the ScrollViewer events when the associated object is attached.

public void Attach(DependencyObject associatedObject)
{
    AssociatedObject = associatedObject;
    var hub = associatedObject as Hub;
    if (null == hub) return;
 
    _scroller = hub.GetChildOfType<ScrollViewer>();
    if (_scroller == null)
    {
        hub.Loaded += OnHubLoaded;
    }
    else
    {
        _scroller.ViewChanged += ScrollerOnViewChanged;
    }
}
 
private void OnHubLoaded(object sender, RoutedEventArgs routedEventArgs)
{
    var hub = (Hub)sender;
 
    _scroller = hub.GetChildOfType<ScrollViewer>();
    if (_scroller != null)
    {
        _scroller.ViewChanged += ScrollerOnViewChanged;
        hub.Loaded -= OnHubLoaded;
    }
}

The behavior listens to the loaded event of the hub because it is possible that the hub it attached after the hub has loaded or before. 99% of the time, it will be before the hub had loaded, but you never know. From there it is pretty much the same.

You can download a Universal app solution in which the Windows Phone project uses the behavior and the Windows project uses the new control. Either solution can be used, the choice of which was used for the the project was random.

blog comments powered by Disqus