読者です 読者をやめる 読者になる 読者になる

みかづきメモ

学習したことのメモとか、日記とか、備忘録。

Pivot も INavigationService で画面遷移したい!

C# UWP

「ストア」アプリなどで使われている Pivot 。
「ストア」アプリなどの挙動をよく見ると、 Pivot の Content の部分だけが遷移しています。
ということで、そこも Prism の INavigationService で遷移させてみました。


深夜テンションで書いたのでちょっとあれですが、許してください。
まずは、 Pivot に対して Behavior を作ります。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

using Microsoft.Xaml.Interactivity;

using Pyxis.Attach;

namespace Pyxis.Behaviors
{
    internal sealed class AttachNavigationToPivotBehavior : Behavior<Pivot>
    {
        public static readonly DependencyProperty RootFrameProperty =
            DependencyProperty.Register(nameof(RootFrame), typeof(Frame), typeof(AttachNavigationToPivotBehavior),
                                        new PropertyMetadata(null));

        private readonly Stack<int> _pageStack;

        private bool _isAttached;
        private int _oldIndex; // 1つ前の Index

        public Frame RootFrame
        {
            get { return (Frame) GetValue(RootFrameProperty); }
            set { SetValue(RootFrameProperty, value); }
        }

        public AttachNavigationToPivotBehavior()
        {
            _pageStack = new Stack<int>();
            _isAttached = false;
            _oldIndex = -1;
        }

        // https://github.com/PrismLibrary/Prism/blob/3dded2/Source/Windows10/Prism.Windows/PrismApplication.cs#L148-L171
        private Type GetPageType(string pageToken)
        {
            var assemblyQualifiedAppType = GetType().AssemblyQualifiedName;

            var pageNameWithParameter = assemblyQualifiedAppType.Replace(GetType().FullName,
                                                                         typeof(App).Namespace + ".Views.{0}Page");

            var viewFullName = string.Format(CultureInfo.InvariantCulture, pageNameWithParameter, pageToken);
            var viewType = Type.GetType(viewFullName);

            if (viewType == null)
                throw new ArgumentException(string.Format("{0}'{1}' is not found.", nameof(pageToken), pageToken));

            return viewType;
        }

        private void OnSelectionChanged(object sender, SelectionChangedEventArgs args)
        {
            if (!_isAttached)
            {
                RootFrame.Navigating += RootFrameOnNavigating;
                _isAttached = true;
            }
            ReplaceRootFrame();
        }

        private void ReplaceRootFrame()
        {
            var item = AssociatedObject.ItemsPanelRoot?.Children[AssociatedObject.SelectedIndex] as PivotItem;
            if (item == null)
                return;
            foreach (var pivot in AssociatedObject.ItemsPanelRoot?.Children.Select((w, i) => new {Item = w, Index = i}))
            {
                if (pivot.Index == AssociatedObject.SelectedIndex)
                    continue;
                ((PivotItem) pivot.Item).Content = new Frame();
            }
            var pageToken = NavigateTo.GetPageToken(item);
            if (!string.IsNullOrWhiteSpace(pageToken))
                RootFrame.Navigate(GetPageType(pageToken));
            item.Content = RootFrame;
        }

        private void RootFrameOnNavigating(object sender, NavigatingCancelEventArgs args)
        {
            if (args.NavigationMode == NavigationMode.Back)
                AssociatedObject.SelectedIndex = _pageStack.Pop();
            else if (args.NavigationMode == NavigationMode.New)
            {
                if (_oldIndex >= 0)
                    _pageStack.Push(_oldIndex);
            }

            _oldIndex = AssociatedObject.SelectedIndex;
            ReplaceRootFrame();
        }

        #region Overrides of Behavior

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            RootFrame.Navigating -= RootFrameOnNavigating;
            AssociatedObject.SelectionChanged -= OnSelectionChanged;
            base.OnDetaching();
        }

        #endregion
    }
}

Prism の INavigationService は、 PrismApplication で作成された Frame に対して
操作を行っているため、現在表示されている PivotItemContent 部分を、
PrismApplication で作成された Frame へと置き換えます。

また、「ストア」アプリでは、「戻る」ボタンを押すことで、 Pivot の選択項目も変わるため、
遷移が行われるたびに、その時選択状態にあった Pivot の IndexStack へ入れています。

次に、添付プロパティ。

using Windows.UI.Xaml;

namespace Pyxis.Attach
{
    public static class NavigateTo
    {
        public static readonly DependencyProperty PageTokenProperty =
            DependencyProperty.RegisterAttached("PageToken", typeof(string), typeof(NavigateTo),
                                                new PropertyMetadata(string.Empty));

        public static string GetPageToken(DependencyObject obj) => (string) obj.GetValue(PageTokenProperty);

        public static void SetPageToken(DependencyObject obj, string value) => obj.SetValue(PageTokenProperty, value);
    }
}

Prism の INavigationService で遷移する際、 ns.Navigate("Secondary", null) という風に、
遷移先のページを指定する必要があるので、それを XAML から行うためのものです。

Behavior の ReplaceRootFrame の最後でトークンを取得して、遷移を行っています。

最後に XAML とそのコードビハインド。

<Page x:Class="Pyxis.AppShell"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:attach="using:Pyxis.Attach"
      xmlns:behaviors="using:Pyxis.Behaviors"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:i="using:Microsoft.Xaml.Interactivity"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Pivot x:Name="Pivot">
            <i:Interaction.Behaviors>
                <behaviors:AttachNavigationToPivotBehavior RootFrame="{x:Bind AppRootFrame}" />
            </i:Interaction.Behaviors>
            <Pivot.Resources>
                <Style TargetType="TextBlock">
                    <Setter Property="FontSize" Value="18" />
                </Style>
            </Pivot.Resources>
            <PivotItem attach:NavigateTo.PageToken="Page1">
                <PivotItem.Header>
                    <TextBlock Text="Page 1" />
                </PivotItem.Header>
            </PivotItem>
            <PivotItem attach:NavigateTo.PageToken="Page2">
                <PivotItem.Header>
                    <TextBlock Text="Page 2" />
                </PivotItem.Header>
            </PivotItem>
            <PivotItem attach:NavigateTo.PageToken="Page3">
                <PivotItem.Header>
                    <TextBlock Text="Page 3" />
                </PivotItem.Header>
            </PivotItem>
        </Pivot>
    </Grid>
</Page>
using System.ComponentModel;
using System.Runtime.CompilerServices;

using Windows.UI.Xaml.Controls;

namespace Pyxis
{
    /// <summary>
    ///     それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class AppShell : Page, INotifyPropertyChanged
    {
        public AppShell()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void StoreContentFrame(Frame frame) => AppRootFrame = frame;

        private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        #region AppRootFrame

        private Frame _appRootFrame;

        public Frame AppRootFrame
        {
            get { return _appRootFrame; }
            set
            {
                if (_appRootFrame == value)
                    return;
                _appRootFrame = value;
                OnPropertyChanged();
            }
        }

        #endregion
    }
}

App.xaml.csCreateShell で、 StoreContentFrame を呼び出せば OK。

こんな感じで、ちゃんと動きます。

f:id:MikazukiFuyuno:20160616030041g:plain:w400