Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En este artículo se muestra cómo aplicar patrones de arquitectura probados a aplicaciones de escritorio winUI 3 compiladas con el SDK de Aplicaciones para Windows. Aprenderá a configurar la inyección de dependencias, gestionar la configuración y estructurar el código para escenarios empresariales de línea de negocio (LOB).
Prerequisites
- SDK de Aplicaciones para Windows 1.5 o posterior
- .NET 8 o posterior
- Visual Studio 2022, versión 17.10 o posterior, con las cargas de trabajo de desarrollo de escritorio de .NET y desarrollo de aplicaciones de Windows
Inserción de dependencia
Las aplicaciones de escritorio WinUI 3 no incluyen un contenedor integrado de inyección de dependencias (DI), como sí lo hace ASP.NET Core, pero puede agregar uno con el mismo paquete NuGet Microsoft.Extensions.DependencyInjection. La inyección de dependencias (DI) hace que tu código sea comprobable, esté débilmente acoplado y sea más fácil de mantener.
Configuración de un contenedor de inserción de dependencias
Instale el paquete NuGet:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
Configure el host y los servicios en 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
Si olvida registrar un servicio, la GetService<T>() función auxiliar anterior lanza un ArgumentException en tiempo de ejecución con el nombre del tipo que falta. Ejecute la aplicación y vaya a cada página durante el desarrollo para comprobar que todos los registros son correctos.
Inserción de dependencias en ViewModels
Con el contenedor configurado, viewModels recibe dependencias mediante la inserción de constructores:
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";
}
}
Duración del servicio
Elija la duración adecuada al registrar los servicios:
| Ciclo de vida | Método | Usado para |
|---|---|---|
| Singleton | AddSingleton<T>() |
Navegación, estado de toda la aplicación, cachés |
| Limitado por contexto | AddScoped<T>() |
Contextos por ventana o por cuadro de diálogo |
| Transeúnte | AddTransient<T>() |
ViewModels, servicios sin estado |
Tip
Registre ViewModels como transitorio para que cada navegación cree una nueva instancia. Registre los servicios que mantienen el estado global de la aplicación como Singleton.
Administración de configuración
Use Microsoft.Extensions.Configuration para administrar la configuración de la aplicación en aplicaciones de escritorio, el mismo patrón que se usa en ASP.NET Core.
Añadir soporte de configuración
Instale los paquetes necesarios:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
Cree un appsettings.json archivo en la raíz del proyecto. En Explorador de soluciones, haga clic con el botón derecho en el archivo, seleccione Propiedades y establezca Copiar en directorio de salida en Copiar si es más reciente. Como alternativa, agregue <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> a la entrada del archivo en .csproj.
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
Enlace la configuración en la configuración de inserción de dependencias:
.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>();
})
Use la configuración en los servicios
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)
};
}
}
Persistencia de configuración del usuario
Para la configuración por usuario que sobrevive a las actualizaciones de aplicaciones, use Windows.Storage.ApplicationData (aplicaciones empaquetadas) o un archivo JSON local (aplicaciones sin empaquetar):
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
Las aplicaciones empaquetadas (MSIX) pueden usar ApplicationData.Current.LocalSettings para pares sencillos de clave-valor. Las aplicaciones sin empaquetar deben administrar su propia ubicación de almacenamiento.
Banderas de características
Implemente feature flags para permitir un despliegue gradual y pruebas A/B sin necesidad de volver a desplegar.
Banderas de funcionalidad locales con configuración
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;
}
integración con Azure App Configuration
Para los indicadores de características administrados en la nube, use 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 el desarrollo local, puede usar un cadena de conexión en lugar de DefaultAzureCredential. Guarde la cadena de conexión en una variable de entorno o en el Administrador de credenciales de Windows; nunca en el control de código fuente:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Tip
Para un despliegue gradual a través de la tienda a nivel de paquete, consulta Despliegue gradual por paquete.
Patrones de empresa y LOB
Las aplicaciones de línea de negocio tienen requisitos adicionales en torno a la identidad, la protección de datos y la administración de dispositivos.
Identidad y acceso condicional
Use MSAL (Biblioteca de autenticación de Microsoft) para la autenticación 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
El http://localhost URI de redirección es apropiado para el desarrollo. En el caso de las aplicaciones de escritorio de producción, use el agente de Windows (WAM) en su lugar, que proporciona SSO con la cuenta de Windows del usuario y una protección de token más sólida.
Las aplicaciones empresariales implementadas a través de Intune pueden aplicar directivas de acceso condicional que requieran:
- Cumplimiento de dispositivos (cifrado, PIN, versión del sistema operativo)
- Autenticación multifactor
- Restricciones de ubicación de red
Almacenamiento en caché y datos sin conexión
Las aplicaciones LOB de escritorio suelen tener que funcionar sin conexión. Implemente un patrón de repositorio con almacenamiento en caché 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>();
}
}
}
Protección de los datos
Use Windows.Security.Cryptography.DataProtection (aplicaciones empaquetadas) o la clase DataProtectionProvider de .NET para cifrar datos locales confidenciales.
Instale el paquete necesario:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
A continuación, registre la protección de datos en el contenedor de inserción de dependencias:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
Arquitectura superpuesta
Estructura la aplicación WinUI 3 en capas para mantener las dependencias fluyendo en una dirección:
┌─────────────────────────────┐
│ 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
└─────────────────────────────┘
Reglas:
- Cada capa depende solo de la capa directamente debajo.
- ViewModels nunca hace referencia a tipos de interfaz de usuario (
Page,Window,ContentDialog). - Los servicios definen interfaces; las implementaciones residen en la capa de datos.
- Registre todas las dependencias entre capas en el contenedor de inserción de dependencias.
Compatibilidad con versiones anteriores y control de versiones
Al publicar nuevas versiones de la aplicación, tenga en cuenta lo siguiente:
- Migración de datos: versión del esquema de la base de datos local. Use un ejecutor de migración al inicio para actualizar desde cualquier esquema anterior al actual.
- Migración de configuración: almacene una versión de esquema en el archivo de configuración. Al cargar, aplique transformaciones de formatos antiguos a nuevos.
- Instalaciones en paralelo: MSIX actualiza un paquete en su lugar de forma predeterminada. Para ejecutar varias versiones principales en paralelo, asigne cada versión un nombre de familia de paquete distinto en tiempo de diseño.
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);
}
}
Comprobación de la configuración
Ejecute la aplicación y vaya a cada página para comprobar que los servicios se resuelven correctamente. Si un servicio no está registrado, verá un InvalidOperationException en tiempo de ejecución con el nombre de tipo que falta. Confirme también lo siguiente:
- Los valores de configuración se cargan desde
appsettings.json(compruebe una propiedad vinculada en el depurador). - Los indicadores de características funcionan como se espera (activa o desactiva un indicador y reinicia).
- El almacenamiento en caché sin conexión devuelve datos cuando la red no está disponible.