🙌

MAUIがリリースされる前にAvaloniaUIのDIを試す

2022/03/04に公開

【前置き】MAUIとAvaloniaの比較

どちらもC#でクロスプラットフォームアプリケーションをつくるためのフレームワーク

MAUIは2022年の夏ごろにリリース予定(プレビュー版はすでに試せる)

比較項目 MAUI Avalonia
開発元 マイクロソフト OSS
対応プラットフォーム Android、iOS、macOS、Windows, Linux[1] + WASM[2]

両フレームワークを比較している記事

Avaloniaを学ぶモチベーション

個人的にはマイクロソフトがサポートしているMAUIを選びたくなる

しかしながら、Avaloniaの公式HPを読んでまだ日が浅いものの、XAMLの記述がかなり簡素にできそうな気がしている
Avaloniaの良い点はMAUIにも活かせる(と信じて)、MAUIリリースまでにAvaloniaを学ぶ

本記事ではDependency Injectionの機能を試す

なぜDI?

AvaloniaUIはXAML側のフレームワークなので、DIから入るのは本来おかしい話だけれども・・・
依存性をうまく解決(隠ぺい)するにはDIの補助が不可欠なので、先に確認したい

以上が前置きで、次章から本題に入る

Avaloniaプロジェクトを作成する

Visual Sutdioの拡張機能をつかって、MVVMテンレートプロジェクトを作成する

mvvm

【本題】Dependency Injection

AvaloniaはReactiveUIと親和性が高い
using Splatを追加すれば、すぐにDI機能が使える

しかしながら、ReactiveUIのDI機能自体については説明があるものの、それをAvaloniaとどう組み合わせるかについては言及されていない
Dependency Injection - ReactiveUI

とくに登録についてはAppクラスの親クラスが持っているpublic virtual void RegisterServices();をつかってみたが、これが正攻法である保証はない

登録_Registration

App.xaml.csにあるAppクラスのメソッドをオーバーライドする

別途ModelsフォルダーにPersonクラスとそのインターフェイスを用意し、このメソッド内で登録する
2つは名前付きのインスタンス、残り1つはシングルトンとして登録する

// add references
using Splat;
using AvaloniaApp.Models;

public class App : Application
{
    // override method
    public override void RegisterServices()
    {
        Locator.CurrentMutable.Register<IPerson>(() => new Person("Alice"), "a");
        Locator.CurrentMutable.Register<IPerson>(() => new Person("Bob"), "b");
        Locator.CurrentMutable.RegisterConstant<IPerson>(new Person("Carol"));

        base.RegisterServices();
    }
}

名前解決_Resolution

AliceとBobは呼ばれるたびにインスタンスが生成される
一方でCarolは登録時にインスタンスが生成されている
created atの時刻をみても、Carolの方が早い時間に生成されていることがわかる

RegisterLazySingletonというメソッドもあるが、これは呼ばれたときにインスタンスが生成されるタイプ
引数がデリゲートであるか否かで、インスタンスが生成されるタイミングを類推できる

using Splat;

public MainWindowViewModel()
{
    var a = Locator.Current.GetService<IPerson>("a");
    var b = Locator.Current.GetService<IPerson>("b");
    var c = Locator.Current.GetService<IPerson>();

    a.Greeting();
    b.Greeting();
    c.Greeting();
}

/*
I am Alice, created at 20:21:09
I am Bob, created at 20:21:09
I am Carol, created at 20:21:03
*/

インスタンスの寿命

  • Registerで登録したインスタンスは、生成されたスコープ内で同一
  • Constantで登録したインスタンスは、どこで呼ばれても同一
public MainWindowViewModel()
{
    var a = Locator.Current.GetService<IPerson>("a");
    var c = Locator.Current.GetService<IPerson>();

    a.Greeting(); // 1
    c.Greeting(); // 2
    GreetingTemp();
}

private void GreetingTemp()
{
    Thread.Sleep(2000);
    var a = Locator.Current.GetService<IPerson>("a");
    var c = Locator.Current.GetService<IPerson>();
    
    a.Greeting(); // 3
    c.Greeting(); // 4
}

/*
I am Alice, created at 3:48:41 
I am Carol, created at 3:48:39 
I am Alice, created at 3:48:43
I am Carol, created at 3:48:39
*/

登録時の注意

あとに登録したインスタンスが優先されるため、下記の場合Carolを呼び出す術がなくなる

// Registration
Locator.CurrentMutable.RegisterConstant<IPerson>(new Person("Carol"));
Locator.CurrentMutable.RegisterConstant<IPerson>(new Person("CarolBrother"));                   

// Resolution
var c = Locator.Current.GetService<IPerson>(); // <- CarolBrother

おわりに

AvaloniaのMVVMテンプレートをもとに、Dependency Injectionを試した

Appが継承しているメソッドをオーバーライドするとおさまりがよさそう
※ 引数なしの関数なので、override RegisterServiceに入れなきゃいけないわけではない

脚注
  1. MAUIがサポートするプラットフォーム - その他のプラットフォームのサポート ↩︎

  2. GitHub Issue "Roadmap" - AvaloniaUI/Avalonia ↩︎

Discussion