SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

Windows Template Studio雛形コードの使い方~Blank & CodeBehindなプロジェクト~

これから何回かに分けて、WindowsTemplateStudioで生成したプロジェクトが、どのような構造になっているか見ていこうと思います。

今回は最初なので、UWPの基本的な仕組みも含めて色々と試してみます。

プロジェクトの作成

WindowsTemplateStudioの雛形で、以下の構成でプロジェクトを作成します。

  • ProjectType: Blank
  • Framework: Code Behind
  • Pages, Features: デフォルトのまま

全体の構成

まずは、プロジェクト全体の構造のおさらい。

こんなプロジェクトが生成されます。
f:id:minami_SC:20170527124602p:plain

この構成のプロジェクトでは、主にViewsやModelsフォルダにコードを書いていくことになりそうな雛形ですね。

アプリのスタートアップ処理

で、この辺の処理ではActivationService/ActivationHandlerなどのクラスが色々と出てきます。
この辺の処理は複雑なので、スタートアップ処理全体の流れは、一番最後にまとめて確認したいと思います。

まずはAppクラスの中身を確認します。
最初にチェックしておくポイントは、以下の関数。

このActivationServiceのコンストラクタで、第二引数に渡している型が、最初に表示されるページになります。

    /// <summary>
    /// Provides application-specific behavior to supplement the default Application class.
    /// </summary>
    sealed partial class App : Application
    {
        // :
        // 略
        // :
        private ActivationService CreateActivationService()
        {
            return new ActivationService(this, typeof(Views.MainPage));
        }
    }

ページ表示

MainPageクラス

今回のサンプルは、MVVMは使わず、コードビハインドを利用した雛形となっています。

そのため、ViewModelクラスはなく、以下のようにMainPageクラスでINotifyPropertyChangedインターフェースを実装しています。
Setメソッドも用意されていて、PropertyChangedイベントを呼び出す処理が実装されています。

    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        public MainPage()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
        {
            if (Equals(storage, value))
            {
                return;
            }

            storage = value;
            OnPropertyChanged(propertyName);
        }

        private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

MainPageへの処理追加

このページのコードをちょっと修正し、以下のようなサンプルを作ってみたいと思います。

  • TextBoxで文字列を入力
  • ↑の文字列をTextBlockで表示
  • ボタンを押すと、TextBoxの入力内容をクリア

実行するとこんな感じの画面になります。
f:id:minami_SC:20170527124634p:plain

string型のInputプロパティを作り、ボタンクリック時にはそのプロパティをクリアするだけのサンプルです。
MainPage.xaml/MainPage.xaml.csを、それぞれ以下のように修正します。

MainPage.xaml

<Page
    x:Class="Blank_CodeBehind.Views.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid
        x:Name="ContentArea"
        Margin="12,0,12,0">

        <Grid.RowDefinitions>
            <RowDefinition x:Name="TitleRow" Height="48"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBlock
            x:Name="TitlePage"
            x:Uid="Main_Title"
            Text="Navigation Item 2"
            FontSize="28" FontWeight="SemiLight" TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" VerticalAlignment="Center"
            Margin="0,0,12,7"/>

        <Grid 
            Grid.Row="1" 
            Background="{ThemeResource SystemControlPageBackgroundChromeLowBrush}">
            <StackPanel>
                <TextBox Margin="5" Width="120" Text="{x:Bind Input, Mode=TwoWay}" HorizontalAlignment="Left" />
                <TextBlock Text="{x:Bind Input, Mode=OneWay}" />
                <Button Content="Clear" HorizontalAlignment="Left" Margin="5" Click="Button_Click"/>
            </StackPanel>
        </Grid>
    </Grid>
</Page>

