Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym artykule pokazano, jak zastosować sprawdzone wzorce architektury do aplikacji klasycznych WinUI 3 utworzonych za pomocą Zestaw SDK do aplikacji systemu Windows. Dowiesz się, jak skonfigurować wstrzykiwanie zależności, zarządzać konfiguracją i strukturyzować kod na potrzeby korporacyjnych scenariuszy biznesowych (LOB).
Wymagania wstępne
- Zestaw SDK do aplikacji systemu Windows wersji 1.5 lub nowszej
- .NET 8 lub nowszy
- Visual Studio 2022 w wersji 17.10 lub nowszej z obciążeniami Programowanie aplikacji klasycznych .NET i Programowanie aplikacji systemu Windows
Wstrzykiwanie zależności
Aplikacje klasyczne WinUI 3 nie zawierają wbudowanego kontenera iniekcji zależności (DI), takiego jak ASP.NET Core, ale można go dodać przy użyciu tego samego Microsoft.Extensions.DependencyInjection pakietu NuGet. Di sprawia, że kod jest testowalny, luźno powiązany i łatwiejszy w obsłudze.
Konfigurowanie kontenera DI
Zainstaluj pakiet NuGet:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
Skonfiguruj hosta i usługi w 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
Jeśli zapomnisz zarejestrować usługę, GetService<T>() powyższy pomocnik zgłasza błąd ArgumentException w czasie wykonywania z brakującą nazwą typu. Uruchom aplikację i odwiedź każdą stronę w trakcie prac programistycznych, aby sprawdzić, czy wszystkie elementy zostały zarejestrowane poprawnie.
Wstrzykiwanie zależności do modelu ViewModels
Po skonfigurowaniu kontenera moduł ViewModels odbiera zależności za pomocą iniekcji konstruktora:
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";
}
}
Okresy istnienia usługi
Wybierz odpowiedni okres istnienia podczas rejestrowania usług:
| Okres istnienia | Metoda | Użyj dla |
|---|---|---|
| Singleton | AddSingleton<T>() |
Nawigacja, stan całej aplikacji, pamięci podręczne |
| Zakresu | AddScoped<T>() |
Konteksty poszczególnych okien lub okien dialogowych |
| Transient | AddTransient<T>() |
ViewModels, usługi bezstanowe |
Wskazówka
Zarejestruj ViewModele jako Transient, aby każda nawigacja powodowała utworzenie nowej instancji. Zarejestruj usługi, które przechowują stan całej aplikacji jako singleton.
Zarządzanie konfiguracją
Użyj Microsoft.Extensions.Configuration, aby zarządzać ustawieniami aplikacji w aplikacjach komputerowych, przy użyciu tego samego wzorca, który jest stosowany w ASP.NET Core.
Dodaj obsługę konfiguracji
Zainstaluj wymagane pakiety:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
Utwórz plik appsettings.json w katalogu głównym projektu. W Eksplorator rozwiązań kliknij plik prawym przyciskiem myszy, wybierz pozycję Właściwości i ustaw opcję Kopiuj do katalogu wyjściowego na Kopiuj, jeśli nowszy. Możesz też dodać <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> do wpisu tego pliku w .csproj.
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
Powiąż konfigurację w konfiguracji 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>();
})
Korzystanie z konfiguracji w usługach
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)
};
}
}
Trwałość ustawień użytkownika
W przypadku ustawień dla poszczególnych użytkowników, które przetrwają aktualizacje aplikacji, użyj Windows.Storage.ApplicationData (spakowanych aplikacji) lub lokalnego pliku JSON (aplikacje rozpakowane):
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
Aplikacje spakietowane (MSIX) mogą używać ApplicationData.Current.LocalSettings do prostych par klucz-wartość. Aplikacje bez pakietu muszą zarządzać własną lokalizacją przechowywania danych.
Flagi funkcjonalności
Zaimplementuj flagi funkcji, aby umożliwić stopniowe wdrażanie i testowanie A/B bez ponownego wdrażania.
Flagi funkcji lokalnych z konfiguracją
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;
}
Integracja z Azure App Configuration
W przypadku flag funkcji zarządzanych przez chmurę użyj 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
W przypadku programowania lokalnego można użyć parametry połączenia zamiast DefaultAzureCredential. Przechowuj parametry połączenia w zmiennej środowiskowej lub w Menedżerze poświadczeń systemu Windows — nigdy w systemie kontroli wersji:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Wskazówka
Informacje o stopniowym wdrażaniu pakietu za pośrednictwem Sklepu znajdziesz w artykule Stopniowe wdrażanie pakietu.
Wzorce dla przedsiębiorstw i obszarów działalności biznesowej
Aplikacje biznesowe mają dodatkowe wymagania dotyczące tożsamości, ochrony danych i zarządzania urządzeniami.
Tożsamość i dostęp warunkowy
Użyj biblioteki MSAL (Microsoft Authentication Library) na potrzeby uwierzytelniania przedsiębiorstwa:
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
Adres URI przekierowania http://localhost nadaje się do celów programistycznych. W przypadku aplikacji klasycznych w środowisku produkcyjnym należy zamiast tego użyć brokera Windows (WAM), który zapewnia logowanie jednokrotne przy użyciu konta Windows użytkownika i silniejszej ochrony tokenów.
Aplikacje dla przedsiębiorstw wdrożone za pośrednictwem usługi Intune mogą wymuszać zasady dostępu warunkowego, które wymagają:
- Zgodność urządzenia (szyfrowanie, numer PIN, wersja systemu operacyjnego)
- Uwierzytelnianie wieloskładnikowe
- Ograniczenia lokalizacji sieciowej
Dane i buforowanie w trybie offline
Aplikacje biznesowe dla komputerów stacjonarnych często muszą działać w trybie offline. Zaimplementuj wzorzec repozytorium przy użyciu buforowania lokalnego:
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>();
}
}
}
Ochrona danych
Użyj Windows.Security.Cryptography.DataProtection (spakowanych aplikacji) lub .NET DataProtectionProvider do szyfrowania poufnych danych lokalnych.
Zainstaluj wymagany pakiet:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
Następnie zarejestruj ochronę danych w kontenerze DI:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
Architektura warstwowa
Utwórz strukturę aplikacji WinUI 3 w warstwach, aby zapewnić przepływ zależności w jednym kierunku:
┌─────────────────────────────┐
│ 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
└─────────────────────────────┘
Zasady:
- Każda warstwa zależy tylko od warstwy bezpośrednio poniżej.
- Modele view nigdy nie odwołują się do typów interfejsu użytkownika (
Page,Window,ContentDialog). - Usługi definiują interfejsy; implementacje działają w warstwie danych.
- Zarejestruj wszystkie zależności między warstwami w kontenerze DI.
Zgodność z poprzednimi wersjami i przechowywanie wersji
Po wydaniu nowych wersji aplikacji rozważ następujące kwestie:
- Migracja danych: wersja lokalnego schematu bazy danych. Użyj modułu uruchamiającego migrację podczas uruchamiania, aby uaktualnić z dowolnego poprzedniego schematu do bieżącego.
- Migracja ustawień: przechowywanie wersji schematu w pliku ustawień. Podczas ładowania zastosuj przekształcenia ze starych formatów na nowe.
- Instalacje równoległe: MSIX domyślnie aktualizuje pakiet na miejscu. Aby uruchomić wiele wersji głównych obok siebie, przypisz każdą wersję unikatową nazwę rodziny pakietów w czasie projektowania.
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);
}
}
Weryfikowanie konfiguracji
Uruchom aplikację i przejdź kolejno do każdej strony, aby sprawdzić, czy usługi są poprawnie rozwiązywane. Jeśli usługa nie jest zarejestrowana, w czasie wykonywania zostanie wyświetlony element InvalidOperationException z nazwą brakującego typu. Upewnij się również, że:
- Wartości konfiguracji ładują się z
appsettings.json(sprawdź powiązaną właściwość w debuggerze). - Flagi funkcji działają zgodnie z oczekiwaniami (przełącz flagę i uruchom ponownie).
- Buforowanie w trybie offline zwraca dane, gdy sieć jest niedostępna.