将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
@@ -0,0 +1,74 @@
using System;
using Serilog;
using XP.Common.Logging.Interfaces;
namespace XP.Common.Logging.Implementations
{
/// <summary>
/// Serilog日志服务实现(适配ILoggerService接口)| Serilog logger service implementation (adapts ILoggerService interface)
/// </summary>
public class SerilogLoggerService : ILoggerService
{
private ILogger _logger;
/// <summary>
/// 构造函数:初始化全局日志实例 | Constructor: initialize global logger instance
/// </summary>
public SerilogLoggerService()
{
_logger = Log.Logger;
}
/// <summary>
/// 私有构造函数:用于模块标记 | Private constructor: for module tagging
/// </summary>
private SerilogLoggerService(ILogger logger)
{
_logger = logger ?? Log.Logger;
}
public void Debug(string message, params object[] args)
{
_logger.Debug(message, args);
}
public void Info(string message, params object[] args)
{
_logger.Information(message, args);
}
public void Warn(string message, params object[] args)
{
_logger.Warning(message, args);
}
public void Error(Exception ex, string message, params object[] args)
{
_logger.Error(ex, message, args);
}
public void Fatal(Exception ex, string message, params object[] args)
{
_logger.Fatal(ex, message, args);
}
/// <summary>
/// 手动指定模块名 | Manually specify module name
/// </summary>
public ILoggerService ForModule(string moduleName)
{
if (string.IsNullOrEmpty(moduleName)) return this;
return new SerilogLoggerService(_logger.ForContext("SourceContext", moduleName));
}
/// <summary>
/// 自动使用类型全名作为模块名 | Automatically use type full name as module name
/// </summary>
/// <typeparam name="T">类型参数 | Type parameter</typeparam>
public ILoggerService ForModule<T>()
{
var typeName = typeof(T).FullName ?? typeof(T).Name;
return new SerilogLoggerService(_logger.ForContext("SourceContext", typeName));
}
}
}
@@ -0,0 +1,47 @@
using System;
namespace XP.Common.Logging.Interfaces
{
/// <summary>
/// 通用日志服务接口(与具体日志框架解耦)| Generic logger service interface (decoupled from specific logging framework)
/// </summary>
public interface ILoggerService
{
/// <summary>
/// 调试日志 | Debug log
/// </summary>
void Debug(string message, params object[] args);
/// <summary>
/// 信息日志 | Information log
/// </summary>
void Info(string message, params object[] args);
/// <summary>
/// 警告日志 | Warning log
/// </summary>
void Warn(string message, params object[] args);
/// <summary>
/// 错误日志(带异常)| Error log (with exception)
/// </summary>
void Error(Exception ex, string message, params object[] args);
/// <summary>
/// 致命错误日志(带异常)| Fatal error log (with exception)
/// </summary>
void Fatal(Exception ex, string message, params object[] args);
/// <summary>
/// 标记日志所属模块(手动指定模块名)| Mark logger module (manually specify module name)
/// </summary>
/// <param name="moduleName">模块名称 | Module name</param>
ILoggerService ForModule(string moduleName);
/// <summary>
/// 标记日志所属模块(自动使用类型全名)| Mark logger module (automatically use type full name)
/// </summary>
/// <typeparam name="T">类型参数(自动推断命名空间+类名)| Type parameter (automatically infer namespace + class name)</typeparam>
ILoggerService ForModule<T>();
}
}
+57
View File
@@ -0,0 +1,57 @@
using System;
using Serilog;
using Serilog.Events;
using XP.Common.Configs;
using XP.Common.Logging.ViewModels;
namespace XP.Common.Logging
{
/// <summary>
/// Serilog全局初始化工具(应用启动时调用一次)
/// </summary>
public static class SerilogInitializer
{
/// <summary>
/// 初始化Serilog日志
/// </summary>
public static void Initialize(SerilogConfig config)
{
if (config == null) throw new ArgumentNullException(nameof(config));
// 确保日志目录存在
if (!System.IO.Directory.Exists(config.LogPath))
System.IO.Directory.CreateDirectory(config.LogPath);
// 解析日志级别
if (!Enum.TryParse<LogEventLevel>(config.MinimumLevel, true, out var minLevel))
minLevel = LogEventLevel.Information;
// 解析文件分割规则
if (!Enum.TryParse<RollingInterval>(config.RollingInterval, true, out var rollingInterval))
rollingInterval = RollingInterval.Day;
// 构建Serilog配置
var loggerConfig = new LoggerConfiguration()
.MinimumLevel.Is(minLevel)
.Enrich.FromLogContext() // 启用上下文(模块标记)
.WriteTo.File(
path: System.IO.Path.Combine(config.LogPath, "app_.log"), // 最终文件名:app_20260307.log
rollingInterval: rollingInterval,
fileSizeLimitBytes: config.FileSizeLimitMB * 1024 * 1024,
retainedFileCountLimit: config.RetainedFileCountLimit,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
);
// 调试环境输出到控制台
if (config.EnableConsole)
loggerConfig.WriteTo.Console(
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
// 注册实时日志查看器 Sink(接收所有级别日志)| Register real-time log viewer sink (receive all levels)
loggerConfig.WriteTo.Sink(RealTimeLogSink.Instance, Serilog.Events.LogEventLevel.Verbose);
// 全局初始化
Log.Logger = loggerConfig.CreateLogger();
}
}
}
@@ -0,0 +1,49 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Serilog.Core;
using Serilog.Events;
namespace XP.Common.Logging.ViewModels
{
/// <summary>
/// Serilog 自定义 Sink,将日志事件转发到 RealTimeLogViewerViewModel,并维护环形缓冲区
/// Custom Serilog Sink that forwards log events and maintains a ring buffer for history
/// </summary>
public class RealTimeLogSink : ILogEventSink
{
private const int BufferCapacity = 500;
private readonly ConcurrentQueue<LogEvent> _buffer = new();
/// <summary>
/// 全局单例实例 | Global singleton instance
/// </summary>
public static RealTimeLogSink Instance { get; } = new();
/// <summary>
/// 日志事件到达时触发 | Fired when a log event arrives
/// </summary>
public event Action<LogEvent>? LogEventReceived;
public void Emit(LogEvent logEvent)
{
// 入队缓冲区 | Enqueue to buffer
_buffer.Enqueue(logEvent);
while (_buffer.Count > BufferCapacity)
{
_buffer.TryDequeue(out _);
}
LogEventReceived?.Invoke(logEvent);
}
/// <summary>
/// 获取缓冲区中的历史日志快照 | Get a snapshot of buffered history logs
/// </summary>
public List<LogEvent> GetBufferedHistory()
{
return _buffer.ToList();
}
}
}
@@ -0,0 +1,329 @@
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; }
}
}
@@ -0,0 +1,148 @@
<Window x:Class="XP.Common.GeneralForm.Views.RealTimeLogViewer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
Title="{loc:Localization LogViewer_Title}"
Width="960" Height="600"
MinWidth="700" MinHeight="400"
WindowStartupLocation="CenterScreen"
ShowInTaskbar="True"
WindowStyle="SingleBorderWindow">
<Grid>
<Grid.RowDefinitions>
<!-- 顶部工具栏 | Top toolbar -->
<RowDefinition Height="Auto"/>
<!-- 级别筛选栏 | Level filter bar -->
<RowDefinition Height="Auto"/>
<!-- 日志显示区 | Log display area -->
<RowDefinition Height="*"/>
<!-- 底部状态栏 | Bottom status bar -->
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- === 顶部工具栏 | Top Toolbar === -->
<DockPanel Grid.Row="0" Margin="8,6" LastChildFill="True">
<!-- 左侧:自动滚动开关 + 清空按钮 | Left: Auto-scroll toggle + Clear button -->
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center">
<telerik:RadToggleButton x:Name="AutoScrollToggle"
telerik:StyleManager.Theme="Crystal"
IsChecked="{Binding IsAutoScroll, Mode=TwoWay}"
Content="{loc:Localization LogViewer_AutoScroll}"
Padding="10,4" Margin="0,0,6,0"/>
<telerik:RadButton telerik:StyleManager.Theme="Crystal"
Command="{Binding ClearCommand}"
Content="{loc:Localization LogViewer_ClearLog}"
Padding="10,4" Margin="0,0,12,0"/>
</StackPanel>
<!-- 右侧:过滤输入框 | Right: Filter input -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Text="{loc:Localization LogViewer_Filter}" VerticalAlignment="Center" Margin="0,0,4,0"/>
<telerik:RadWatermarkTextBox telerik:StyleManager.Theme="Crystal"
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, Delay=300}"
WatermarkContent="{loc:Localization LogViewer_FilterWatermark}"
Width="260" Padding="4,3"
VerticalAlignment="Center"/>
</StackPanel>
</DockPanel>
<!-- === 级别筛选栏 | Level Filter Bar === -->
<Border Grid.Row="1" Background="#FFF8F8F8" BorderBrush="#FFDDDDDD" BorderThickness="0,0,0,1" Padding="8,4">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{loc:Localization LogViewer_LevelFilter}" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="#FF666666" FontSize="12"/>
<CheckBox Content="Debug" IsChecked="{Binding ShowDebug}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Gray"/>
<CheckBox Content="Info" IsChecked="{Binding ShowInfo}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FF1E1E1E"/>
<CheckBox Content="Warning" IsChecked="{Binding ShowWarning}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFC88200"/>
<CheckBox Content="Error" IsChecked="{Binding ShowError}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Red"/>
<CheckBox Content="Fatal" IsChecked="{Binding ShowFatal}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFB40000"/>
</StackPanel>
</Border>
<!-- === 日志显示区(RadGridView| Log Display Area === -->
<telerik:RadGridView Grid.Row="2"
x:Name="LogGridView"
telerik:StyleManager.Theme="Crystal"
ItemsSource="{Binding FilteredEntries}"
AutoGenerateColumns="False"
IsReadOnly="True"
RowIndicatorVisibility="Collapsed"
ShowGroupPanel="False"
ShowColumnHeaders="True"
CanUserFreezeColumns="False"
CanUserReorderColumns="False"
CanUserSortColumns="False"
CanUserResizeColumns="True"
IsFilteringAllowed="False"
SelectionMode="Extended"
FontFamily="Consolas"
FontSize="12"
Margin="4,0,4,0">
<telerik:RadGridView.Columns>
<!-- 时间戳列 | Timestamp column -->
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColTime}"
DataMemberBinding="{Binding TimestampDisplay}"
Width="100"
IsReadOnly="True"/>
<!-- 级别列 | Level column -->
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColLevel}"
DataMemberBinding="{Binding Level}"
Width="60"
IsReadOnly="True">
<telerik:GridViewDataColumn.CellStyle>
<Style TargetType="telerik:GridViewCell">
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
</telerik:GridViewDataColumn.CellStyle>
</telerik:GridViewDataColumn>
<!-- 来源列 | Source column -->
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColSource}"
DataMemberBinding="{Binding Source}"
Width="200"
IsReadOnly="True">
<telerik:GridViewDataColumn.CellStyle>
<Style TargetType="telerik:GridViewCell">
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
</Style>
</telerik:GridViewDataColumn.CellStyle>
</telerik:GridViewDataColumn>
<!-- 消息列 | Message column -->
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColMessage}"
DataMemberBinding="{Binding Message}"
Width="*"
IsReadOnly="True">
<telerik:GridViewDataColumn.CellStyle>
<Style TargetType="telerik:GridViewCell">
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
</Style>
</telerik:GridViewDataColumn.CellStyle>
</telerik:GridViewDataColumn>
</telerik:RadGridView.Columns>
</telerik:RadGridView>
<!-- === 底部状态栏 | Bottom Status Bar === -->
<StatusBar Grid.Row="3" Background="#FFF0F0F0" BorderBrush="#FFCCCCCC" BorderThickness="0,1,0,0">
<StatusBarItem>
<TextBlock Text="{Binding StatusText}" Foreground="#FF666666" FontSize="12"/>
</StatusBarItem>
<Separator/>
<StatusBarItem HorizontalAlignment="Right">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{loc:Localization LogViewer_MaxLines}" Foreground="#FF999999" FontSize="11" VerticalAlignment="Center"/>
<telerik:RadNumericUpDown telerik:StyleManager.Theme="Crystal"
Value="{Binding MaxLines, Mode=TwoWay}"
Minimum="100" Maximum="10000"
NumberDecimalDigits="0"
SmallChange="500"
Width="90" Height="22"
VerticalAlignment="Center"/>
</StackPanel>
</StatusBarItem>
</StatusBar>
</Grid>
</Window>
@@ -0,0 +1,85 @@
using System;
using System.Windows;
using Serilog.Events;
using XP.Common.Logging.ViewModels;
namespace XP.Common.GeneralForm.Views
{
/// <summary>
/// 实时日志查看器窗口,订阅 Serilog 事件并实时显示
/// Real-time log viewer window, subscribes to Serilog events and displays in real-time
/// </summary>
public partial class RealTimeLogViewer : Window
{
private readonly RealTimeLogViewerViewModel _viewModel;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
/// <param name="maxLines">最大行数限制,默认 2000 | Max line count, default 2000</param>
public RealTimeLogViewer(int maxLines = 2000)
{
_viewModel = new RealTimeLogViewerViewModel { MaxLines = maxLines };
DataContext = _viewModel;
InitializeComponent();
// 继承主窗口图标 | Inherit main window icon
if (Application.Current?.MainWindow != null)
{
Icon = Application.Current.MainWindow.Icon;
}
// 订阅自动滚动事件 | Subscribe to auto-scroll event
_viewModel.ScrollToBottomRequested += OnScrollToBottomRequested;
// 加载缓冲区中的历史日志 | Load buffered history logs
var history = RealTimeLogSink.Instance.GetBufferedHistory();
foreach (var logEvent in history)
{
_viewModel.AddLogEvent(logEvent);
}
// 订阅 Serilog Sink 事件(在历史加载之后,避免重复)| Subscribe after history load
RealTimeLogSink.Instance.LogEventReceived += OnLogEventReceived;
Closed += OnWindowClosed;
}
/// <summary>
/// 接收 Serilog 日志事件 | Receive Serilog log event
/// </summary>
private void OnLogEventReceived(LogEvent logEvent)
{
_viewModel.AddLogEvent(logEvent);
}
/// <summary>
/// 自动滚动到底部 | Auto-scroll to bottom
/// </summary>
private void OnScrollToBottomRequested()
{
try
{
if (LogGridView.Items.Count > 0)
{
var lastItem = LogGridView.Items[LogGridView.Items.Count - 1];
LogGridView.ScrollIntoView(lastItem);
}
}
catch
{
// 滚动失败时静默处理 | Silently handle scroll failures
}
}
/// <summary>
/// 窗口关闭时取消订阅,防止内存泄漏 | Unsubscribe on close to prevent memory leaks
/// </summary>
private void OnWindowClosed(object? sender, EventArgs e)
{
_viewModel.ScrollToBottomRequested -= OnScrollToBottomRequested;
RealTimeLogSink.Instance.LogEventReceived -= OnLogEventReceived;
}
}
}