Prism 8应用中如何将ViewModel作为ILogEventSink并兼顾导航与依赖注入?
解决方案
核心思路
要让ViewModel同时承担ILogEventSink职责,又不影响Prism导航/区域功能,核心要做到三点:
- 让ViewModel实现
ILogEventSink并保留依赖注入构造函数 - 确保ViewModel在容器中是单例(避免Serilog使用的实例与导航实例不一致)
- 通过自定义Serilog扩展方法,从Prism容器解析Sink实例完成注册
步骤1:实现兼具LogSink功能的ViewModel
定义ViewModel时同时实现ILogEventSink,并通过构造函数注入所需依赖(如ITextFormatter):
using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Prism.Mvvm; using System.Collections.ObjectModel; using System.IO; using System.Windows.Threading; public class LogViewerViewModel : BindableBase, ILogEventSink { private readonly ITextFormatter _textFormatter; private ObservableCollection<string> _logEntries; public ObservableCollection<string> LogEntries { get => _logEntries; set => SetProperty(ref _logEntries, value); } // 依赖注入构造函数,Prism容器会自动注入ITextFormatter public LogViewerViewModel(ITextFormatter textFormatter) { _textFormatter = textFormatter; LogEntries = new ObservableCollection<string>(); } // 实现ILogEventSink的日志接收逻辑 public void Emit(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); using var writer = new StringWriter(); _textFormatter.Format(logEvent, writer); // 跨线程更新UI需用Dispatcher Application.Current.Dispatcher.Invoke(() => { LogEntries.Insert(0, writer.ToString()); }); } }
对应的View(如LogViewerView.xaml)需开启ViewModelLocator.AutoWireViewModel:
<UserControl x:Class="YourApp.Views.LogViewerView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:prism="http://prismlibrary.com/" prism:ViewModelLocator.AutoWireViewModel="True"> <!-- UI布局示例:用ListView绑定日志列表 --> <ListView ItemsSource="{Binding LogEntries}" /> </UserControl>
步骤2:注册组件到Prism容器
在Prism应用的RegisterTypes方法中,完成View/ViewModel、依赖项的注册,必须将ViewModel注册为单例:
using Prism.Ioc; using Serilog.Formatting.Display; protected override void RegisterTypes(IContainerRegistry containerRegistry) { // 1. 注册单例ViewModel(确保Serilog和导航用同一个实例) containerRegistry.RegisterSingleton<LogViewerViewModel>(); // 2. 注册View与ViewModel的导航关联 containerRegistry.RegisterForNavigation<LogViewerView, LogViewerViewModel>(); // 3. 注册ITextFormatter实例(这里用Serilog自带的模板格式化器) containerRegistry.Register<ITextFormatter>(() => new MessageTemplateTextFormatter("{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")); }
步骤3:自定义Serilog扩展方法注册Sink
不是必须用扩展方法,但这是Serilog推荐的配置风格。扩展方法的核心是从Prism容器解析已注册的ViewModel实例:
using Serilog; using Serilog.Configuration; using Serilog.Core; using Prism.Ioc; public static class SerilogSinkExtensions { public static LoggerConfiguration PrismViewModelSink(this LoggerSinkConfiguration sinkConfiguration, IContainerProvider containerProvider) { // 从Prism容器解析ViewModel(即LogSink实例) var logSink = containerProvider.Resolve<LogViewerViewModel>(); // 将解析后的实例作为Serilog的Sink return sinkConfiguration.Sink(logSink); } }
步骤4:初始化Serilog并关联Sink
在Prism应用的OnInitialized方法中,获取容器实例并完成Serilog配置:
using Serilog; protected override void OnInitialized() { // 获取Prism容器实例 var containerProvider = Container; // 配置Serilog并注册自定义Sink Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.PrismViewModelSink(containerProvider) .CreateLogger(); // 正常执行导航逻辑 NavigationService.NavigateAsync("LogViewerView"); }
关键注意事项
- 单例注册:如果ViewModel不注册为单例,Serilog使用的实例和导航时创建的实例会是两个,导致日志无法显示在当前View上
- 线程安全:
Emit方法可能在非UI线程执行,必须用Dispatcher回到UI线程更新集合 - 扩展方法可选性:你也可以直接在Serilog配置中手动从容器解析Sink,比如:
但扩展方法让代码更符合Serilog的配置风格,可读性更强Log.Logger = new LoggerConfiguration() .WriteTo.Sink(Container.Resolve<LogViewerViewModel>()) .CreateLogger();
内容的提问来源于stack exchange,提问作者EuroEager
相关产品推荐
相关产品推荐

