Files
XplorePlane/XP.Common/Logging/ViewModels/RealTimeLogViewerViewModel.cs
T

330 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using Prism.Commands;
using Prism.Mvvm;
using Serilog.Events;
using XP.Common.Localization;
namespace XP.Common.Logging.ViewModels
{
/// <summary>
/// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑
/// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc.
/// </summary>
public class RealTimeLogViewerViewModel : BindableBase
{
private readonly Dispatcher _dispatcher;
private readonly ObservableCollection<LogDisplayEntry> _allEntries = new();
private string _filterText = string.Empty;
private bool _isAutoScroll = true;
private int _maxLines = 2000;
private int _totalCount;
private int _filteredCount;
private bool _showDebug = true;
private bool _showInfo = true;
private bool _showWarning = true;
private bool _showError = true;
private bool _showFatal = true;
public RealTimeLogViewerViewModel()
{
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
FilteredEntries = new ObservableCollection<LogDisplayEntry>();
ClearCommand = new DelegateCommand(ClearAll);
}
/// <summary>
/// 过滤后的日志条目集合(绑定到 UI| Filtered log entries (bound to UI)
/// </summary>
public ObservableCollection<LogDisplayEntry> FilteredEntries { get; }
/// <summary>
/// 过滤关键词 | Filter keyword
/// </summary>
public string FilterText
{
get => _filterText;
set
{
if (SetProperty(ref _filterText, value))
{
ApplyFilter();
}
}
}
/// <summary>
/// 是否自动滚动到底部 | Whether to auto-scroll to bottom
/// </summary>
public bool IsAutoScroll
{
get => _isAutoScroll;
set => SetProperty(ref _isAutoScroll, value);
}
/// <summary>
/// 最大行数限制 | Maximum line count limit
/// </summary>
public int MaxLines
{
get => _maxLines;
set => SetProperty(ref _maxLines, value);
}
/// <summary>
/// 总日志条数 | Total log entry count
/// </summary>
public int TotalCount
{
get => _totalCount;
private set => SetProperty(ref _totalCount, value);
}
/// <summary>
/// 过滤后的日志条数 | Filtered log entry count
/// </summary>
public int FilteredCount
{
get => _filteredCount;
private set => SetProperty(ref _filteredCount, value);
}
/// <summary>
/// 是否显示 Debug 级别日志 | Whether to show Debug level logs
/// </summary>
public bool ShowDebug
{
get => _showDebug;
set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); }
}
/// <summary>
/// 是否显示 Info 级别日志 | Whether to show Information level logs
/// </summary>
public bool ShowInfo
{
get => _showInfo;
set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); }
}
/// <summary>
/// 是否显示 Warning 级别日志 | Whether to show Warning level logs
/// </summary>
public bool ShowWarning
{
get => _showWarning;
set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); }
}
/// <summary>
/// 是否显示 Error 级别日志 | Whether to show Error level logs
/// </summary>
public bool ShowError
{
get => _showError;
set { if (SetProperty(ref _showError, value)) ApplyFilter(); }
}
/// <summary>
/// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs
/// </summary>
public bool ShowFatal
{
get => _showFatal;
set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); }
}
/// <summary>
/// 状态栏文本 | Status bar text
/// </summary>
public string StatusText
{
get
{
bool hasLevelFilter = !(_showDebug && _showInfo && _showWarning && _showError && _showFatal);
bool hasTextFilter = !string.IsNullOrEmpty(_filterText);
if (hasLevelFilter || hasTextFilter)
return LocalizationHelper.Get("LogViewer_StatusFiltered", TotalCount, FilteredCount);
return LocalizationHelper.Get("LogViewer_StatusTotal", TotalCount);
}
}
/// <summary>
/// 清空日志命令 | Clear log command
/// </summary>
public DelegateCommand ClearCommand { get; }
/// <summary>
/// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event
/// </summary>
public event Action? ScrollToBottomRequested;
/// <summary>
/// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe)
/// </summary>
public void AddLogEvent(LogEvent logEvent)
{
if (logEvent == null) return;
var entry = new LogDisplayEntry(logEvent);
if (_dispatcher.CheckAccess())
{
AddEntryInternal(entry);
}
else
{
_dispatcher.InvokeAsync(() => AddEntryInternal(entry));
}
}
/// <summary>
/// 内部添加日志条目 | Internal add log entry
/// </summary>
private void AddEntryInternal(LogDisplayEntry entry)
{
_allEntries.Add(entry);
// 超出最大行数时移除最旧的条目 | Remove oldest entries when exceeding max lines
while (_allEntries.Count > _maxLines)
{
var removed = _allEntries[0];
_allEntries.RemoveAt(0);
FilteredEntries.Remove(removed);
}
// 判断是否匹配过滤条件 | Check if entry matches filter
if (MatchesFilter(entry))
{
FilteredEntries.Add(entry);
}
TotalCount = _allEntries.Count;
FilteredCount = FilteredEntries.Count;
RaisePropertyChanged(nameof(StatusText));
// 请求自动滚动 | Request auto-scroll
if (_isAutoScroll)
{
ScrollToBottomRequested?.Invoke();
}
}
/// <summary>
/// 应用过滤器 | Apply filter
/// </summary>
private void ApplyFilter()
{
FilteredEntries.Clear();
foreach (var entry in _allEntries)
{
if (MatchesFilter(entry))
{
FilteredEntries.Add(entry);
}
}
FilteredCount = FilteredEntries.Count;
RaisePropertyChanged(nameof(StatusText));
}
/// <summary>
/// 判断条目是否匹配过滤条件 | Check if entry matches filter
/// </summary>
private bool MatchesFilter(LogDisplayEntry entry)
{
// 级别过滤 | Level filter
var levelMatch = entry.LevelFull switch
{
"Debug" => _showDebug,
"Information" => _showInfo,
"Warning" => _showWarning,
"Error" => _showError,
"Fatal" => _showFatal,
_ => true
};
if (!levelMatch) return false;
// 关键词过滤 | Keyword filter
if (string.IsNullOrWhiteSpace(_filterText)) return true;
return entry.Message.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|| entry.Level.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|| entry.Source.Contains(_filterText, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 清空所有日志 | Clear all logs
/// </summary>
private void ClearAll()
{
_allEntries.Clear();
FilteredEntries.Clear();
TotalCount = 0;
FilteredCount = 0;
RaisePropertyChanged(nameof(StatusText));
}
}
/// <summary>
/// 日志显示条目模型 | Log display entry model
/// </summary>
public class LogDisplayEntry
{
public LogDisplayEntry(LogEvent logEvent)
{
Timestamp = logEvent.Timestamp.LocalDateTime;
TimestampDisplay = Timestamp.ToString("HH:mm:ss.fff");
Level = logEvent.Level.ToString().Substring(0, 3).ToUpper();
LevelFull = logEvent.Level.ToString();
// 渲染消息文本 | Render message text
Message = logEvent.RenderMessage();
// 如果有异常,追加异常信息 | Append exception info if present
if (logEvent.Exception != null)
{
Message += Environment.NewLine + logEvent.Exception.ToString();
}
// 提取 SourceContext | Extract SourceContext
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceValue))
{
Source = sourceValue.ToString().Trim('"');
}
else
{
Source = string.Empty;
}
// 根据日志级别设置颜色 | Set color based on log level
LevelColor = logEvent.Level switch
{
LogEventLevel.Fatal => new SolidColorBrush(Color.FromRgb(180, 0, 0)),
LogEventLevel.Error => new SolidColorBrush(Colors.Red),
LogEventLevel.Warning => new SolidColorBrush(Color.FromRgb(200, 130, 0)),
LogEventLevel.Debug => new SolidColorBrush(Colors.Gray),
LogEventLevel.Verbose => new SolidColorBrush(Colors.LightGray),
_ => new SolidColorBrush(Color.FromRgb(30, 30, 30))
};
// Freeze 画刷,使其可跨线程访问(避免 DependencySource 线程异常)
// Freeze the brush so it can be accessed from any thread
LevelColor.Freeze();
}
public DateTime Timestamp { get; }
public string TimestampDisplay { get; }
public string Level { get; }
public string LevelFull { get; }
public string Message { get; }
public string Source { get; }
public SolidColorBrush LevelColor { get; }
}
}