[WPF]在WPF中使用ObservableCollections顯示Microsoft.Extensions.Logging的日志信息
背景
先前一段時間用RichTextBox實現了Microsoft.Extension.Logger的日志顯示。雖然是用RichTextBox總感覺哪里不對勁,想要添加過濾顯得非常復雜。最近了解并學習了ObservableCollection這個庫(有點火星救援了啊),遂想到了一個更好的實現方式。
引入ObservableCollections庫
- 在包管理中引入ObservableCollections庫
可觀察的日志
- 首先定義一個Log實體
LogMessage。為了后面更好實現Filiter功能,添加LogLevel,EventId等屬性。 - 在應用程序運行期間,需要有個存儲
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等其他東西
- 實現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();
}
}
}
- 實現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,
});
}
}
- 實現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
- 創建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}]\r\n{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>
- 創建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中使用的。

浙公網安備 33010602011771號