将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
+183
View File
@@ -0,0 +1,183 @@
# Dump 文件管理服务使用指南 | Dump File Management Service Usage Guide
## 概述 | Overview
XplorePlane 提供 Dump 文件管理功能,用于在应用程序崩溃或需要诊断时生成进程转储文件。支持三种触发方式:崩溃自动触发、定时触发和手动触发,并提供文件大小限制、自动清理和可配置存储路径等管理能力。
Dump 功能通过 Windows `MiniDumpWriteDump` API 实现,作为 `XP.Common` 的子模块集成到 `CommonModule` 中。
## 基本用法 | Basic Usage
### 通过依赖注入获取服务 | Get Service via DI
```csharp
public class DiagnosticsService
{
private readonly IDumpService _dumpService;
private readonly ILoggerService _logger;
public DiagnosticsService(IDumpService dumpService, ILoggerService logger)
{
_dumpService = dumpService ?? throw new ArgumentNullException(nameof(dumpService));
_logger = logger?.ForModule<DiagnosticsService>() ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 手动生成 Mini Dump | Manually generate Mini Dump
/// </summary>
public void CaptureMiniDump()
{
var filePath = _dumpService.CreateMiniDump();
if (filePath != null)
{
_logger.Info("Mini Dump 已生成:{FilePath} | Mini Dump generated: {FilePath}", filePath);
}
}
/// <summary>
/// 手动生成 Full Dump(包含完整内存)| Manually generate Full Dump (full memory)
/// </summary>
public void CaptureFullDump()
{
var filePath = _dumpService.CreateFullDump();
if (filePath != null)
{
_logger.Info("Full Dump 已生成:{FilePath} | Full Dump generated: {FilePath}", filePath);
}
}
}
```
## 触发方式 | Trigger Modes
### 1. 崩溃自动触发 | Crash Auto Trigger
服务启动后自动订阅 `AppDomain.CurrentDomain.UnhandledException``TaskScheduler.UnobservedTaskException`,无需额外配置。崩溃时自动生成 Mini Dump。
### 2. 定时触发 | Scheduled Trigger
`App.config` 中启用定时触发后,服务按配置的时间间隔周期性生成 Mini Dump:
```xml
<add key="Dump:EnableScheduledDump" value="true" />
<add key="Dump:ScheduledIntervalMinutes" value="60" />
```
### 3. 手动触发 | Manual Trigger
通过 `IDumpService` 接口的方法手动触发:
```csharp
// 生成 Mini Dump(线程信息 + 数据段 + 句柄信息)
// Generate Mini Dump (thread info + data segments + handle data)
string? miniPath = _dumpService.CreateMiniDump();
// 生成 Full Dump(完整内存,仅手动触发允许)
// Generate Full Dump (full memory, manual trigger only)
string? fullPath = _dumpService.CreateFullDump();
```
## Dump 类型说明 | Dump Type Description
| 类型 | 包含内容 | 文件大小 | 触发限制 |
|------|----------|----------|----------|
| Mini Dump | 线程信息、数据段、句柄信息 | 较小(受大小限制约束) | 所有触发方式 |
| Full Dump | 进程完整内存 | 较大(无大小限制) | 仅手动触发 |
> 非手动触发(崩溃、定时)请求 Full Dump 时,系统会自动降级为 Mini Dump。
## 文件命名规则 | File Naming Convention
格式:`XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp`
示例:
- `XplorePlane_20260317_1530_Crash.dmp` — 崩溃触发
- `XplorePlane_20260317_1600_Scheduled.dmp` — 定时触发
- `XplorePlane_20260317_1645_Manual.dmp` — 手动触发
## 自动清理 | Auto Cleanup
- 服务启动时立即执行一次清理
- 运行期间每 24 小时执行一次清理
- 超过保留天数(默认 7 天)的 `.dmp` 文件会被自动删除
- 单个文件删除失败不影响其余文件的清理
## 配置 | Configuration
`App.config``<appSettings>` 中配置:
```xml
<appSettings>
<!-- Dump 文件存储路径 | Dump file storage path -->
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
<!-- 是否启用定时触发 | Enable scheduled trigger -->
<add key="Dump:EnableScheduledDump" value="false" />
<!-- 定时触发间隔(分钟)| Scheduled trigger interval (minutes) -->
<add key="Dump:ScheduledIntervalMinutes" value="60" />
<!-- Mini Dump 文件大小上限(MB| Mini Dump file size limit (MB) -->
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
<!-- 文件保留天数 | File retention days -->
<add key="Dump:RetentionDays" value="7" />
</appSettings>
```
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `Dump:StoragePath` | `D:\XplorePlane\Dump` | Dump 文件存储路径 |
| `Dump:EnableScheduledDump` | `false` | 是否启用定时触发 |
| `Dump:ScheduledIntervalMinutes` | `60` | 定时触发间隔(分钟) |
| `Dump:MiniDumpSizeLimitMB` | `100` | Mini Dump 文件大小上限(MB),超过则删除 |
| `Dump:RetentionDays` | `7` | 文件保留天数,超过则自动清理 |
## 错误处理 | Error Handling
Dump 功能遵循"记录并继续"原则,自身错误不影响主应用程序运行:
- Dump 文件写入失败 → 记录错误日志,返回 `null`,不抛出异常
- Mini Dump 超过大小限制 → 删除文件,记录警告
- 存储目录创建失败 → 回退到默认路径 `D:\XplorePlane\Dump`
- 清理过程中文件删除失败 → 记录错误,继续清理其余文件
## IDumpService 接口 | IDumpService Interface
```csharp
public interface IDumpService : IDisposable
{
/// <summary>
/// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
/// </summary>
string? CreateMiniDump();
/// <summary>
/// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
/// </summary>
string? CreateFullDump();
/// <summary>
/// 启动服务 | Start service
/// </summary>
void Start();
/// <summary>
/// 停止服务 | Stop service
/// </summary>
void Stop();
}
```
## 文件结构 | File Structure
```
XP.Common/Dump/
├── Interfaces/
│ └── IDumpService.cs # 服务接口 | Service interface
├── Implementations/
│ ├── DumpService.cs # 服务实现 | Service implementation
│ └── DumpCleaner.cs # 自动清理组件 | Auto cleanup component
├── Configs/
│ ├── DumpConfig.cs # 配置实体 | Config entity
│ └── DumpTriggerType.cs # 触发类型枚举 | Trigger type enum
└── Native/
├── NativeMethods.cs # P/Invoke 声明 | P/Invoke declarations
└── MiniDumpType.cs # Dump 类型标志枚举 | Dump type flags enum
```
+247
View File
@@ -0,0 +1,247 @@
# 通用窗体使用指南 | General Form Usage Guide
## 概述 | Overview
`XP.Common.GeneralForm` 提供 XplorePlane 项目中可复用的通用 WPF 窗体组件。当前包含以下窗体:
| 窗体 | 说明 |
|---|---|
| `ProgressWindow` | 模态进度条窗口,支持线程安全的进度更新和关闭操作 |
| `InputDialog` | 通用输入对话框,支持单行文本输入、可选验证和多语言按钮 |
## 目录结构 | Directory Structure
```
XP.Common/GeneralForm/
├── ViewModels/
│ ├── InputDialogViewModel.cs # 输入对话框 ViewModel
│ └── ProgressWindowViewModel.cs # 进度窗口 ViewModel
└── Views/
├── InputDialog.xaml # 输入对话框 XAML 视图
├── InputDialog.xaml.cs # 输入对话框 Code-Behind
├── ProgressWindow.xaml # 进度窗口 XAML 视图
└── ProgressWindow.xaml.cs # 进度窗口 Code-Behind
```
---
## ProgressWindow 进度条窗口
### 功能特性 | Features
- 模态进度条窗口,居中显示,不可调整大小
- 线程安全:`UpdateProgress()``Close()` 可从任意线程调用,内部自动通过 `Dispatcher` 调度
- 可配置是否允许用户手动关闭窗口(`isCancelable` 参数)
- 不可取消时,通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
- 进度值自动 Clamp 到 `[0, 100]` 范围,超出范围时记录 Warn 日志
- 自动继承主窗口图标
- 使用 Telerik `RadProgressBar` 控件(Crystal 主题)
### 构造函数参数 | Constructor Parameters
```csharp
public ProgressWindow(
string title = "操作进行中", // 窗口标题
string message = "请稍候...", // 提示信息
bool isCancelable = true, // 是否允许用户关闭窗口
ILoggerService? logger = null // 日志服务(可选)
)
```
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `title` | `string` | `"操作进行中"` | 窗口标题栏文本 |
| `message` | `string` | `"请稍候..."` | 进度条上方的提示信息 |
| `isCancelable` | `bool` | `true` | `true`:用户可手动关闭;`false`:禁用关闭按钮 |
| `logger` | `ILoggerService?` | `null` | 传入日志服务后自动记录窗口生命周期日志 |
### 公开方法 | Public Methods
#### UpdateProgress - 更新进度
```csharp
// 线程安全,可从任意线程调用
void UpdateProgress(string message, double progress)
```
- `message`:更新提示信息文本
- `progress`:进度值(0-100),超出范围自动修正
#### Close - 关闭窗口
```csharp
// 线程安全,可从任意线程调用(隐藏基类 Window.Close()
new void Close()
```
### 基本用法 | Basic Usage
```csharp
using XP.Common.GeneralForm.Views;
using XP.Common.Logging.Interfaces;
public class SomeService
{
private readonly ILoggerService _logger;
public SomeService(ILoggerService logger)
{
_logger = logger?.ForModule<SomeService>()
?? throw new ArgumentNullException(nameof(logger));
}
public async Task ExecuteLongOperation()
{
// 创建进度窗口(不可取消)
var progressWindow = new ProgressWindow(
title: "数据处理中",
message: "正在初始化...",
isCancelable: false,
logger: _logger);
// 显示模态窗口(需在 UI 线程调用)
// 注意:ShowDialog() 会阻塞,通常配合 Task 使用
_ = Task.Run(async () =>
{
try
{
progressWindow.UpdateProgress("正在加载数据...", 20);
await Task.Delay(1000); // 模拟耗时操作
progressWindow.UpdateProgress("正在处理数据...", 60);
await Task.Delay(1000);
progressWindow.UpdateProgress("即将完成...", 90);
await Task.Delay(500);
progressWindow.UpdateProgress("完成", 100);
}
finally
{
// 关闭窗口(线程安全)
progressWindow.Close();
}
});
progressWindow.ShowDialog();
}
}
```
### 允许用户取消的用法 | Cancelable Usage
```csharp
// 创建可取消的进度窗口
var progressWindow = new ProgressWindow(
title: "文件导出",
message: "正在导出文件...",
isCancelable: true, // 用户可以点击关闭按钮取消
logger: _logger);
progressWindow.ShowDialog();
```
### ViewModel 绑定属性 | ViewModel Binding Properties
`ProgressWindowViewModel` 继承自 `BindableBase`,提供以下可绑定属性:
| 属性 | 类型 | 说明 |
|---|---|---|
| `Title` | `string` | 窗口标题(只读) |
| `Message` | `string` | 提示信息文本(可通知) |
| `Progress` | `double` | 进度值 0-100(可通知) |
| `ProgressText` | `string` | 百分比显示文本,如 `"75%"`(只读,自动计算) |
| `IsCancelable` | `bool` | 是否允许用户关闭(只读) |
### 注意事项 | Notes
1. `ShowDialog()` 必须在 UI 线程调用,它会阻塞当前线程直到窗口关闭
2. `UpdateProgress()``Close()` 内部已处理跨线程调度,可安全地从后台线程调用
3.`isCancelable = false` 时,窗口关闭按钮会被 Win32 API 禁用(灰色),用户无法通过 Alt+F4 或点击关闭
4. 进度值超出 `[0, 100]` 范围时会自动修正并记录 Warn 级别日志
5. `Close()` 使用 `new` 关键字隐藏基类方法(因 `Window.Close()` 非虚方法),确保通过 `ProgressWindow` 类型引用调用
---
## InputDialog 输入对话框
### 功能特性 | Features
- 模态输入对话框,居中于父窗口显示,不可调整大小
- 自动继承主窗口图标
- 按钮文本支持多语言(使用 `Button_OK` / `Button_Cancel` 资源键)
- 可选的输入验证委托(`Func<string, string?>`),验证失败时在输入框下方显示红色错误提示
- 输入内容变化时自动清除验证错误
- 使用 Telerik `RadWatermarkTextBox``RadButton` 控件(Crystal 主题)
- 提供静态 `Show()` 便捷方法,一行代码即可调用
### 静态方法 | Static Method
```csharp
public static string? Show(
string prompt, // 提示文本
string title, // 窗口标题
string defaultValue = "", // 默认值
Func<string, string?>? validate = null, // 验证委托(可选)
Window? owner = null // 父窗口(可选)
)
```
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `prompt` | `string` | 必填 | 输入框上方的提示文本 |
| `title` | `string` | 必填 | 窗口标题栏文本 |
| `defaultValue` | `string` | `""` | 输入框的初始值 |
| `validate` | `Func<string, string?>?` | `null` | 验证委托:返回 `null` 表示通过,返回错误信息则阻止确认 |
| `owner` | `Window?` | `null` | 父窗口,设置后对话框居中于父窗口 |
返回值:用户输入的字符串,取消时返回 `null`
### 基本用法 | Basic Usage
```csharp
using XP.Common.GeneralForm.Views;
// 最简用法,无验证
var name = InputDialog.Show("请输入名称:", "新建项目");
if (name == null) return; // 用户取消
// 带默认值
var port = InputDialog.Show("请输入端口号:", "配置", "8080");
```
### 带验证的用法 | Usage with Validation
```csharp
// 验证委托:返回 null 表示通过,返回错误信息则显示在输入框下方
var groupId = InputDialog.Show(
"请输入 Group ID",
"新增 Group",
validate: input =>
{
if (string.IsNullOrWhiteSpace(input))
return "ID 不能为空";
if (existingIds.Contains(input))
return "ID 已存在,请使用不同的 ID";
return null; // 验证通过
});
// 验证非负整数
var dbNumber = InputDialog.Show(
"请输入 DB 块号(非负整数):",
"新增 Group",
"0",
validate: input =>
{
if (!int.TryParse(input, out int val) || val < 0)
return "必须为非负整数";
return null;
});
```
### 注意事项 | Notes
1. `Show()` 必须在 UI 线程调用(内部使用 `ShowDialog()`
2. 验证委托在用户点击确定按钮时执行,验证失败不会关闭对话框
3. 用户修改输入内容时会自动清除上一次的验证错误提示
4. 按钮文本从 XP.Common 资源文件读取(`Button_OK` / `Button_Cancel`),自动跟随应用语言
File diff suppressed because it is too large Load Diff
+265
View File
@@ -0,0 +1,265 @@
# 多语言支持快速开始 | Localization Quick Start
## 概述 | Overview
XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
### 核心特性 | Key Features
- ✅ 基于 .NET Resx 资源文件,编译时类型安全
- ✅ 简洁的 XAML 标记扩展语法
- ✅ 完整的 ViewModel 集成支持
- ✅ 语言设置持久化到 App.config
- ✅ 跨模块事件通知机制
- ✅ 健壮的错误处理和回退机制
- ✅ 多资源源 Fallback Chain 机制,支持模块级资源注册
---
## 快速开始 | Quick Start
### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
#### 基础用法 | Basic Usage
```xml
<Window x:Class="XplorePlane.App.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
Title="{loc:Localization Key=App_Title}">
<Grid>
<!-- 按钮文本本地化 | Localized button text -->
<Button Content="{loc:Localization Key=Button_OK}" />
<!-- 标签文本本地化 | Localized label text -->
<Label Content="{loc:Localization Key=Settings_Language}" />
<!-- 菜单项本地化 | Localized menu item -->
<MenuItem Header="{loc:Localization Key=Menu_File}" />
</Grid>
</Window>
```
#### 命名空间声明 | Namespace Declaration
在 XAML 文件顶部添加命名空间引用:
```xml
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
```
#### 语法说明 | Syntax Explanation
- `{loc:Localization Key=ResourceKey}` - 完整语法
- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
- 资源键不存在时,显示键名本身(便于调试)
---
### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
适用于不方便依赖注入的场景(静态方法、工具类等)。
```csharp
using XP.Common.Localization;
// 基本用法 | Basic usage
var title = LocalizationHelper.Get("App_Title");
// 带格式化参数 | With format arguments
var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
```
- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock
- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
- 两者读取同一套 Resx 资源文件,结果一致
> **V1.4.1.1 变更:** `LocalizationHelper` 新增 `Initialize(ILocalizationService)` 方法。初始化后,`Get()` 会优先通过 `ILocalizationService` 获取字符串(支持 Fallback Chain);未初始化时仍兼容回退到原始 `ResourceManager`。建议在 `CommonModule` 或 App 启动时调用初始化。
```csharp
// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
LocalizationHelper.Initialize(localizationService);
```
---
### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
#### 依赖注入 | Dependency Injection
```csharp
using XplorePlane.Common.Localization.Interfaces;
using XplorePlane.Common.Localization.Enums;
using Prism.Mvvm;
namespace XplorePlane.App.ViewModels
{
public class MyViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
private readonly ILoggerService _logger;
public MyViewModel(
ILocalizationService localizationService,
ILoggerService logger)
{
_localizationService = localizationService;
_logger = logger;
}
// 获取本地化字符串 | Get localized string
public string GetWelcomeMessage()
{
return _localizationService.GetString("Welcome_Message");
}
// 获取当前语言 | Get current language
public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
}
}
```
#### 动态文本绑定 | Dynamic Text Binding
```csharp
public class StatusViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public StatusViewModel(ILocalizationService localizationService)
{
_localizationService = localizationService;
// 订阅语言切换事件 | Subscribe to language changed event
_localizationService.LanguageChanged += OnLanguageChanged;
// 初始化状态消息 | Initialize status message
UpdateStatusMessage();
}
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
// 语言切换时更新文本 | Update text when language changes
UpdateStatusMessage();
}
private void UpdateStatusMessage()
{
StatusMessage = _localizationService.GetString("Status_Ready");
}
}
```
#### 数据验证消息本地化 | Localized Validation Messages
```csharp
public class FormViewModel : BindableBase, IDataErrorInfo
{
private readonly ILocalizationService _localizationService;
private string _username;
public string Username
{
get => _username;
set => SetProperty(ref _username, value);
}
public string this[string columnName]
{
get
{
if (columnName == nameof(Username))
{
if (string.IsNullOrWhiteSpace(Username))
{
return _localizationService.GetString("Validation_UsernameRequired");
}
if (Username.Length < 3)
{
return _localizationService.GetString("Validation_UsernameTooShort");
}
}
return null;
}
}
public string Error => null;
}
```
---
### 4. 多资源源 Fallback Chain | Multi-Source Fallback Chain
V1.1 版本引入了多资源源 Fallback Chain 机制,允许各模块注册自己的 Resx 资源文件。查找资源键时,从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。
#### 架构说明 | Architecture
```
Fallback Chain(查找顺序从右到左):
[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
↑ 最高优先级
```
- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
- 后注册的模块优先级更高
- 单个资源源查找异常时自动跳过,继续遍历下一个
- 全部未找到时返回 key 本身并记录警告日志
#### 注册模块资源源 | Register Module Resource Source
在 Prism 模块的 `OnInitialized` 中注册:
```csharp
using System.Resources;
using XP.Common.Localization.Interfaces;
public class ScanModule : IModule
{
private readonly ILocalizationService _localizationService;
public ScanModule(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void OnInitialized(IContainerProvider containerProvider)
{
// 注册模块资源源到 Fallback Chain
var resourceManager = new ResourceManager(
"XP.Scan.Resources.Resources",
typeof(ScanModule).Assembly);
_localizationService.RegisterResourceSource("XP.Scan", resourceManager);
}
public void RegisterTypes(IContainerRegistry containerRegistry) { }
}
```
#### 注销模块资源源 | Unregister Module Resource Source
```csharp
// 注销指定资源源(不可注销默认的 "XP.Common"
_localizationService.UnregisterResourceSource("XP.Scan");
```
#### 注意事项 | Notes
- 资源源名称不可重复,重复注册会抛出 `InvalidOperationException`
- 注销 `"XP.Common"` 会抛出 `InvalidOperationException`
- 注销不存在的名称会静默忽略并记录警告日志
- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
---
**版本 | Version:** 1.1
**最后更新 | Last Updated:** 2026-04-01
+233
View File
@@ -0,0 +1,233 @@
# 日志服务使用示例 | Logger Service Usage Examples
## 示例 1:服务类中使用 | Example 1: Usage in Service Class
### 旧方式(手动传递字符串)| Old Way (Manual String)
```csharp
public class PlcService
{
private readonly ILoggerService _logger;
public PlcService(ILoggerService logger)
{
// 需要手动输入类名,容易出错
// Need to manually type class name, error-prone
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
}
}
// 日志输出 | Log output:
// [PlcService] 正在初始化 PLC 连接...
```
### 新方式(自动类型推断)| New Way (Auto Type Inference)
```csharp
public class PlcService
{
private readonly ILoggerService _logger;
public PlcService(ILoggerService logger)
{
// 自动获取完整类型名,重构安全
// Automatically get full type name, refactoring-safe
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
}
}
// 日志输出 | Log output:
// [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接...
```
## 示例 2ViewModel 中使用 | Example 2: Usage in ViewModel
```csharp
using Prism.Mvvm;
using XP.Common.Logging.Interfaces;
namespace XP.Hardware.RaySource.ViewModels
{
public class RaySourceOperateViewModel : BindableBase
{
private readonly ILoggerService _logger;
public RaySourceOperateViewModel(ILoggerService logger)
{
// 自动使用:XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
// Automatically uses: XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
_logger = logger?.ForModule<RaySourceOperateViewModel>() ?? throw new ArgumentNullException(nameof(logger));
_logger.Info("射线源操作视图模型已初始化 | Ray source operate view model initialized");
}
public void StartXRay()
{
_logger.Info("用户请求启动射线 | User requested to start X-ray");
// ... 业务逻辑 | business logic
}
}
}
```
## 示例 3:工厂类中使用 | Example 3: Usage in Factory Class
```csharp
using XP.Common.Logging.Interfaces;
using XP.Hardware.RaySource.Abstractions;
namespace XP.Hardware.RaySource.Factories
{
public class RaySourceFactory : IRaySourceFactory
{
private readonly ILoggerService _logger;
public RaySourceFactory(ILoggerService logger)
{
// 自动使用:XP.Hardware.RaySource.Factories.RaySourceFactory
// Automatically uses: XP.Hardware.RaySource.Factories.RaySourceFactory
_logger = logger?.ForModule<RaySourceFactory>() ?? throw new ArgumentNullException(nameof(logger));
}
public IXRaySource CreateRaySource(string deviceType)
{
_logger.Info("创建射线源实例:类型={DeviceType} | Creating ray source instance: type={DeviceType}", deviceType);
switch (deviceType)
{
case "Comet225":
return new Comet225RaySource(_logger);
default:
_logger.Error(null, "不支持的设备类型:{DeviceType} | Unsupported device type: {DeviceType}", deviceType);
throw new NotSupportedException($"不支持的设备类型:{deviceType}");
}
}
}
}
```
## 示例 4:静态方法中使用 | Example 4: Usage in Static Methods
```csharp
public class ConfigLoader
{
public static PlcConfig LoadConfig(ILoggerService logger)
{
// 静态方法中也可以使用泛型
// Can also use generics in static methods
var log = logger.ForModule<ConfigLoader>();
log.Info("正在加载 PLC 配置 | Loading PLC configuration");
try
{
// ... 加载逻辑 | loading logic
log.Info("PLC 配置加载成功 | PLC configuration loaded successfully");
return config;
}
catch (Exception ex)
{
log.Error(ex, "PLC 配置加载失败 | PLC configuration loading failed");
throw;
}
}
}
```
## 示例 5:嵌套类中使用 | Example 5: Usage in Nested Classes
```csharp
public class RaySourceService
{
private readonly ILoggerService _logger;
public RaySourceService(ILoggerService logger)
{
_logger = logger?.ForModule<RaySourceService>() ?? throw new ArgumentNullException(nameof(logger));
}
// 嵌套类 | Nested class
public class ConnectionManager
{
private readonly ILoggerService _logger;
public ConnectionManager(ILoggerService logger)
{
// 自动使用:XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
// Automatically uses: XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
_logger = logger?.ForModule<ConnectionManager>() ?? throw new ArgumentNullException(nameof(logger));
}
}
}
```
## 示例 6:混合使用 | Example 6: Mixed Usage
有时你可能想要自定义模块名以保持简洁:
```csharp
public class VeryLongNamespaceAndClassName
{
private readonly ILoggerService _logger;
public VeryLongNamespaceAndClassName(ILoggerService logger)
{
// 选项 1:使用完整类型名(详细但冗长)
// Option 1: Use full type name (detailed but verbose)
// _logger = logger?.ForModule<VeryLongNamespaceAndClassName>();
// 输出 | Output: [XP.Some.Very.Long.Namespace.VeryLongNamespaceAndClassName]
// 选项 2:使用简短自定义名(简洁但需手动维护)
// Option 2: Use short custom name (concise but needs manual maintenance)
_logger = logger?.ForModule("VeryLong") ?? throw new ArgumentNullException(nameof(logger));
// 输出 | Output: [VeryLong]
}
}
```
## 优势对比 | Advantages Comparison
### ForModule<T>() 的优势 | Advantages of ForModule<T>()
**重构安全**:重命名类时自动更新
**Refactoring-safe**: Automatically updates when renaming class
**无拼写错误**:编译器检查类型
**No typos**: Compiler checks type
**完整信息**:包含命名空间,便于定位
**Complete info**: Includes namespace, easy to locate
**智能提示**IDE 自动补全
**IntelliSense**: IDE auto-completion
### ForModule(string) 的优势 | Advantages of ForModule(string)
**简洁输出**:日志文件更易读
**Concise output**: Log files more readable
**自定义名称**:可以使用业务术语
**Custom names**: Can use business terms
**灵活性**:可以为不同场景使用不同名称
**Flexibility**: Can use different names for different scenarios
## 推荐使用场景 | Recommended Usage Scenarios
| 场景 | 推荐方式 | 原因 |
|------|---------|------|
| 服务类 | `ForModule<T>()` | 需要完整追踪 |
| ViewModel | `ForModule<T>()` | 需要完整追踪 |
| 工厂类 | `ForModule<T>()` | 需要完整追踪 |
| 简单工具类 | `ForModule("ToolName")` | 保持简洁 |
| 临时调试 | `ForModule("Debug")` | 快速定位 |
| 第三方集成 | `ForModule("ThirdParty.XXX")` | 明确标识 |
| Scenario | Recommended | Reason |
|----------|------------|--------|
| Service classes | `ForModule<T>()` | Need full tracing |
| ViewModels | `ForModule<T>()` | Need full tracing |
| Factory classes | `ForModule<T>()` | Need full tracing |
| Simple utility classes | `ForModule("ToolName")` | Keep concise |
| Temporary debugging | `ForModule("Debug")` | Quick location |
| Third-party integration | `ForModule("ThirdParty.XXX")` | Clear identification |
+177
View File
@@ -0,0 +1,177 @@
# 日志服务使用指南 | Logger Service Usage Guide
## 概述 | Overview
XplorePlane 使用 Serilog 作为底层日志框架,通过 `ILoggerService` 接口提供统一的日志服务。
## 基本用法 | Basic Usage
### 方式 1:自动类型推断(推荐)| Method 1: Auto Type Inference (Recommended)
使用泛型方法 `ForModule<T>()` 自动获取类型的完整名称(命名空间 + 类名):
```csharp
public class PlcService
{
private readonly ILoggerService _logger;
public PlcService(ILoggerService logger)
{
// 自动使用 "XP.Hardware.Plc.Services.PlcService" 作为模块名
// Automatically uses "XP.Hardware.Plc.Services.PlcService" as module name
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
}
public void DoSomething()
{
_logger.Info("执行操作 | Performing operation");
}
}
```
### 方式 2:手动指定模块名 | Method 2: Manual Module Name
如果需要自定义模块名,可以使用字符串参数:
```csharp
public class PlcService
{
private readonly ILoggerService _logger;
public PlcService(ILoggerService logger)
{
// 手动指定简短的模块名
// Manually specify a short module name
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
}
}
```
### 方式 3:使用 typeof 获取类型名 | Method 3: Using typeof for Type Name
在静态方法或无法使用泛型的场景:
```csharp
public class PlcService
{
private readonly ILoggerService _logger;
public PlcService(ILoggerService logger)
{
// 使用 typeof 获取类型
// Use typeof to get type
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
}
public static void StaticMethod(ILoggerService logger)
{
// 静态方法中也可以使用泛型
// Can also use generics in static methods
var log = logger.ForModule<PlcService>();
log.Info("静态方法日志 | Static method log");
}
}
```
## 日志级别 | Log Levels
```csharp
// 调试信息(开发环境)| Debug information (development environment)
_logger.Debug("调试信息:变量值={Value} | Debug info: variable value={Value}", someValue);
// 一般信息 | General information
_logger.Info("操作成功 | Operation successful");
// 警告信息 | Warning information
_logger.Warn("连接不稳定 | Connection unstable");
// 错误信息(带异常)| Error information (with exception)
_logger.Error(ex, "操作失败:{Message} | Operation failed: {Message}", ex.Message);
// 致命错误 | Fatal error
_logger.Fatal(ex, "系统崩溃 | System crash");
```
## 日志输出格式 | Log Output Format
使用 `ForModule<T>()` 后,日志会自动包含完整的类型信息:
```
2026-03-12 10:30:15.123 [INF] [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接... | Initializing PLC connection...
2026-03-12 10:30:16.456 [INF] [XP.Hardware.Plc.Services.PlcService] PLC 连接成功 | PLC connection successful
```
## 最佳实践 | Best Practices
### 1. 在构造函数中初始化日志器 | Initialize Logger in Constructor
```csharp
public class MyService
{
private readonly ILoggerService _logger;
public MyService(ILoggerService logger)
{
_logger = logger?.ForModule<MyService>() ?? throw new ArgumentNullException(nameof(logger));
}
}
```
### 2. 使用结构化日志 | Use Structured Logging
```csharp
// 好的做法:使用占位符 | Good: use placeholders
_logger.Info("用户 {UserId} 执行了操作 {Action} | User {UserId} performed action {Action}", userId, action);
// 不好的做法:字符串拼接 | Bad: string concatenation
_logger.Info($"用户 {userId} 执行了操作 {action}");
```
### 3. 异常日志包含上下文 | Exception Logs Include Context
```csharp
try
{
await _plcClient.ConnectAsync(config);
}
catch (PlcException ex)
{
_logger.Error(ex, "PLC 连接失败:地址={Address}, 端口={Port} | PLC connection failed: address={Address}, port={Port}",
config.Address, config.Port);
throw;
}
```
## 对比:三种方式的输出 | Comparison: Output of Three Methods
```csharp
// 方式 1ForModule<T>() - 完整类型名
// Method 1: ForModule<T>() - Full type name
_logger = logger.ForModule<PlcService>();
// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
// 方式 2ForModule("PlcService") - 自定义名称
// Method 2: ForModule("PlcService") - Custom name
_logger = logger.ForModule("PlcService");
// 输出 | Output: [PlcService]
// 方式 3:不调用 ForModule - 无模块标记
// Method 3: Don't call ForModule - No module tag
_logger = logger;
// 输出 | Output: [] (空标记 | empty tag)
```
## 配置 | Configuration
日志配置在 `App.config` 中设置,通过 `SerilogConfig` 加载:
```xml
<appSettings>
<add key="Serilog:LogPath" value="C:\Logs\XplorePlane" />
<add key="Serilog:MinimumLevel" value="Information" />
<add key="Serilog:EnableConsole" value="true" />
<add key="Serilog:RollingInterval" value="Day" />
<add key="Serilog:FileSizeLimitMB" value="100" />
<add key="Serilog:RetainedFileCountLimit" value="30" />
</appSettings>
```
+299
View File
@@ -0,0 +1,299 @@
# PDF 查看与打印模块使用指南 | PDF Viewer & Printer Module Usage Guide
## 概述 | Overview
`XP.Common.PdfViewer` 提供基于 Telerik RadPdfViewer 的 PDF 文件查看与打印功能模块。模块作为 XP.Common 的通用可复用组件,通过 Prism DI 容器注册服务接口,供外部类库通过构造函数注入使用。
### 核心功能 | Core Features
- PDF 文件加载与显示(支持文件路径和文件流两种方式)
- 内置 RadPdfViewerToolbar 提供页面导航、缩放、旋转、打印等完整工具栏 UI
- 静默打印(指定打印机、页面范围、打印份数)
- 打印设置对话框(用户交互式配置打印参数)
- 打印预览功能
- 多语言支持(简体中文、繁体中文、英文)
- 结构化日志记录(使用 ILoggerService
- 资源自动释放(IDisposable + 终结器安全网)
---
## 目录结构 | Directory Structure
```
XP.Common/PdfViewer/
├── Exceptions/ # 自定义异常
│ ├── PdfLoadException.cs # PDF 加载异常
│ ├── PrinterNotFoundException.cs # 打印机未找到异常
│ └── PrintException.cs # 打印异常
├── Interfaces/ # 服务接口
│ ├── IPdfViewerService.cs # PDF 查看服务接口
│ └── IPdfPrintService.cs # PDF 打印服务接口
├── Implementations/ # 服务实现
│ ├── PdfViewerService.cs # PdfViewerService 实现
│ └── PdfPrintService.cs # PdfPrintService 实现
├── ViewModels/ # ViewModel
│ └── PdfViewerWindowViewModel.cs # 阅读器窗口 ViewModel
└── Views/ # 视图
├── PdfViewerWindow.xaml # 阅读器窗口 XAML
└── PdfViewerWindow.xaml.cs # 阅读器窗口 Code-Behind
```
---
## 服务接口 | Service Interfaces
### IPdfViewerService - PDF 查看服务
负责 PDF 文件加载和阅读器窗口管理。
```csharp
public interface IPdfViewerService : IDisposable
{
/// 通过文件路径打开 PDF 阅读器窗口
void OpenViewer(string filePath);
/// 通过文件流打开 PDF 阅读器窗口
void OpenViewer(Stream stream, string? title = null);
}
```
### IPdfPrintService - PDF 打印服务
负责 PDF 打印功能,包括静默打印和交互式打印。
```csharp
public interface IPdfPrintService
{
/// 使用指定打印机打印 PDF 文件
void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
/// 打开打印设置对话框并打印
bool PrintWithDialog(string filePath);
/// 打开打印预览对话框
void PrintPreview(string filePath);
}
```
---
## 使用示例 | Usage Examples
### 1. 通过文件路径打开 PDF
```csharp
using XP.Common.PdfViewer.Interfaces;
public class MyService
{
private readonly IPdfViewerService _pdfViewerService;
public MyService(IPdfViewerService pdfViewerService)
{
_pdfViewerService = pdfViewerService;
}
public void OpenPdfByPath()
{
// 打开指定路径的 PDF 文件
_pdfViewerService.OpenViewer(@"C:\Documents\UserManual.pdf");
}
}
```
### 2. 通过文件流打开 PDF
```csharp
public void OpenPdfByStream()
{
// 从文件流打开 PDF(窗口标题可选)
using var stream = File.OpenRead(@"C:\Documents\UserManual.pdf");
_pdfViewerService.OpenViewer(stream, "用户手册.pdf");
}
```
### 3. 静默打印到指定打印机
```csharp
using XP.Common.PdfViewer.Interfaces;
public class MyService
{
private readonly IPdfPrintService _printService;
public MyService(IPdfPrintService printService)
{
_printService = printService;
}
public void PrintPdf()
{
// 打印全部页面到指定打印机
_printService.Print(
filePath: @"C:\Documents\UserManual.pdf",
printerName: "HP LaserJet Pro",
pageFrom: null, // null 表示从第一页
pageTo: null, // null 表示到最后一页
copies: 1 // 打印 1 份
);
// 打印指定范围(第 1-3 页)
_printService.Print(
filePath: @"C:\Documents\UserManual.pdf",
printerName: "HP LaserJet Pro",
pageFrom: 1,
pageTo: 3,
copies: 2
);
}
}
```
### 4. 打开打印设置对话框
```csharp
public void OpenPrintDialog()
{
// 显示打印设置对话框,用户确认后打印
bool userConfirmed = _printService.PrintWithDialog(@"C:\Documents\UserManual.pdf");
if (userConfirmed)
{
// 用户点击了"确定"按钮
}
else
{
// 用户点击了"取消"按钮
}
}
```
### 5. 打开打印预览
```csharp
public void ShowPrintPreview()
{
// 打开打印预览对话框
_printService.PrintPreview(@"C:\Documents\UserManual.pdf");
}
```
---
## DI 注册 | DI Registration
`CommonModule.RegisterTypes` 中已注册为单例服务:
```csharp
containerRegistry.RegisterSingleton<IPdfPrintService, PdfPrintService>();
containerRegistry.RegisterSingleton<IPdfViewerService, PdfViewerService>();
```
在 ViewModel 或 Service 中通过构造函数注入使用:
```csharp
public class MyViewModel
{
private readonly IPdfViewerService _pdfViewerService;
private readonly IPdfPrintService _printService;
public MyViewModel(
IPdfViewerService pdfViewerService,
IPdfPrintService printService)
{
_pdfViewerService = pdfViewerService;
_printService = printService;
}
}
```
---
## 多语言资源 | Localization Resources
PDF 模块支持多语言,资源键如下:
| 资源键 | zh-CN | zh-TW | en-US |
|--------|-------|-------|-------|
| `PdfViewer_Title` | PDF 阅读器 | PDF 閱讀器 | PDF Viewer |
| `PdfViewer_TitleWithFile` | PDF 阅读器 - {0} | PDF 閱讀器 - {0} | PDF Viewer - {0} |
| `PdfViewer_LoadSuccess` | PDF 文件加载成功:{0}({1} 页)| PDF 檔案載入成功:{0}{1} 頁)| PDF loaded: {0} ({1} pages) |
| `PdfViewer_LoadFailed` | PDF 文件加载失败 | PDF 檔案載入失敗 | PDF file load failed |
| `PdfViewer_PrintSuccess` | 打印任务已提交:{0} → {1} | 列印任務已提交:{0} → {1} | Print job submitted: {0} → {1} |
| `PdfViewer_PrintFailed` | 打印失败 | 列印失敗 | Print failed |
| `PdfViewer_PrinterNotFound` | 打印机未找到:{0} | 印表機未找到:{0} | Printer not found: {0} |
---
## 异常处理 | Exception Handling
| 异常类型 | 触发条件 | 说明 |
|---------|---------|------|
| `FileNotFoundException` | 文件路径不存在 | `OpenViewer(filePath)``Print(filePath, ...)` |
| `ArgumentNullException` | 流参数为 null | `OpenViewer(null, ...)` |
| `PdfLoadException` | PDF 格式无效或加载失败 | 文件损坏、非 PDF 格式等 |
| `PrinterNotFoundException` | 指定打印机不存在 | `Print(filePath, printerName, ...)` |
| `PrintException` | 打印过程中发生错误 | 打印机错误、驱动问题等 |
---
## 注意事项 | Notes
1. **RadPdfViewerToolbar 内置功能**:页面导航(首页/上一页/下一页/末页)、缩放(放大/缩小/适合宽度/适合整页)、旋转(顺时针/逆时针)等功能由 RadPdfViewerToolbar 自动提供,无需手动实现。
2. **资源释放**`PdfViewerService` 实现 `IDisposable`,窗口闭时会自动释放 PDF 文档资源。终结器作为安全网,确保未显式释放时也能清理资源。
3. **多语言支持**RadPdfViewerToolbar 的内置按钮文本(如"首页"、"上一页"、"放大"等)由 Telerik 自身的本地化机制管理,无需在 XP.Common 的 Resources 中维护。
4. **打印设置**Telerik 提供内置的 `PrintSettings` 类,无需自定义打印设置模型。
5. **日志记录**:所有关键操作(加载成功/失败、打印成功/失败)都会通过 `ILoggerService` 记录结构化日志。
---
## 典型应用场景 | Typical Use Cases
### 场景 1:主窗口添加"用户手册"按钮
```csharp
// MainWindowViewModel.cs
private void ExecuteOpenUserManual()
{
var manualPath = ConfigurationManager.AppSettings["UserManual"];
var stream = File.OpenRead(manualPath);
var fileName = Path.GetFileName(manualPath);
_pdfViewerService.OpenViewer(stream, fileName);
}
```
### 场景 2:导出报告后自动打开 PDF 预览
```csharp
public void ExportAndPreview()
{
// 生成 PDF 报告到临时文件
var tempPath = Path.Combine(Path.GetTempPath(), $"Report_{DateTime.Now:yyyyMMdd_HHmmss}.pdf");
GenerateReport(tempPath);
// 自动打开 PDF 预览
_pdfViewerService.OpenViewer(tempPath);
}
```
### 场景 3:批量打印检测报告
```csharp
public void BatchPrintReports(List<string> reportPaths, string printerName)
{
foreach (var path in reportPaths)
{
_printService.Print(
filePath: path,
printerName: printerName,
pageFrom: 1,
pageTo: null,
copies: 1
);
}
}
```
@@ -0,0 +1,50 @@
# 实时日志查看器设计文档
#### 1. 核心目标
构建一个模态或非模态的 WPF 窗口,用于实时订阅 Serilog 事件,根据预定义的格式(如包含 `@l` 级别标记)自动渲染颜色,并提供过滤与自动滚动控制。
---
#### 2. 功能模块规划
为了保证代码的可维护性和复用性,建议采用 MVVM 模式,参考 `XP.Common.GeneralForm` 的目录结构进行扩展:
text
编辑
```
XP.Common/
└── GeneralForm/
└── Views/
├── RealTimeLogViewer.xaml // 视图:RichTextBox + 控制栏
└── RealTimeLogViewer.xaml.cs
└── ViewModels/
└── RealTimeLogViewerViewModel.cs // 核心逻辑:处理颜色标记、过滤、滚动
```
#### 3. 详细功能规格
| 功能模块 | 详细描述 | 交互逻辑 |
| ------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| 颜色渲染引擎 | 基于 Serilog 标记<br>解析日志事件中的 Level 或 Message Template。 | * Error/Fatal: 红色/深红色<br>* Warning: 橙色/黄色<br>* Info: 默认色<br>* Debug: 浅灰色<br>利用 `RichTextBox``TextRange` 动态追加。 |
| 动态过滤器 | 关键词黑白名单 | * 包含: 仅显示包含关键字的行。<br>* 排除: 隐藏包含关键字的行。<br>* 重置: 恢复显示所有。 |
| 智能滚动 | 跟随开关 | * 开启: 新日志到达时,滚动条自动到底。<br>* 关闭: 用户可自由浏览历史,新日志仅在后台缓存/计数。 |
| 行数限制 | 内存保护机制 | * 配置: 默认 2000 行,可调。<br>* 清理: 达到上限后,自动移除最旧的文本段落(`Paragraph`)。 |
| 日志源接入 | 线程安全订阅 | 使用 `IObserver<LogEvent>``IObservable`,内部通过 `Dispatcher` 安全更新。 |
#### 4. 视觉交互 (UI/UX)
- **布局**
- **顶部工具栏**:包含“自动滚动”开关(带图标)、“清空日志”按钮、过滤输入框。
- **中部显示区**`RichTextBox` (只读),启用垂直滚动条。
- **底部状态栏**:显示当前总日志条数、过滤后的条数。
- **性能优化**
- 由于 WPF `RichTextBox` 在大量文本下性能较差,建议增加**最大行数限制**(如默认保留 4000 行,超出自动删除顶部旧日志)。
#### 6. 开发建议
1. **颜色解析**:如果 Serilog 输出的是纯文本(如 `[INF] User logged in`),你需要编写正则表达式来匹配 `[INF]``[ERR]` 等前缀并着色。
2. **线程安全**:务必参考 `ProgressWindow` 中 `UpdateProgress` 的实现,使用 `Dispatcher.InvokeAsync` 或 `Dispatcher.CheckAccess` 来确保从后台线程(如 Serilog 的异步写入线程)更新 UI 时不会崩溃。
3. **资源占用**:考虑到这是一个“基础设施”类库,建议在窗口关闭时(`OnClosed` 事件)取消对 `IObservable` 的订阅,防止内存泄漏。