shuhelohelo’s blog

C#、WPF、Xamarin.Formsが好きですが、ASP.NET Coreも好きです。

ブランクXamarin.FormsプロジェクトにPrismを適用する

https://xamgirl.com/prism-in-xamarin-forms-step-by-step-part-1

環境

ブランクアプリの作成

まずはブランクXamarin.Formsプロジェクトを作成します。

f:id:shuhelohelo:20190829205819p:plain

インストール済みパッケージの更新

パッケージの更新があればやっておきます。

Prismのインストール

NuGetマネージャでPrismを検索します。

f:id:shuhelohelo:20200123191023p:plain

Prism関連のパッケージが表示されますが、この中からPrism.Unity.Formsを選択します。なぜかというと、これしか知らないからです。

Unityという文字が入っていますが、ゲーム開発などで有名なUnityではありません(お約束)。

App.xamlにPrismの参照を追加

<?xml version="1.0" encoding="utf-8" ?>
<prism:PrismApplication xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:prism="clr-namespace:Prism.Unity;assembly=Prism.Unity.Forms"
             mc:Ignorable="d"
             x:Class="ApplyPrismToXamarinFormsFromScratch.App">
    <Application.Resources>

    </Application.Resources>
</prism:PrismApplication>

追加したのはこれ。

 xmlns:prism="clr-namespace:Prism.Unity;assembly=Prism.Unity.Forms"

その上で、Application要素をprism:PrismApplicationに変更する。

App.xaml.csでPrismApplicationクラスを継承する必要はない

上記サイトだと、App.xaml.cs内のAppクラスがPrismApplicationクラスを継承するようにしているが、App.xamlの変更によって親クラスがPrismApplicationになっているので、ここで継承する必要はありません。

App.xaml.csAppクラスの親クラスはApp.xamlのルートクラスになります。

これを確認してみます。

App.xaml.cs内のAppクラスにマウスオーバーして右クリックからGo to Definition(定義へ移動?)を選択すると、移動先の候補がおそらくVisual Studioのウィンドウ下部に表示されるので、App.xaml.g.csを選択してジャンプします。

この中でAppクラスに関する定義があるので見てみると、以下のようにPrismApplicationクラスを継承するようになっているのがわかります。

    public partial class App : global::Prism.Unity.PrismApplication {
        
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Xamarin.Forms.Build.Tasks.XamlG", "2.0.0.0")]
        private void InitializeComponent() {
            global::Xamarin.Forms.Xaml.Extensions.LoadFromXaml(this, typeof(App));
        }
    }

メソッドの実装

PrismApplicationを継承したことによって、現時点でApp.xaml.csAppクラスには赤い波線がついて、OnInitializedRegisterTypesメソッドを実装するように求められています。

f:id:shuhelohelo:20190831113622p:plain

この2つのメソッドをAppクラスに追加します。同時にもともと定義されていたメソッド群は削除します(下のコードではコメントアウトしてありますが)。

    public partial class App
    {
        //public App()
        //{
        //    InitializeComponent();

        //    MainPage = new MainPage();
        //}

        //protected override void OnStart()
        //{
        //    // Handle when your app starts
        //}

        //protected override void OnSleep()
        //{
        //    // Handle when your app sleeps
        //}

        //protected override void OnResume()
        //{
        //    // Handle when your app resumes
        //}


        public App(IPlatformInitializer initializer=null):base(initializer)
        {
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            //ここではDIコンテナへの登録を記述する
        }

        protected override void OnInitialized()
        {
            InitializeComponent();
        }
    }

コンストラクタの追加とネイティブ側からの呼び出し部分の変更

Appクラスのコンストラクタを以下のようにします. 引数でIPlatformInitializerを受け取るようにします.

        public App(IPlatformInitializer initializer=null):base(initializer)
        {
        }

IPlatformInitializerについては以下の記事が詳しいです.

プラットフォームごとのネイティブ側の機能をDIによって呼び分けるための仕組みです.OSごとに異なる処理をさせたい場合に使うようです.

blog.okazuki.jp

コンストラクタのシグニチャの変更に伴い,コンストラクタを呼び出している各プラットフォーム側にも変更を加えます.

Android

MainActivity.csを開きます.

