かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

UWPでSplitViewの左側のページ名のリストと右側の実際の表示されてるページを同期させる

Prism使ってやってみましょう。UWPアプリを作ってPrism.Unityを追加してAppクラスを書き換えます。

XAML側

<Prism:PrismUnityApplication x:Class="App14.App"
                             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                             xmlns:local="using:App14"
                             xmlns:Prism="using:Prism.Unity.Windows"
                             RequestedTheme="Light">

</Prism:PrismUnityApplication>

C#側

using App14.ViewModels;
using App14.Views;
using Microsoft.Practices.Unity;
using Prism.Unity.Windows;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App14
{
    sealed partial class App : PrismUnityApplication
    {
        public App()
        {
            Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync(
                Microsoft.ApplicationInsights.WindowsCollectors.Metadata |
                Microsoft.ApplicationInsights.WindowsCollectors.Session);
            this.InitializeComponent();
        }

        protected override Task OnLaunchApplicationAsync(LaunchActivatedEventArgs args)
        {
            this.NavigationService.Navigate("Main", null);
            return Task.CompletedTask;
        }
    }
}

画面遷移テスト用に3つくらい画面を作ります。ViewsフォルダにMainPage.xaml、NextPage.xaml、AboutPage.xamlくらい作ります。

f:id:okazuki:20160412213539p:plain

Shellっていう名前でSplitViewを持ったページを作ります。こいつは、SplitViewを持ったページです。

<Page x:Class="App14.Views.Shell"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App14.Views"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Mvvm="using:Prism.Windows.Mvvm"
      Mvvm:ViewModelLocator.AutoWireViewModel="True"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <SplitView x:Name="FrameHost"
                   x:FieldModifier="public"
                   IsPaneOpen="True"
                   DisplayMode="CompactInline">
            <SplitView.Pane>
                <ListView >
                </ListView>
            </SplitView.Pane>
        </SplitView>
    </Grid>
</Page>

ListViewにページ名を出して、右側にページを表示するようにします。PrismでSplitViewとかを持ったクラスを作るには、AppクラスのCreateShellメソッドをオーバーライドして渡されたFrameを使ってページを構築します。今回の場合SplitViewにFrameHostという名前を付けてるので、こいつのContentにFrameを突っ込みます。

protected override UIElement CreateShell(Frame rootFrame)
{
    var shell = this.Container.Resolve<Shell>();
    shell.FrameHost.Content = rootFrame;
    return shell;
}

なんとなく下地ができたので、ページ遷移を管理するクラスを作ります。Prismで画面遷移するためのページ名を管理するクラスです。ページクラスからページ名への変換ロジックも持たせてあります。後で使います。

using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;

namespace App14.ViewModels
{
    public class NavigationStateManager : BindableBase
    {
        public ObservableCollection<string> PageTokens { get; } = new ObservableCollection<string>();

        private string currentPageToken;

        public string CurrentPageToken
        {
            get { return this.currentPageToken; }
            set { this.SetProperty(ref this.currentPageToken, value); }
        }

        public void SetCurrentPageTokenFromPageType(Type pageType)
        {
            var typeName = pageType.Name;
            this.CurrentPageToken = typeName.Substring(0, typeName.Length - 4);
        }
    }
}

こいつをUnityのコンテナにシングルトンで登録します。そして、FrameのNavigatedイベントで現在のページをセットするようにします。CurrentPageTokenが変わったら、そのページに画面遷移もするようにしておきましょう。ということでAppクラスが以下のように化けます。

