将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -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
@@ -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
|
||||
@@ -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 连接...
|
||||
```
|
||||
|
||||
## 示例 2:ViewModel 中使用 | 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 |
|
||||
@@ -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
|
||||
// 方式 1:ForModule<T>() - 完整类型名
|
||||
// Method 1: ForModule<T>() - Full type name
|
||||
_logger = logger.ForModule<PlcService>();
|
||||
// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
|
||||
|
||||
// 方式 2:ForModule("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>
|
||||
```
|
||||
@@ -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` 的订阅,防止内存泄漏。
|
||||
Reference in New Issue
Block a user