Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этой статье показано, как применять проверенные шаблоны архитектуры к классическим приложениям WinUI 3, созданным с помощью Windows App SDK. Вы узнаете, как настроить внедрение зависимостей, управлять конфигурацией и структурировать код для сценариев корпоративных бизнес-приложений (LOB).
Необходимые условия
- Windows App SDK 1.5 или более поздней версии
- .NET 8 или более поздней версии
- Visual Studio 2022 версии 17.10 или более поздней версии с рабочими нагрузками разработки классических приложений .NET и разработки приложений Windows
Внедрение зависимостей
Настольные приложения WinUI 3 не включают встроенный контейнер для внедрения зависимостей (DI), как ASP.NET Core, но его можно добавить с помощью того же пакета NuGet Microsoft.Extensions.DependencyInjection. 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();
}
}
Замечание
Если вы забудете зарегистрировать службу, то вспомогательный метод GetService<T>() выше во время выполнения выбросит исключение ArgumentException с указанием имени отсутствующего типа. Запустите приложение и перейдите на каждую страницу во время разработки, чтобы проверить правильность всех регистраций.
Внедрение зависимостей в ViewModels
После настройки контейнера ваши ViewModel получают зависимости через конструкторное внедрение зависимостей:
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";
}
}
Срок службы
Выберите правильное время жизни при регистрации сервисов:
| Продолжительность жизни | Метод | Применение для |
|---|---|---|
| Singleton | AddSingleton<T>() |
Навигация, состояние приложения, кэши |
| Сфокусированные | AddScoped<T>() |
Контексты для каждого окна или диалогового окна |
| Transient | AddTransient<T>() |
ViewModels, сервисы без состояния |
Tip
Зарегистрируйте модели представления как Transient, чтобы при каждой навигации создавался новый экземпляр. Зарегистрируйте службы, которые хранят состояние всего приложения, как Singleton.
Управление конфигурацией
Используйте 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 в корневом каталоге проекта. В Обозреватель решений щелкните файл правой кнопкой мыши, выберите "Свойства" и задайте для параметра Copy to Output Directory значение Copy if new. Или добавьте <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));
}
}
Замечание
Упакованные приложения (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
Для флагов функций, управляемых облаком, используйте Конфигурация приложений Azure:
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();
Замечание
Для локальной разработки можно использовать строка подключения вместо DefaultAzureCredential. Сохраните строку подключения в переменной среды или в диспетчере учетных данных Windows — никогда не в системе контроля версий:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Tip
Сведения о постепенном развертывании через Магазин на уровне пакета см. в разделе Постепенное развертывание пакета.
Корпоративные и бизнес-шаблоны
Бизнес-приложения имеют дополнительные требования к идентификации, защите данных и управлению устройствами.
Удостоверение и условный доступ
Используйте 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
URI перенаправления http://localhost подходит для разработки. Для классических приложений, используемых в рабочей среде, вместо этого используйте брокер Windows (WAM), который обеспечивает единый вход с использованием учетной записи Windows пользователя и более надежную защиту токенов.
Корпоративные приложения, развернутые с помощью Intune, могут применять политики условного доступа, требующие:
- Соответствие устройств (шифрование, ПИН-код, версия ОС)
- Многофакторная проверка подлинности
- Ограничения сетевого расположения
Данные в автономном режиме и кэширование
Бизнес-приложения для настольных компьютеров часто должны работать в автономном режиме. Реализуйте шаблон репозитория с помощью локального кэширования:
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
└─────────────────────────────┘
Правила:
- Каждый слой зависит только от слоя непосредственно под ним.
- ViewModels никогда не ссылается на типы пользовательского интерфейса (
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