MainPage.xaml.cs

    public sealed partial class MainPage : Page, INotifyPropertyChanged
    {
        public MainPage()
        {
            InitializeComponent();

            this.DataContext = this;
        }

        private string input;
        public string Input
        {
            get { return input; }
            set { this.Set(ref this.input, value); }
        }

        private void Button_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            this.Input = string.Empty;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
        {
            if (Equals(storage, value))
            {
                return;
            }

            storage = value;
            OnPropertyChanged(propertyName);
        }

        private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

ページ遷移

ここで作った雛形では、単一のMainPageしか用意されていません。
自分で新規ページを追加すると、以下のような手順でページ遷移が行えます。

プロジェクト作成時のウィザードで、複数のページを作っていた場合も、同様の方法でページ遷移できます。

ページの追加

Viewsフォルダを右クリックし、メニューから「新しい項目」を選んでクリックします。
「空白のページ」を選んで、新しいページをプロジェクトに追加しましょう。
f:id:minami_SC:20170527124654p:plain
f:id:minami_SC:20170527124720p:plain
このページには、以下のようにTextBlockでのメッセージだけ追加しておきます。
BlankPage1.xaml

<Page x:Class="Blank_CodeBehind.Views.BlankPage1"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:local="using:Blank_CodeBehind.Views"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <TextBlock Margin="5"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="This is BlankPage1.xaml"
                   TextWrapping="Wrap" />
    </Grid>
</Page>

追加したページへの移動

MainPageに以下のようなボタンを追加します。
MainPage.xaml

<Button Content="Navigate" HorizontalAlignment="Left" Margin="5" Click="GoToBlankPage"/>

ボタンクリック時のイベントハンドラは、以下のようにします。
NavigationServiceクラスを用いて、ページ遷移の指示を行います。

MainPage.xaml.cs

        private void GoToBlankPage(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            Services.NavigationService.Navigate<BlankPage1>();
        }

Navigateボタンをクリックすると、ページ遷移します。
ページ遷移すると、ウィンドウ左上に「←」ボタンが出るので、このボタンでページを戻ることができます。
f:id:minami_SC:20170527124918p:plain f:id:minami_SC:20170527124931p:plain

戻る/進む

先ほどのサンプルでは、自動で表示される「←」ボタンでページを戻りましたが、
NavigationServiceの以下のメソッドで、ページを戻る/進む、という動作を実行できます。

戻る処理

            if (Services.NavigationService.CanGoBack)
            {
                Services.NavigationService.GoBack();
            }

進む処理

            if (Services.NavigationService.CanGoForward)
            {
                Services.NavigationService.GoForward();
            }

ローカライズ処理

x:Uid属性とreswファイルを用いたローカライズ

UWPでは、reswファイルを用いたローカライズが行えます。
詳細は、↓のページなどに詳しくまとまっています。
http://www.atmarkit.co.jp/ait/articles/1210/25/news026.html
http://aile.hatenablog.com/entry/2016/04/05/224806

雛形のデフォルトの状態だと、「en-us」のリソースのみが用意されています。
ここでは、「ja-jp」のリソースを追加して、日本語環境用の表示文字列を定義してみます。

ja-jpフォルダを作成し、その中に「リソースファイル(.resw)」を作成します。
f:id:minami_SC:20170527125021p:plain

そして、以下のように「Main_Title.Text」の項目に、日本語環境で表示したい文字列を設定します。
f:id:minami_SC:20170527125033p:plain

実行するとこの通り。
f:id:minami_SC:20170527125050p:plain

これだけで、x:Uid="Main_Title"の要素を日英対応するようローカライズできました。

言語設定の切り替え

ここで行ったローカライズでは、OSの表示言語設定に応じてアプリ表示の文字列が切り替わります。

しかし、OSの表示言語設定を切り替えるのは少し面倒です。

ちょっとだけ別言語での表示を確認したい、というようなときは、以下のようにAppクラスのOnLaunchedイベントで言語設定を切り替えてしまえば、別言語環境で表示される文字列をチェックでできます。

        protected override async void OnLaunched(LaunchActivatedEventArgs e)
        {
            // 表示言語の設定
            System.Globalization.CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
            if (!e.PrelaunchActivated)
            {
                await ActivationService.ActivateAsync(e); 
            }
        }

この方法だと、言語設定が完全には切り替わりません。
ですので、reswファイルで設定した文字列のチェック程度にとどめておき、画面キャプチャなどが必要な場合には実際にOSの表示言語を切り替えた方がよいかと思います。

ヘルパークラス類

ResouceExtensionsクラス

x:Uidやreswを使うと、各種コントロールなどに表示する文字列は簡単にローカライズできます。
しかし、コードビハインドなど、C#コードからreswに定義した文字列にアクセスしたくなる場合もあると思います。

このResourceExtensionsクラスは、reswの内容へのアクセスを補助するヘルパークラスです。
呼び出し方はこんな感じ。

            var text = Helpers.ResourceExtensions.GetLocalized("Main_Title/Text");
            var dlg = new MessageDialog(text);
            await dlg.ShowAsync();

.reswファイルで、Main_Title.Textのように「.」区切りで書いていた部分は、「/」に置き換えてアクセスする必要があるので注意!!

Singletonクラス

一応、こんな風に任意のクラスをシングルトンとして扱えるようにする、汎用的なヘルパークラスです。

        private async void Button_Click_1(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            var person = Helpers.Singleton<Person>.Instance;
            System.Diagnostics.Debug.WriteLine(person.Name);
        }


    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

スタートアップ処理の全体像

ActivationService & ActivationHandlerクラス

WindowsTemplateStudioで生成されるコードで、一番の鬼門になるのはこの辺かと思います。 ちょっとクラス構成も入り組んでますし、各関数の呼ばれるタイミングも、少しわかりにくいです。

ということで、GitHubリポジトリでも、↓にこのActivationService周りの説明をするページが用意されていました。 https://github.com/Microsoft/WindowsTemplateStudio/blob/master/docs/activation.md

アプリを普通に起動するのではなく、サスペンドからの復帰だったり、ファイル関連付けからの起動などなど、
アプリがアクティブになる、様々なパターンを扱いやすくするようにこんな構造になっているようです。

クラス構成

アプリの起動処理に関わるクラス構成は、以下のようになっています。

f:id:minami_SC:20170527125103p:plain

これらのクラスが、以下のようなシーケンスで呼び出されて、初期画面のMainPageが表示されます。
f:id:minami_SC:20170527125115p:plain

少々複雑な構成ですが、ここさえ理解できれば、このテンプレートでは他に難しいところは特にないかと思います。

基本的には、ActivationHandler派生のクラスを作成し、作成したクラスをActivationSerivceに登録することで、起動処理をカスタマイズできるような構成になっています。

アプリ起動処理のカスタマイズ(ファイルの関連付け)

このアプリを、ファイル関連付けから起動できるようにカスタマイズしてみます。

まずは、プロジェクトのプロパティから、「パッケージマニフェスト」の画面を開き、以下のように赤線の部分を設定します。
f:id:minami_SC:20170527125133p:plain

続いて、App.xaml.csでOnFileActivatedイベントの処理を追加します。
ファイル関連付けから起動された場合にも、ActivationServiceでの起動処理が実行されるようにしています。

App.xaml.cs

        protected override async void OnFileActivated(FileActivatedEventArgs args)
        {
            //base.OnFileActivated(args);
            await ActivationService.ActivateAsync(args);
        }

次に、FileAssociationServiceというクラスをServicesフォルダに追加します。
このファイルの内容は以下の通り。
FileAssociationService.cs

    internal class FileAssociationService : ActivationHandler<File<200b>Activated<200b>Event<200b>Args>
    {
        protected override async Task HandleInternalAsync(FileActivatedEventArgs args)
        {
            // ファイルの情報を表示
            var file = args.Files.FirstOrDefault();
            var dlg = new MessageDialog($"{file.Name}を指定して、起動されました。");
            await dlg.ShowAsync();

            // MainPageへと遷移
            NavigationService.Navigate(typeof(MainPage));
            await Task.CompletedTask;
        }
    }

最後に、ActivationSerivceクラスに以下の行を追加し、アクティベーション処理の中で、先ほど追加したFileAssociationServiceのインスタンスを登録しておきます。

ActivationService.cs

        private IEnumerable<ActivationHandler> GetActivationHandlers()
        {
            yield return Helpers.Singleton<FileAssociationService>.Instance;    // ←追加
            yield break;
        }

ここまで出来たら、一度ビルドしてから実行しておきます。

動作確認

作ったアプリを一度実行しておくと、以下のように「プログラムから開く」のメニューに、このアプリが追加されています。
f:id:minami_SC:20170527125154p:plain

ここからアプリを起動すると、先ほど作成したFileAssociationServiceの処理が実行されていることが確認できます。
f:id:minami_SC:20170527125207p:plain

ファイル関連付けの解除

作成したアプリをアンインストールすると、「プログラムから開く」メニューの項目は自動で削除されます。

今回はここまで。
次は、MVVM Basicなプロジェクトを見ていきたいと思います。