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

かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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

UWP

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...)