using App14.ViewModels;
using App14.Views;
using Microsoft.Practices.Unity;
using Prism.Unity.Windows;
using System.Diagnostics;
using System.Threading.Tasks;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace App14
{
    sealed partial class App : PrismUnityApplication
    {
        public App()
        {
            Microsoft.ApplicationInsights.WindowsAppInitializer.InitializeAsync(
                Microsoft.ApplicationInsights.WindowsCollectors.Metadata |
                Microsoft.ApplicationInsights.WindowsCollectors.Session);
            this.InitializeComponent();
        }

        protected override void ConfigureContainer()
        {
            base.ConfigureContainer();
            // NavigationStateManagerをシングルトンで管理してもらう
            this.Container.RegisterType<NavigationStateManager>(new ContainerControlledLifetimeManager());
        }

        protected override Frame OnCreateRootFrame()
        {
            // Frameをカスタマイズ
            var frame = new Frame();
            frame.Navigated += (_, e) =>
            {
                this.Container.Resolve<NavigationStateManager>().SetCurrentPageTokenFromPageType(e.SourcePageType);
            };
            return frame;
        }

        protected override Task OnInitializeAsync(IActivatedEventArgs args)
        {
            // 初期化
            var navigationStateManager = this.Container.Resolve<NavigationStateManager>();
            navigationStateManager.PageTokens.Add("Main");
            navigationStateManager.PageTokens.Add("Next");
            navigationStateManager.PageTokens.Add("About");
            navigationStateManager.CurrentPageToken = "Main";

            // CurrentPageTokenが変わったら画面遷移する
            this.Container.Resolve<NavigationStateManager>()
                .PropertyChanged += (sender, e) =>
                {
                    if (e.PropertyName == nameof(NavigationStateManager.CurrentPageToken))
                    {
                        this.NavigationService.Navigate(navigationStateManager.CurrentPageToken, null);
                    }
                };
            return Task.CompletedTask;
        }

        protected override UIElement CreateShell(Frame rootFrame)
        {
            var shell = this.Container.Resolve<Shell>();
            shell.FrameHost.Content = rootFrame;
            return shell;
        }

        protected override Task OnLaunchApplicationAsync(LaunchActivatedEventArgs args)
        {
            this.NavigationService.Navigate("Main", null);
            return Task.CompletedTask;
        }
    }
}

これで、NavigationStateManagerの状態に同期して画面遷移するようになりました。あとは、NavigationStateManagerをSplitViewの左側のListViewにバインドするだけです。ShellViewModelクラスを作って、以下のようにNavigationStateManagerを受け取って外部に公開しましょう。

using Prism.Windows.Mvvm;

namespace App14.ViewModels
{
    public class ShellViewModel : ViewModelBase
    {
        public NavigationStateManager NavigationStateManager { get; }

        public ShellViewModel(NavigationStateManager navigationStateManager)
        {
            this.NavigationStateManager = navigationStateManager;
        }

    }
}

そして、Shell.xamlでListViewにバインドします。バインドするのは、PageTokensプロパティとCurrentPageTokenプロパティになります。

<Page x:Class="App14.Views.Shell"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:local="using:App14.Views"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Mvvm="using:Prism.Windows.Mvvm"
      Mvvm:ViewModelLocator.AutoWireViewModel="True"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <SplitView x:Name="FrameHost"
                   x:FieldModifier="public"
                   IsPaneOpen="True"
                   DisplayMode="CompactInline">
            <SplitView.Pane>
                <ListView ItemsSource="{x:Bind ViewModel.NavigationStateManager.PageTokens}"
                          SelectedItem="{x:Bind ViewModel.NavigationStateManager.CurrentPageToken, Mode=TwoWay}">
                </ListView>
            </SplitView.Pane>
        </SplitView>
    </Grid>
</Page>

実行すると以下のようになります。初期状態ではMainPageが表示されてます。左側のListViewの選択もMainになってるのが確認できます。

f:id:okazuki:20160412214532p:plain

ListViewを操作すると、画面遷移していくことが確認できます。下図は、Aboutをクリックしたときの様子です。

f:id:okazuki:20160412214621p:plain

戻るボタンを押すときちんとページが戻ってListViewの選択も同期してることが確認できます。

f:id:okazuki:20160412214704p:plain

まとめ

ということで、SplitViewの左側のページのリストと実際に右側に表示されてるページの同期をとってみました。管理クラスを作って、ページの状態をそれと同期させるというアプローチです。もうちょっと複雑な要件になってくると、もうちょっと賢く作らないといけないかもしれないですね。(ページのパラメータ渡すとかetc...)