[WPF]在WPF中使用ObservableCollections显示Microsoft.Extensions.Logging的日志信息

背景

先前一段时间用RichTextBox实现了Microsoft.Extension.Logger的日志显示。虽然是用RichTextBox总感觉哪里不对劲,想要添加过滤显得非常复杂。最近了解并学习了ObservableCollection这个库(有点火星救援了啊),遂想到了一个更好的实现方式。

引入ObservableCollections库

  1. 在包管理中引入ObservableCollections库

可观察的日志

  1. 首先定义一个Log实体LogMessage。为了后面更好实现Filiter功能,添加LogLevel,EventId等属性。
  2. 在应用程序运行期间,需要有个存储LogMessage的地方,这里定义接口ILogMessageHolder, 使用ObservableFixedSizeRingBuffer做容器(环形数组,可以使用指定大小的size),整个Observable Logs就是这个ObservableCollections库里的容器。
 public struct LogMessage {     public LogLevel LogLevel { get; set;}     public EventId EventId { get; set;}     public string Category { get; set;}     public DateTime Time { get; set;}     public string Message { get; set;} }  public interface ILoggerMessageHolder {     public ObservableFixedSizeRingBuffer<LogMessage> LogMessages {get;} } 

实现Logger等其他东西

  1. 实现LogMessageProcessor, 这里依然参照ConsoleLoggerProcessor的实现。(有个疑问,直接往LogMessage容器里添加新项,性能能开销应该不大,这里还需要使用工作线程来执行Enqueue操作吗?)
internal class LogMessageProcessor {     //... 省略字段和其余方法实现      public LogMessageProcessor(ILogMessageHolder logMessageHolder, LoggerQueueFullMode fullMode, int maxQueueLength)     {         _logMessageHolder = logMessageHolder;         _messageQueue = new();         FullMode = fullMode;         MaxQueueLength = maxQueueLength;         _outputThread = new Thread(ProcessMessageQueue)         {             IsBackground = true,             Name = "LogMessage queue processing thread"         };         _outputThread.Start();     }      //将 WriteMessage 中写入Console的部分修改为往LogMessage容器里添加LogMessage     internal void WriteMessage(LogMessage message)     {         try         {             _logMessageHolder.LogMessages.AddLast(message);         }         catch         {             CompleteAdding();         }     } } 
  1. 实现Logger。其实这里的LogFormatter属性可要可不要,因为这里的Formatter只对TState做格式化,可以直接做成一个内部的格式化器。当然,这里使用LogFormatter方便后期直接在配置中直接替换实现
internal class Logger : ILogger {     private readonly string _category;     private readonly LogMessageProcessor _processor;     private StringWriter? t_stringWriter;     internal IExternalScopeProvider ScopeProvider { get; set; }     public Logger(string category, LogMessageProcessor processor, LogFormatter formatter,IExternalScopeProvider scopeProvider)     {         _category = category;         _processor = processor;         Formatter = formatter;         ScopeProvider = scopeProvider;     }      public LogFormatter Formatter { get; set; }      public IDisposable? BeginScope<TState>(TState state) where TState : notnull     {         return ScopeProvider.Push(state) ?? NullScope.Instance;     }      public bool IsEnabled(LogLevel logLevel)     {         return true;     }      public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)     {         t_stringWriter ??= new StringWriter();         var entry = new LogEntry<TState>(logLevel, _category, eventId, state, exception, formatter);          Formatter.Write(entry, t_stringWriter);          var sb = t_stringWriter.GetStringBuilder();         var computedString = sb.ToString();         sb.Clear();         _processor.WriteMessage(new LogMessage()         {             Time = DateTime.Now,             Id = eventId,             Level = logLevel,             Category = _category,             Message = computedString,         });     } } 
  1. 实现LoggerProvider和LoggingBuilderExtension
internal class LoggerProvider : ILoggerProvider {     private readonly LogFormatter _formatter;     private readonly ConcurrentDictionary<string, Logger> _loggers = [];     private readonly LogMessageProcessor _processor;      public LoggerProvider(ILogMessageHolder holder, LogFormatter formatter)     {         _formatter = formatter;         _processor = new LogMessageProcessor(holder, LoggerQueueFullMode.Wait, 2500);     }      public ILogger CreateLogger(string categoryName)     {         return _loggers.GetOrAdd(categoryName, new Logger(categoryName, _processor, _formatter));     }      public void Dispose()     {         _processor.Dispose();     } }  public static class LoggingBuilderExtension {     public static ILoggingBuilder AddObservableLogs(this ILoggingBuilder builder)     {         builder.Services.AddSingleton<ILoggerProvider, LoggerProvider>();         builder.Services.AddSingleton<IMfgLoggerProvider, LoggerProvider>();         builder.Services.AddTransient<LogFormatter, SimpleLogFormatter>();         builder.Services.AddSingleton<ILogMessageHolder, LogMessageHolder>();         return builder;     } } 
  • 至此,Logger的核心已经完成,接下来是在View中显示Log

简单实现LogViewer

  1. 创建LogWindow,使用ItemsControl显示LogMessage。
    实际上,LogMessage的Formatter,是下文的LogMessageConverter。
public class LogMessageConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {     if (value is not LogMessage message)     {         return "";     }     return $"{message.Time:yyyy-MM-dd HH:mm:ss,fff} {GetLogLevelString(message.LogLevel)}: {message.Category} [{message.EventId}]rn{message.Message}"; }  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {     throw new NotImplementedException(); }  private static string GetLogLevelString(LogLevel logLevel) => logLevel switch {     LogLevel.Trace => "trce",     LogLevel.Debug => "dbug",     LogLevel.Information => "info",     LogLevel.Warning => "warn",     LogLevel.Error => "fail",     LogLevel.Critical => "crit",     _ => throw new ArgumentOutOfRangeException(nameof(logLevel)) }; } 
<!--省略Window的命名空间和其余属性--> <ItemsControl ItemsSource="{Binding LogMessages}">     <ItemsControl.ItemTemplate>         <DataTemplate>             <!--如果想要显示日志等级的颜色,需要其他TextBlock和添加trigger-->             <TextBlock Text="{Binding ., Converter={StaticResource LogMessageConverter}}"></TextBlock>         </DataTemplate>     </ItemsControl.ItemTemplate> </ItemsControl> 
  1. 创建LogWindow的DataContext
    这里就涉及到ObservableCollections库的知识了,最后绑定到View上的是LogMessages属性。如果想对LogMessages进行过滤,比如选取LogLevel.Information等,使用_viewList的AttachFiliter就好啦。
public class LogWindowViewModel {     private readonly ISynchronizedView<LogMessage, LogMessage> _viewList;     private readonly ILogMessageHolder _logMessageHolder;            public LogWindowViewModel(ILogMessageHolder logMessageHolder)     {         _logMessageHolder = logMessageHolder;         _viewList = logMessageHolder.LogMessages.CreateView(x => x);         LogMessages = _viewList.ToNotifyCollectionChanged();     }      public INotifyCollectionChangedSynchronizedViewList<LogMessage> LogMessages { get; } } 

结尾

使用ObservableCollections可以非常简单的将LogMessage显示到某个UI上,并且LogMessages的生命周期是跟随应用程序,随时可以打开查看应用程序的日志。由于ObservableCollections是纯C#实现,理论上是可以在Avalonia中使用的。

发表评论

评论已关闭。

相关文章