Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Este artigo mostra como aplicar padrões comprovados de arquitetura a aplicações de desktop WinUI 3 construídas com o SDK de Aplicações Windows. Vai aprender a configurar injeção de dependências, gerir a configuração e estruturar código para cenários de linha de negócio empresarial (LOB).
Pré-requisitos
- SDK de Aplicações Windows 1.5 ou posterior
- .NET 8 ou posterior
- Visual Studio 2022 versão 17.10 ou posterior com as cargas de desenvolvimento de desktop .NET e desenvolvimento de aplicações Windows
Injeção de dependência
As aplicações de desktop do WinUI 3 não incluem um contentor de injeção de dependências (DI) incorporado como o ASP.NET Core, mas podes adicionar um usando o mesmo Microsoft.Extensions.DependencyInjection pacote NuGet. O DI torna o teu código tavaliável, pouco acoplado e mais fácil de manter.
Configurar um contentor DI
Instale o pacote NuGet:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
Configure o host e os serviços no vosso 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
Se te esqueceres de registar um serviço, a GetService<T>() função auxiliar acima lança uma ArgumentException em tempo de execução, indicando o nome do tipo em falta. Abra a aplicação e navegue até cada página durante o desenvolvimento para verificar se todos os registos estão corretos.
Injetar dependências nos ViewModels
Com o contentor configurado, os seus ViewModels recebem dependências através da injeção de construtores:
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";
}
}
Duração dos serviços
Escolha a vida útil adequada ao registar serviços:
| Duração de vida | Método | Uso para |
|---|---|---|
| Singleton | AddSingleton<T>() |
Navegação, estado em toda a aplicação, caches |
| Âmbito de aplicação | AddScoped<T>() |
Contextos por janela ou por diálogo |
| Transient | AddTransient<T>() |
ViewModels, serviços sem estado |
Sugestão
Regista ViewModels como Transient para que cada navegação crie uma instância nova. Registe os serviços que contêm o estado global da aplicação como Singleton.
Gestão de configuração
Utilize Microsoft.Extensions.Configuration para gerir as definições da aplicação em aplicações de computador, seguindo o mesmo padrão utilizado no ASP.NET Core.
Adicionar suporte de configuração
Instale os pacotes necessários:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
Cria um appsettings.json ficheiro na raiz do teu projeto. No Explorador de Soluções, clique com o botão direito no ficheiro, selecione Propriedades e defina Copiar para Diretório de Saída para Copiar se for mais recente. Em alternativa, adicione <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> à entrada do ficheiro no .csproj.
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
Atribui a configuração na tua configuração 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>();
})
Utilize a configuração nos serviços
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)
};
}
}
Persistência das definições do utilizador
Para definições por utilizador que sobrevivem a atualizações de aplicações, use Windows.Storage.ApplicationData (aplicações empacotadas) ou um ficheiro JSON local (aplicações não empacotadas):
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
As aplicações empacotadas (MSIX) podem utilizar ApplicationData.Current.LocalSettings para pares chave-valor simples. As aplicações não embaladas têm de gerir a sua própria localização de armazenamento.
Marcadores de funcionalidade
Implementar sinalizadores de funcionalidade para permitir uma disponibilização gradual e testes A/B sem necessidade de nova implementação.
Sinalizadores locais de funcionalidades com configuração
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;
}
integração do Azure App Configuration
Para sinalizadores de funcionalidades geridos na cloud, utilize o 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
Para desenvolvimento local, pode usar uma cadeia de ligação em vez de DefaultAzureCredential. Armazene a cadeia de ligação numa variável de ambiente ou no Windows Credential Manager — nunca no controlo de código-fonte:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Sugestão
Para o lançamento gradual baseado na Store ao nível dos pacotes, veja Lançamento gradual de pacotes.
Padrões Enterprise e LOB
As aplicações de negócio têm requisitos adicionais relacionados com identidade, proteção de dados e gestão de dispositivos.
Identidade e acesso condicional
Use MSAL (Biblioteca de Autenticação da Microsoft) para autenticação empresarial:
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);
});
Importante
O http://localhost URI de redirecionamento é adequado para desenvolvimento. Para aplicações de ambiente de trabalho em produção, use antes o Windows broker (WAM), que fornece SSO com a conta do Windows do utilizador e uma proteção de tokens mais robusta.
Aplicações empresariais implementadas através do Intune podem impor políticas de acesso condicional que exigem:
- Conformidade do dispositivo (encriptação, PIN, versão do SO)
- Autenticação de múltiplos fatores
- Restrições de localização da rede
Dados offline e cache
As aplicações LOB de ambiente de trabalho frequentemente precisam de funcionar offline. Implemente um padrão de repositório com cache local:
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>();
}
}
}
Proteção de dados
Use Windows.Security.Cryptography.DataProtection (aplicações incluídas) ou o .NET DataProtectionProvider para encriptar dados locais sensíveis.
Instale o pacote necessário:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
Depois, regista a proteção de dados no teu contentor DI:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
Arquitetura em camadas
Estrutura a tua aplicação WinUI 3 em camadas para manter as dependências a fluir numa só direção:
┌─────────────────────────────┐
│ 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
└─────────────────────────────┘
Regras:
- Cada camada depende apenas da camada diretamente abaixo.
- Os ViewModels nunca referenciam tipos de interface (
Page,Window,ContentDialog). - Os serviços definem interfaces; As implementações vivem na camada de dados.
- Registar todas as dependências entre camadas no contentor DI.
Compatibilidade retroativa e versionamento
Quando lançar novas versões da sua aplicação, considere:
- Migração de dados: Versão do esquema da base de dados local. Utiliza uma ferramenta de migração no arranque para atualizar qualquer esquema anterior para o esquema atual.
- Migração de definições: Armazene uma versão do esquema no seu ficheiro de definições. Ao carregar, aplicar transformações dos formatos antigos para os novos.
- Instalações lado a lado: O MSIX atualiza um pacote existente por defeito. Para executar várias versões principais lado a lado, atribui a cada versão um nome de família de pacotes distinto no momento do design.
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);
}
}
Verifica a tua configuração
Executar a aplicação e aceder a cada página para verificar se os serviços são resolvidos corretamente. Se um serviço não estiver registado, verá um InvalidOperationException em tempo de execução com o nome do tipo em falta. Confirme também que:
- Os valores de configuração carregam a partir de
appsettings.json(verifique uma propriedade limitada no depurador). - As flags de funcionalidade são avaliadas como esperado (ative ou desative uma flag e reinicie).
- A cache offline devolve dados quando a rede não está disponível.
Conteúdo relacionado
Windows developer