sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

Prism Full App (.NET Core) テンプレートを体験する

f:id:shikaku_sh:20211013180838p:plain:w400

Visual Studio 拡張機能Visual Studio Marketplace から「Prism Template Pack」をインストールすることができます。これに「Prism Full App (.NET Core)」というテンプレートがあり、Prism 製作者が提案するプロジェクトのテンプレートがあるので、構造を理解して参考にしよう、という記事です。

f:id:shikaku_sh:20211013154915p:plain:w500
テストするときはダウンロードしてください

テンプレートの構成について

基本的には以下の記事でよいのだけど、サンプルのリンクが切れていたりするので、そのあたりをフォローした内容になります。

f:id:shikaku_sh:20211013154859p:plain

上図のように、テンプレートは6つのプロジェクトを持ちます。整理すると以下のとおり:

  • WPF_Core_FullSample
    • Core
    • Modules.ModuleName
    • Services.Interfaces
      • Services.Services
    • Tests

テンプレートを1つずつ整理します。

WPF_Core_FullSample プロジェクト

プロジェクトのエントリーポイント。(実際の Window を含むなど)基本になる。

プロジェクト参照は以下:

  • Core
  • Modules.ModuleName
  • Services.Interfaces
    • Services.Services

テストを除いたすべてのプロジェクトが参照されているため、一番上位のプロジェクトであることがわかります。(お互いのプロジェクト A, B は相互参照できないですし、するべきではない)

  • 「A ←→ B」とはできない
  • 「A ← B」or「A → B」となる

Core プロジェクト

Modules に追加されるプロジェクトが参照するような共通クラスを定義します。なので、他のプロジェクト参照がありません。

プロジェクト参照は以下:

  • なし

VM の基底クラスであったり、ContentRegion が定義されています。Core という名前なのもですが、全てのプロジェクトから参照を受けてもよいデータを格納すること、というのが原則になります。

Modules.ModuleName プロジェクト

View, ViewModel を定義します。

プロジェクト参照は以下:

  • Core
  • Services.Interfaces

Services.Interface のみを参照しているので、実装を担うプロジェクト Services.Serives と繋げていないところがポイント。

ModuleName というのは、Modules.作ったモジュール名 ってことなので、このプロジェクトは、削除して作り直したほうがよいです。

Services.Interfaces プロジェクト

Services.Services のモデル(インターフェース)を定義します。なので、他のプロジェクト参照がありません。

プロジェクト参照は以下:

  • なし

Modules に格納されるプロジェクトから利用されるのが基本です。ただし、全てのプロジェクトから参照を受けてもよいデータを格納すること、というのが原則になります。

この考え方は結構わかりやすくて、DB の場合だと以下のように考えるとわかりやすいと思いました。

具体的な実装と切り離すことができているので、DB の変更も容易です。具体的には、エントリーのプロジェクトの修正だけ。(RegisterTypes でインターフェースを解決する実装クラスの変更)VM にこれを仕込むのは、とても慎重な設計ですね。

Prism に関係していない点もポイント。

Services.Services プロジェクト

Services.Interfaces プロジェクトで定義したインターフェースを具体的に実装します。 Services を丁寧に設計した理由がここに詰まる感じだと思います。

プロジェクト参照は以下:

  • ServicesInterfaces

参照がないので、どうやってデータを作るのか、参照を増やすのか、注意が必要。ロジックとビューを完全に切り離す設計になっていると思います。

Prism に関係していない点もポイント。

新しいサービスを追加するテスト

IMessageService のサンプルとして、IMessageServiceAsync を追加してみます。(元サンプルのとおりですが

Services.Interfaces の修正

namespace WPF_Core_FullSample.Services.Interfaces
{
    public interface IMessageService
    {
        string GetMessage();
    }

    public interface IMessageServiceAsync
    {
        ValueTask<string> GetMessageAsync();
    }
}

Services.Services の追加

GetMessageAsync の具体的なコードを以下とします。 JsonSerializer を利用しているので、System.Text.Json をNuGet からダウンロードしておこう。

namespace WPF_Core_FullSample.Services
{
    public class MessageServiceAsync : IMessageServiceAsync
    {
        private readonly HttpClient _HttpClient;

        public MessageServiceAsync(HttpClient httpClient)
        {
            _HttpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public async ValueTask<string> GetMessageAsync()
        {
            using var jsonStream = await _HttpClient.GetStreamAsync("https://raw.githubusercontent.com/runceel/mockapi/master/message.json");

            // 2秒遅らせる
            await Task.Delay(2000);

            var result = await JsonSerializer.DeserializeAsync<MessageResult>(
                jsonStream,
                new JsonSerializerOptions
                {
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                });

            return result.Message;
        }

        public class MessageResult
        {
            public string Message { get; set; }
        }
    }
}

ちなみに URL から取得しているデータは json データで次のとおり:

{
  "message": "Hello from GitHub"
}

Modules.ModuleName の修正

ViewAViewModel クラスに MessageAsync プロパティを追加して、コンストラクタの引数をひとつ増やして IMessageServiceAsync を追加します。

ViewAViewModel.cs の修正:

public class ViewAViewModel : RegionViewModelBase
{
    private string _message;
    public string Message
    {
        get { return _message; }
        set { SetProperty(ref _message, value); }
    }

    private string _messageAsync = "Now Loading...";
    public string MessageAsync
    {
        get { return _messageAsync; }
        set { SetProperty(ref _messageAsync, value); }
    }

    public ViewAViewModel(IRegionManager regionManager, IMessageService messageService, IMessageServiceAsync messageServiceAsync) :
        base(regionManager)
    {
        Message = messageService.GetMessage();
        messageServiceAsync.GetMessageAsync().AsTask().ContinueWith(p =>
        {
            MessageAsync = p.Result;
        });
    }

    public override void OnNavigatedTo(NavigationContext navigationContext)
    {
        //do something
    }
}

ViewA.XAML の修正:

<UserControl x:Class="WPF_Core_FullSample.Modules.ModuleName.Views.ViewA"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WPF_Core_FullSample.Modules.ModuleName.Views"
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="True" >
    <StackPanel Background="Gray" Orientation="Vertical">
        <TextBlock Margin="10" Text="{Binding Message}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="24"/>
        <TextBlock Margin="10" Text="{Binding MessageAsync}"
                   HorizontalAlignment="Center"
                   VerticalAlignment="Center"
                   FontSize="24"/>
    </StackPanel>
</UserControl>

WPF_Core_FullSample プロジェクトの修正

DI パターンの設定です。ここまでやると、DI をすることで疎結合がしっかりしているのがわかります。

App.xaml.cs の修正:

protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
    containerRegistry.RegisterSingleton<IMessageService, MessageService>();
    containerRegistry.RegisterSingleton<IMessageServiceAsync, MessageServiceAsync>();
}

実際のうごきはサンプルのとおり。

サンプル

GitHub にサンプルを公開しています。

f:id:shikaku_sh:20211013154758g:plain:w500

参考