この記事では、Windows アプリ SDKで構築された WinUI 3 デスクトップ アプリに実証済みのアーキテクチャ パターンを適用する方法について説明します。 エンタープライズ基幹業務 (LOB) シナリオの依存関係の挿入、構成の管理、および構造コードを設定する方法について説明します。
前提条件
- Windows アプリ SDK 1.5 以降
- .NET 8 以降
- .NET デスクトップ開発およびWindows アプリケーション開発ワークロードを含む Visual Studio 2022 バージョン 17.10 以降
依存関係の挿入
WinUI 3 デスクトップ アプリには、ASP.NET Coreと同様に組み込みの依存関係挿入 (DI) コンテナーは含まれていませんが、同じMicrosoft.Extensions.DependencyInjection NuGet パッケージを使用して追加できます。 DI を使用すると、コードをテスト可能にし、疎結合し、保守が容易になります。
DI コンテナーを設定する
NuGet パッケージをインストールします。
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
App.xaml.csでホストとサービスを構成します。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;
public partial class App : Application
{
public IHost Host { get; }
public static T GetService<T>() where T : class
{
if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
{
throw new ArgumentException(
$"{typeof(T)} needs to be registered in ConfigureServices.");
}
return service;
}
public App()
{
InitializeComponent();
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.UseContentRoot(AppContext.BaseDirectory)
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IDataService, DataService>();
services.AddTransient<IDialogService, DialogService>();
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<SettingsViewModel>();
// Views
services.AddTransient<MainPage>();
services.AddTransient<SettingsPage>();
})
.Build();
}
}
Note
サービスを登録し忘れた場合、上記の GetService<T>() ヘルパーは実行時に、不足している型名を含む ArgumentException を送出します。 アプリを実行し、開発中に各ページに移動して、すべての登録が正しいことを確認します。
ViewModels に依存関係を挿入する
コンテナーが構成されている場合、ViewModels はコンストラクターの挿入によって依存関係を受け取ります。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
private readonly IDataService _dataService;
private readonly INavigationService _navigationService;
public MainViewModel(IDataService dataService, INavigationService navigationService)
{
_dataService = dataService;
_navigationService = navigationService;
}
[ObservableProperty]
private string _statusMessage = string.Empty;
[RelayCommand]
private async Task LoadDataAsync()
{
StatusMessage = "Loading...";
var items = await _dataService.GetItemsAsync();
StatusMessage = $"Loaded {items.Count} items";
}
}
サービスの有効期間
サービスを登録するときに適切な有効期間を選択します。
| 有効期間 | Method | 用途 |
|---|---|---|
| シングルトン | AddSingleton<T>() |
ナビゲーション、アプリ全体の状態、キャッシュ |
| スコープ | AddScoped<T>() |
ウィンドウごとまたはダイアログごとのコンテキスト |
| 一時的な | AddTransient<T>() |
ViewModels、ステートレスサービス |
ヒント
各ナビゲーションで新しいインスタンスが作成されるように、ViewModels を 一時的 として登録します。 アプリ全体の状態を保持するサービスを シングルトンとして登録します。
構成管理
Microsoft.Extensions.Configurationを使用して、ASP.NET Coreで使用されるのと同じパターンであるデスクトップ アプリのアプリ設定を管理します。
構成のサポートを追加する
必要なパッケージをインストールします。
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
プロジェクト ルートに appsettings.json ファイルを作成します。 ソリューション エクスプローラーでファイルを右クリックし、[プロパティ] を選択し、[出力ディレクトリにコピー] を [新しい場合はコピー] に設定します。 または、<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>内のファイルのエントリに.csprojを追加します。
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
DI の設定で構成情報をバインドします。
.ConfigureServices((context, services) =>
{
// Bind settings to a strongly-typed class
services.Configure<AppSettings>(
context.Configuration.GetSection("AppSettings"));
// Inject IOptions<AppSettings> into services
services.AddSingleton<IApiClient, ApiClient>();
})
サービスで構成を使用する
using Microsoft.Extensions.Options;
public class ApiClient : IApiClient
{
private readonly AppSettings _settings;
private readonly HttpClient _httpClient;
public ApiClient(IOptions<AppSettings> options)
{
_settings = options.Value;
_httpClient = new HttpClient
{
BaseAddress = new Uri(_settings.ApiBaseUrl)
};
}
}
ユーザー設定の永続化
アプリの更新後も有効なユーザーごとの設定の場合は、 Windows.Storage.ApplicationData (パッケージアプリ) またはローカル JSON ファイル (パッケージ化されていないアプリ) を使用します。
public class UserSettingsService : IUserSettingsService
{
private readonly string _settingsPath;
public UserSettingsService()
{
var localAppData = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
_settingsPath = Path.Combine(localAppData, "Contoso", "MyApp", "settings.json");
}
public async Task SaveAsync<T>(string key, T value)
{
var settings = await LoadAllAsync();
settings[key] = JsonSerializer.Serialize(value);
Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!);
await File.WriteAllTextAsync(
_settingsPath, JsonSerializer.Serialize(settings));
}
}
Note
パッケージ アプリ (MSIX) では、単純なキーと値のペアに ApplicationData.Current.LocalSettings を使用できます。 パッケージ化されていないアプリは、独自のストレージの場所を管理する必要があります。
機能フラグ
機能フラグを実装して、再デプロイなしで段階的なロールアウトと A/B テストを有効にします。
設定付きのローカル機能フラグ
public interface IFeatureFlagService
{
bool IsEnabled(string featureName);
}
public class FeatureFlagService : IFeatureFlagService
{
private readonly Dictionary<string, bool> _flags;
public FeatureFlagService(IConfiguration configuration)
{
_flags = configuration.GetSection("FeatureFlags")
.Get<Dictionary<string, bool>>() ?? new();
}
public bool IsEnabled(string featureName) =>
_flags.TryGetValue(featureName, out var enabled) && enabled;
}
Azure App Configuration との統合
クラウド管理機能フラグの場合は、Azure App Configurationを使用します。
dotnet add package Microsoft.Extensions.Configuration.AzureAppConfiguration
dotnet add package Microsoft.FeatureManagement
using Azure.Identity;
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config.AddAzureAppConfiguration(options =>
{
options.Connect(
new Uri("https://<your-store>.azconfig.io"),
new DefaultAzureCredential())
.UseFeatureFlags(flagOptions =>
{
flagOptions.CacheExpirationInterval = TimeSpan.FromMinutes(5);
});
});
})
.ConfigureServices((context, services) =>
{
services.AddFeatureManagement(context.Configuration);
})
.Build();
Note
ローカル開発では、DefaultAzureCredentialの代わりに接続文字列を使用できます。 接続文字列を環境変数またはWindows資格情報マネージャーに格納します。ソース管理には格納されません。
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
ヒント
パッケージ レベルでのストア ベースの段階的なロールアウトについては、「 段階的なパッケージのロールアウト」を参照してください。
エンタープライズパターンと LOB パターン
基幹業務アプリには、ID、データ保護、デバイス管理に関する追加の要件があります。
ID と条件付きアクセス
エンタープライズ認証には MSAL (Microsoft Authentication Library) を使用します。
services.AddSingleton<IAuthService>(sp =>
{
var app = PublicClientApplicationBuilder
.Create("your-client-id")
.WithAuthority(AzureCloudInstance.AzurePublic, "your-tenant-id")
.WithRedirectUri("http://localhost")
.Build();
return new AuthService(app);
});
Important
http://localhostリダイレクト URI は開発に適しています。 運用デスクトップ アプリの場合は、代わりに Windows ブローカー (WAM) を使用します。これによって、ユーザーのWindows アカウントに SSO が提供され、トークン保護が強化されます。
Intune を使用して展開されたエンタープライズ アプリでは、次を必要とする条件付きアクセス ポリシーを適用できます。
- デバイス コンプライアンス (暗号化、PIN、OS バージョン)
- 多要素認証
- ネットワークの場所の制限
オフライン データとキャッシュ
デスクトップ LOB アプリは、オフラインで頻繁に動作する必要があります。 ローカル キャッシュを使用してリポジトリ パターンを実装します。
public class CachedRepository<T> : IRepository<T> where T : class, IEntity
{
private readonly IApiClient _apiClient;
private readonly ILocalDatabase _localDb;
public async Task<IReadOnlyList<T>> GetAllAsync(bool forceRefresh = false)
{
if (!forceRefresh)
{
var cached = await _localDb.GetAllAsync<T>();
if (cached.Any())
return cached;
}
try
{
var items = await _apiClient.GetAsync<List<T>>();
await _localDb.UpsertAllAsync(items);
return items;
}
catch (HttpRequestException)
{
// Offline fallback
return await _localDb.GetAllAsync<T>();
}
}
}
データ保護
機密性の高いローカル データを暗号化するために、Windows.Security.Cryptography.DataProtection (パッケージ アプリ) または.NET DataProtectionProviderを使用します。
必要なパッケージをインストールします。
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
次に、DI コンテナーにデータ保護を登録します。
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
レイヤード アーキテクチャ
依存関係を一方向に流し続けるために、WinUI 3 アプリをレイヤーで構成します。
┌─────────────────────────────┐
│ Views (XAML + code-behind)│ ← UI layer, no business logic
├─────────────────────────────┤
│ ViewModels (MVVM Toolkit) │ ← Presentation logic, commands
├─────────────────────────────┤
│ Services / Use Cases │ ← Business rules, orchestration
├─────────────────────────────┤
│ Repositories / Data │ ← Data access, API clients, caching
└─────────────────────────────┘
準則:
- 各レイヤーは、その真下のレイヤーにのみ依存します。
- ViewModel は UI 型 (
Page、Window、ContentDialog) を参照しません。 - サービスはインターフェイスを定義します。実装はデータ層に含まれています。
- DI コンテナー内のすべてのクロスレイヤー依存関係を登録します。
下位互換性とバージョン管理
アプリの新しいバージョンをリリースする場合は、次の点を考慮してください。
- データ移行: ローカル データベース スキーマのバージョン管理。 起動時に移行ランナーを使用して、以前のスキーマから現在のスキーマにアップグレードします。
- 設定の移行: スキーマ バージョンを設定ファイルに格納します。 読み込み時に、古い形式から新規に変換を適用します。
- サイド バイ サイド インストール: MSIX は、既定でパッケージを所定の場所にアップグレードします。 複数のメジャー バージョンを並べて実行するには、デザイン時に各バージョンに個別のパッケージ ファミリ名を割り当てます。
public class DatabaseMigrator
{
public async Task MigrateAsync(SqliteConnection db)
{
var currentVersion = await GetSchemaVersionAsync(db);
if (currentVersion < 2)
await ApplyMigration_v2(db);
if (currentVersion < 3)
await ApplyMigration_v3(db);
await SetSchemaVersionAsync(db, LatestVersion);
}
}
セットアップを確認する
アプリを実行し、各ページに移動して、サービスが正しく解決されたことを確認します。 サービスが登録されていない場合は、実行時に型名が見つからない InvalidOperationException が表示されます。 また、次の点も確認します。
- 構成値は
appsettings.jsonから読み込まれます (デバッガーでバインドされたプロパティを確認します)。 - 機能フラグは期待どおりに評価されます (フラグを切り替えて再起動します)。
- オフライン キャッシュは、ネットワークが使用できない場合にデータを返します。
関連するコンテンツ
Windows developer