跨WinUI3与Blazor的ViewModel库:Blazor端IUIThreadService的DI注入方案问询
你要的核心目标是让ViewModel通过DI拿到适配Blazor的IUIThreadService,同时不用在每个组件里手动给ViewModel赋值服务,这里有两种可行方案,其中第二种更适合规模化项目:
方案一:Scoped服务+组件初始化注入上下文
这个方案步骤简单,适合小体量项目:
实现Blazor版IUIThreadService
这个实现需要临时存储组件的InvokeAsync和StateHasChanged方法引用,用于调度UI线程操作:public class BlazorUIThreadService : IUIThreadService { private Func<Func<Task>, Task> _invokeAsync; private Func<Task> _stateHasChanged; // 初始化方法,用来接收组件的上下文方法 public void Initialize(Func<Func<Task>, Task> invokeAsync, Func<Task> stateHasChanged) { _invokeAsync = invokeAsync; _stateHasChanged = stateHasChanged; } public Task ExecuteOnUIThreadAsync(Func<Task> action) { if (_invokeAsync == null) throw new InvalidOperationException("BlazorUIThreadService未完成初始化"); return _invokeAsync(action); } public void ExecuteOnUIThread(Action action) { _ = ExecuteOnUIThreadAsync(() => { action(); return Task.CompletedTask; }); _stateHasChanged?.Invoke(); } }注册Scoped服务
在Blazor的Program.cs里,把这个实现注册为Scoped服务(和Blazor组件的生命周期匹配):builder.Services.AddScoped<IUIThreadService, BlazorUIThreadService>();组件中初始化服务
在需要使用ViewModel的组件里,注入IUIThreadService并完成初始化:@inject IUIThreadService UIThreadService @inject MyViewModel ViewModel @code { protected override void OnInitialized() { if (UIThreadService is BlazorUIThreadService blazorService) { blazorService.Initialize(InvokeAsync, StateHasChanged); } base.OnInitialized(); } }ViewModel中直接使用
ViewModel通过构造函数注入IUIThreadService,不用关心平台细节:public class MyViewModel : INotifyPropertyChanged { private readonly IUIThreadService _uiThreadService; private string _loadStatus; public string LoadStatus { get => _loadStatus; set { _loadStatus = value; OnPropertyChanged(); } } public MyViewModel(IUIThreadService uiThreadService) { _uiThreadService = uiThreadService; } public async Task FetchDataAsync() { // 模拟异步操作 var data = await RemoteDataService.GetDataAsync(); // 调度回UI线程更新属性 await _uiThreadService.ExecuteOnUIThreadAsync(() => { LoadStatus = $"共加载{data.Count}条数据"; }); } // INotifyPropertyChanged实现代码省略... }
方案二:基类统一处理+组件上下文服务(更优)
如果项目组件较多,不想重复写初始化代码,可以用这个方案,通过组件基类统一完成上下文注入:
定义组件上下文接口和实现
先封装组件的UI线程相关方法:public interface IComponentContext { Func<Func<Task>, Task> InvokeAsync { get; set; } Func<Task> StateHasChanged { get; set; } } public class ComponentContext : IComponentContext { public Func<Func<Task>, Task> InvokeAsync { get; set; } public Func<Task> StateHasChanged { get; set; } }修改BlazorUIThreadService依赖上下文
让服务直接依赖IComponentContext,不用单独写初始化方法:public class BlazorUIThreadService : IUIThreadService { private readonly IComponentContext _componentContext; public BlazorUIThreadService(IComponentContext componentContext) { _componentContext = componentContext; } public Task ExecuteOnUIThreadAsync(Func<Task> action) { if (_componentContext.InvokeAsync == null) throw new InvalidOperationException("组件上下文未初始化"); return _componentContext.InvokeAsync(action); } public void ExecuteOnUIThread(Action action) { _ = ExecuteOnUIThreadAsync(() => { action(); return Task.CompletedTask; }); _componentContext.StateHasChanged?.Invoke(); } }注册服务
在Program.cs里注册这两个Scoped服务:builder.Services.AddScoped<IComponentContext, ComponentContext>(); builder.Services.AddScoped<IUIThreadService, BlazorUIThreadService>();创建组件基类
所有Blazor组件都继承这个基类,自动完成上下文初始化:public class BaseBlazorComponent : ComponentBase { [Inject] protected IComponentContext ComponentContext { get; set; } protected override void OnInitialized() { ComponentContext.InvokeAsync = InvokeAsync; ComponentContext.StateHasChanged = StateHasChanged; base.OnInitialized(); } }组件和ViewModel使用
组件只需继承基类,ViewModel还是通过构造函数注入IUIThreadService,全程不用手动处理平台适配逻辑:// 组件代码 @inherits BaseBlazorComponent @inject MyViewModel ViewModel <p>@ViewModel.LoadStatus</p> <button @onclick="ViewModel.FetchDataAsync">加载数据</button>
方案对比
- 方案一:实现简单,适合小型项目或少量组件的场景,但每个使用ViewModel的组件都要写初始化代码
- 方案二:通过基类统一处理,减少重复代码,适合中大型项目,维护性更好
两种方案都能保证ViewModel完全不依赖Blazor的特定API,和WinUI端的ViewModel实现保持一致,实现真正的跨平台复用。
内容的提问来源于stack exchange,提问作者Greg Rotman

