サービスとして設計する
マルチターゲットアプリを開発する際には、プラットフォーム間の違いを考慮する必要があります。その違いを吸収するために、処理をサービスとして定義します。またその実装は、DIを用いて切り替えるようにします。
FormFactorサービス
テンプレートで生成されたプロジェクトには、すでにこの考え方を示すサンプルとして、FormFactorサービスが含まれています。このサービスは、実行中のプラットフォームやデバイスの種類を検出するための基本的なしくみを提供するものです。
ImageGenerator.SharedプロジェクトのServicesフォルダにあるIFormFactor.csを見てみましょう。
public interface IFormFactor { public string GetFormFactor(); public string GetPlatform(); }
ここではサービスのインターフェイス(IFormFactor)として、デバイスのフォームファクター(Phone、Tabletなど)と、プラットフォーム(iOS、Android、Webなど)を取得するためのメソッドを定義しています。
このインターフェイスの実装は、MAUIプロジェクトとWebプロジェクトで異なります。まず、MAUIプロジェクト(ImageGeneratorプロジェクト)のServicesフォルダにあるFormFactor.csの実装を見てみましょう。
public string GetFormFactor() { return DeviceInfo.Idiom.ToString(); } public string GetPlatform() { return DeviceInfo.Platform.ToString() + " - " + DeviceInfo.VersionString; }
MAUIの実装では、DeviceInfoクラスを使用して実際のデバイス情報を取得しています。一方、Webプロジェクト(ImageGenerator.Webプロジェクト)のServicesフォルダにあるFormFactor.csは、次のようになっています。
public string GetFormFactor() { return "Web"; } public string GetPlatform() { return Environment.OSVersion.ToString(); }
Webの実装では、GetFormFactorメソッドで固定の文字列(Web)を返し、GetPlatformメソッドでは、Environmentクラスを利用するというものになっています。
このように、共通のインターフェイスを定義し、プラットフォームごとに異なる実装を提供するパターンは、マルチターゲットアプリ開発の基本的なアプローチとなるものです。
サービスの登録方法
サービスの登録方法も確認しておきましょう。MAUIプロジェクトのMauiProgram.csとWebプロジェクトのProgram.csで、それぞれFormFactorサービスを登録していますが、両方のプロジェクトで同じAddSingletonメソッドを使用しています。
builder.Services.AddSingleton<IFormFactor, FormFactor>();
AddSingletonメソッドでは、初回要求時にインスタンスが一つだけ作成され、アプリケーションの終了まで同じインスタンスが使用されます。
画像生成サービスの作成
FormFactorサービスと同様のパターンで、画像生成サービスのインターフェイスも準備しておきましょう。ImageGenerator.Sharedプロジェクト内のServicesフォルダを右クリックし、[追加]-[クラス]を選択してから、項目でインターフェイスに変えます。

ファイル名を、IImageGenerationService.csとして追加作成し、次のように定義を変更します。
public interface IImageGenerationService { // 非同期で画像URLを返す Task<string> GenerateImageAsync(string prompt); }
ここで定義するのは、画像を指示する文字列をパラメータとし、生成された画像のURLを返す非同期メソッドです。
次にこのインターフェイスの実装として、同じServicesフォルダにMockImageService.csを作成し、クラス定義を次のように変更します。
public class MockImageService : IImageGenerationService { // モック画像のURLを返す非同期メソッド public async Task<string> GenerateImageAsync(string prompt) { await Task.Delay(1500); // 生成の遅延をシミュレートする return "https://placehold.jp/512x512.png?text=AI%20Generated%20Image"; } }
今回は実際の画像生成までは行わず、Task.Delayメソッドで待機した後にサンプル画像(モック画像)を指定しています。モック画像は、placehold.jpを利用しました。
このモックサービスをそれぞれのプロジェクトに登録しておきましょう。MAUIプロジェクトのMauiProgram.csとWebプロジェクトのProgram.csで、FormFactorサービスの登録の後に、MockImageGenerationServiceを登録するコードを追加します。
builder.Services.AddSingleton<IFormFactor, FormFactor>(); builder.Services.AddSingleton<IImageGenerationService, MockImageService>();
次に、先ほど作成したImageGeneration.razorに、作成したサービスを使用するコードを追加します。
@page "/generate" @using ImageGenerator.Shared.Services @inject IImageGenerationService ImageService ~中略~ @code { ~中略~ private async Task GenerateImage() { ~中略~ try { // サービスを使用して画像を生成 imageUrl = await ImageService.GenerateImageAsync(prompt); } ~中略~ } }
ファイル上部に、@using ImageGenerator.Shared.Servicesを追加して、サービスの名前空間を使用可能にし、@inject IImageGenerationService ImageServiceでサービスを注入します。そして、GenerateImageメソッド内では、注入されたサービスのGenerateImageAsyncメソッドを呼び出すようにします。
サービスを利用することで、UIロジックとビジネスロジックが適切に分離され、コードの保守性が向上します。次回、このモックサービスを、実際の画像生成API(DALL-E 3)を呼び出す実装と置き換える予定です。実装を変更しても、サービスを呼び出すImageGeneration.razorのコードを修正する必要はありません。

各プラットフォームでの表示確認
作成したアプリを実行して、各プラットフォームで表示を確認してみましょう。
Blazor Webアプリでの確認
ソリューションエクスプローラーのImageGenerator.Webプロジェクトを右クリックし、「スタートアッププロジェクトに設定」を選択します。そして実行ボタンをクリックすると、ブラウザが開き、アプリが表示されます。
ブラウザでアプリが表示されたら、メニューから「画像生成」をクリックし、適当にプロンプトを入力して「画像を生成」ボタンをクリックしてみましょう。固定のモック画像が表示されるはずです。入力するプロンプトには影響されません。

Windows(.NET MAUI)アプリでの確認
同様に、ImageGeneratorプロジェクトを右クリックし、「スタートアッププロジェクトに設定」を選択します。そして、デバッグターゲットとして「Windows Machine」を選んで実行ボタンをクリックします。
MAUIアプリが起動したら、「画像生成」メニューをクリックして機能を確認してみましょう。Webアプリと同じUIが表示され、同じ動作をするはずです。

Androidやその他のプラットフォームでも同様に確認できますが、それには適切な開発環境のセットアップが必要です。次回以降、モバイル環境への適用についても解説する予定です。
最後に
今回は、「.NET MAUI Blazor HybridアプリとWebアプリ」テンプレートを使って、プロジェクトの作成と基本的なUIコンポーネントの実装を行いました。画像生成のロジックをサービスとして作成したことでUIから分離され、保守性の高いコードとなっています。
次回は、「画像生成コアロジックの実装」として、OpenAIのDALL-E 3 APIを使った画像生成ロジックを実装する予定です。