まずはAndroidInitializerという名前でIPatformInitializerを継承したクラスを作ります.これはMainActivityクラス内に追加します.

        public class AndroidInitializer : IPlatformInitializer
        {
            public void RegisterTypes(IContainerRegistry containerRegistry)
            {
                //ここでコンテナに登録
                //例↓
                //var builder = new ContainerBuilder();
                //builder.RegisterType<GreetingService>().As<IGreetingService>().SingleInstance();
                //builder.Update(container);
            }
        }

次に,MainActivityクラスのOnCreateメソッド内でAppクラスをインスタンス化している部分で,コンストラクタに上記のAndroidInitializerクラスのインスタンスを渡すようにします.

        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            LoadApplication(new App(new AndroidInitializer()));// <- ここ
        }

iOS

AppDelegate.csファイルを開きます.

Android側と同じようにIPlatformInitializerを継承したクラスを作ります.

        public class iOSInitializer : IPlatformInitializer
        {
            public void RegisterTypes(IContainerRegistry containerRegistry)
            {
                
            }
        }

AppDelegateクラス内のFinishedLaunchingメソッドでAppクラスのコンストラクタを呼び出しているので,iOSInitializerインスタンスを渡すようにします.

        public override bool FinishedLaunching(UIApplication app, NSDictionary options)
        {
            global::Xamarin.Forms.Forms.Init();
            LoadApplication(new App(new iOSInitializer()));// <- ここ

            return base.FinishedLaunching(app, options);
        }

ここまでで,Prismの導入が完了です.

実装

Viewを作る

共有プロジェクト側にViewsフォルダを作成します.

f:id:shuhelohelo:20200123195743p:plain

その中に最初のViewとしてContentPageファイルを作成します.

f:id:shuhelohelo:20200112151156p:plain 名前はここではMainPageとします.

ViewModelを作る

同じく共有プロジェクト側にViewModelsフォルダを作成します.

その中に先程のMainPageと対になるViewModelとしてClassファイルを作成します.

名前はここではMainPageViewModelとします.

ViewとViewModelの名前の規則は大切で,PrismはView名に対して{View名}ViewModelであることを前提としてViewとViewModelを結びつけます.

この規則はViewModelLocatorを介して変更可能ですが,今回はデフォルトの規則を使用します.

Viewをコンテナに登録する

Viewは全て事前にコンテナに登録しておく必要がある.

App.xaml.csファイル内のAppクラスのRegisterTypesメソッドで登録を行う.

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {
            containerRegistry.RegisterForNavigation<MainPage>();//規則に従ってViewModelと結び付けられる
            containerRegistry.RegisterForNavigation<MainPage, MainPageViewModel>();//明示的に指定
        }

RegisterForNavigationメソッドに対してViewのクラスだけ渡して,ViewModelとの結びつけはPrismに任せるか,ViewとViewModelの双方を渡して明示的に結びつけることができます.

これでViewとViewModelの間でデータバインディングが可能になります.

起動時に表示するページを指定

OnInitializedメソッド内でNavigationService.NavigateAsyncメソッドでViewを指定します.

指定の仕方はいくつもあります.

        protected override void OnInitialized()
        {
            InitializeComponent();

            NavigationService.NavigateAsync(nameof(MainPage));
        }

ViewModelへの変更

各ViewのViewModelではページ遷移などを担うNavigationServiceをコンストラクタの引数として受け取ります.これはPrismによってDIされます.

    public class MainPageViewModel
    {
        INavigationService _navigationService;
        public MainPageViewModel(INavigationService navigationService)
        {
            _navigationService = navigationService;
        }
    }

例えばボタンがクリックされたときにページ遷移する場合は,_navigationService.GoBackAsync()メソッドで遷移するのに使われます.

次に,ViewModelのクラスにBindableBaseクラスを継承させます.

このクラスを継承することによって,データバインディングをサポートする仕組みを使えるようになります.

        private string _title="テスト2";
        public string Title
        {
            get => _title;
            set => SetProperty(ref _title, value);
        }

また,ViewModelのクラスにINavigatedAwareを継承させます. これを継承することで,ページ遷移する直前とページ遷移してきた直後に処理を行う以下の2つのメソッドが追加されます.

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
        }