Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Questo articolo illustra come applicare modelli di architettura collaudati alle app desktop WinUI 3 create con il SDK per app di Windows. Imparerai come configurare l'iniezione delle dipendenze, gestire la configurazione e strutturare il codice per scenari aziendali line-of-business (LOB).
Prerequisiti
- SDK per app di Windows 1.5 o versione successiva
- .NET 8 o versione successiva
- Visual Studio 2022 versione 17.10 o successiva con i carichi di lavoro di sviluppo di applicazioni .NET desktop e Windows
Iniezione di dipendenze
Le app desktop WinUI 3 non includono un contenitore di inserimento delle dipendenze predefinito come ASP.NET Core, ma è possibile aggiungerne uno usando lo stesso Microsoft.Extensions.DependencyInjection pacchetto NuGet. L'inserimento delle dipendenze rende il codice testabile, ad accoppiamento libero e più semplice da gestire.
Configurare un contenitore DI
Installare il pacchetto NuGet:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
Configura l'host e i servizi in 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 si dimentica di registrare un servizio, l'helper GetService<T>() sopra solleva un'eccezione ArgumentException in fase di esecuzione, indicando il nome del tipo mancante. Esegui l'app e passa a ciascuna pagina durante lo sviluppo per verificare che tutte le registrazioni siano corrette.
Inserire dipendenze in ViewModels
Dopo aver configurato il contenitore, i viewModel ricevono dipendenze tramite l'inserimento del costruttore:
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";
}
}
Durata del servizio
Scegliere la durata appropriata durante la registrazione dei servizi:
| Durata della vita | metodo | Usare per |
|---|---|---|
| Singleton | AddSingleton<T>() |
Navigazione, stato a livello di app, cache |
| Scoperto | AddScoped<T>() |
Contesti per finestra o per finestra di dialogo |
| Transitorio | AddTransient<T>() |
ViewModels, servizi senza stato |
Tip
Registrare i ViewModel come Transient in modo che ogni navigazione crei una nuova istanza. Registrare come Singleton i servizi che mantengono lo stato globale dell'app.
Gestione della configurazione
Usare Microsoft.Extensions.Configuration per gestire le impostazioni delle app nelle app desktop, lo stesso modello usato in ASP.NET Core.
Aggiungere il supporto per la configurazione
Installare i pacchetti necessari:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
Creare un appsettings.json file nella radice del progetto. In Esplora soluzioni fare clic con il pulsante destro del mouse sul file, selezionare Proprietà e impostare Copia nella directory di output su Copia se più recente. In alternativa, aggiungi <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> alla voce del file nel tuo .csproj.
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
Associare la configurazione nella configurazione della dependency injection:
.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>();
})
Usare la configurazione nei servizi
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)
};
}
}
Persistenza delle impostazioni utente
Per le impostazioni per utente che superano gli aggiornamenti delle app, usare Windows.Storage.ApplicationData (app in pacchetto) o un file JSON locale (app non in pacchetto):
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
Le app in pacchetto (MSIX) possono essere usate ApplicationData.Current.LocalSettings per coppie chiave-valore semplici. Le app non in pacchetto devono gestire la propria posizione di archiviazione.
Indicatori di funzionalità
Implementare flag di funzionalità per abilitare l'implementazione graduale e i test A/B senza ridistribuzione.
Flag di funzionalità locali con configurazione
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;
}
integrazione di Configurazione app di Azure
Per i flag di funzionalità gestiti dal cloud, usare Configurazione app di 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();
Note
Per lo sviluppo locale, è possibile usare un stringa di connessione anziché DefaultAzureCredential. Memorizza la stringa di connessione in una variabile di ambiente o in Gestione credenziali di Windows — mai nel sistema di controllo del codice sorgente:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Tip
Per l'implementazione graduale basata sullo Store a livello di pacchetto, vedere Implementazione graduale dei pacchetti.
Modelli aziendali e modelli LOB (linea di business)
Le app line-of-business hanno requisiti aggiuntivi relativi all'identità, alla protezione dei dati e alla gestione dei dispositivi.
Identità e accesso condizionale
Usare MSAL (Libreria di Autenticazione Microsoft) per l'autenticazione aziendale:
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
L'URI http://localhost di reindirizzamento è adatto per lo sviluppo. Per le app desktop di produzione, usare invece Windows broker (WAM), che fornisce l'accesso SSO con l'account Windows dell'utente e una protezione dei token più avanzata.
Le app aziendali distribuite tramite Intune possono applicare criteri di accesso condizionale che richiedono:
- Conformità del dispositivo (crittografia, PIN, versione del sistema operativo)
- Autenticazione a più fattori
- Restrizioni relative alle posizioni di rete
Dati offline e memorizzazione nella cache
Le app desktop aziendali devono spesso funzionare offline. Implementare un modello di repository con memorizzazione nella cache locale:
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>();
}
}
}
Protezione dei dati
Usare Windows.Security.Cryptography.DataProtection (app in pacchetto) o il .NET DataProtectionProvider per crittografare i dati locali sensibili.
Installare il pacchetto richiesto:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
Quindi registra la protezione dei dati nel contenitore DI:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
Architettura a più livelli
Struttura l'app WinUI 3 a livelli per fare in modo che le dipendenze fluiscano in un'unica direzione:
┌─────────────────────────────┐
│ 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
└─────────────────────────────┘
Norme:
- Ogni livello dipende solo dal livello direttamente sotto di esso.
- ViewModels non fa mai riferimento ai tipi di interfaccia utente (
Page,Window,ContentDialog). - I servizi definiscono le interfacce; le implementazioni si trovano nel livello dati.
- Registra tutte le dipendenze tra i livelli nel contenitore DI.
Compatibilità e controllo delle versioni precedenti
Quando rilasci nuove versioni dell'app, prendi in considerazione:
- Migrazione dei dati: versione dello schema del database locale. Usare uno strumento di esecuzione della migrazione all'avvio per eseguire l'aggiornamento da qualsiasi schema precedente a quello corrente.
- Migrazione delle impostazioni: archiviare una versione dello schema nel file delle impostazioni. Al caricamento, applicare trasformazioni da formati precedenti a nuovi.
- Installazioni affiancate: per impostazione predefinita, MSIX esegue l'aggiornamento di un pacchetto direttamente. Per eseguire più versioni principali affiancate, assegnare a ogni versione un nome di famiglia di pacchetti distinto in fase di progettazione.
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);
}
}
Verificare la configurazione
Esegui l'app e passa a ciascuna pagina per verificare che i servizi si risolvano correttamente. Se un servizio non è registrato, verrà visualizzato InvalidOperationException in fase di esecuzione con il nome del tipo mancante. Verificare anche che:
- I valori di configurazione vengono caricati da
appsettings.json(verifica una proprietà associata nel debugger). - I flag di funzionalità funzionano come previsto (attiva/disattiva un flag e riavvia).
- La memorizzazione nella cache offline restituisce i dati quando la rete non è disponibile.