Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c83fc72e4 | ||
|
|
dd0681ba90 | ||
|
|
4ec76b7855 |
No files matched your search
@@ -0,0 +1,194 @@
|
||||
# 01 - 最小 WPF 应用
|
||||
|
||||
这是一个最简单的 Sharp WPF 应用示例,演示了框架的基础用法。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 了解 Sharp 框架的基本结构
|
||||
- 掌握 MVVM 模式(ViewModelBase)
|
||||
- 使用 Commands(RelayCommand, AsyncCommand)
|
||||
- 使用对话框服务(IDialogService)
|
||||
- 理解 Quick Start 配置方式
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Common** - Result<T>, 基础工具
|
||||
- **Sharp.Config** - 配置管理
|
||||
- **Sharp.Wpf** - MVVM 基础设施
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
01-MinimalWpfApp/
|
||||
├── App.xaml # 应用程序入口
|
||||
├── App.xaml.cs # Quick Start 配置
|
||||
├── MainWindow.xaml # 主窗口界面
|
||||
├── MainWindow.xaml.cs # 主窗口代码隐藏
|
||||
├── ViewModels/
|
||||
│ └── MainViewModel.cs # 主窗口 ViewModel
|
||||
├── appsettings.json # 配置文件
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/01-MinimalWpfApp
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 应用启动
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 一行添加基础服务(配置、日志、缓存)
|
||||
services.AddSharpEssentials();
|
||||
|
||||
// 一行添加 WPF 服务(对话框、调度器、导航、主题)
|
||||
services.AddSharpWpf();
|
||||
|
||||
// 注册 ViewModel 和 Window
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
// 构建服务提供者
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// 显示主窗口
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- `AddSharpEssentials()` - 自动配置 Config + Log + Cache
|
||||
- `AddSharpWpf()` - 自动注册 WPF 服务
|
||||
- 无需手动配置,使用约定优于配置
|
||||
|
||||
### 2. MainViewModel.cs - ViewModel 示例
|
||||
|
||||
```csharp
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private string _message = "Hello, Sharp!";
|
||||
private int _counter = 0;
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
public int Counter
|
||||
{
|
||||
get => _counter;
|
||||
set => SetProperty(ref _counter, value);
|
||||
}
|
||||
|
||||
// 同步命令
|
||||
public ICommand IncrementCommand { get; }
|
||||
|
||||
// 异步命令
|
||||
public ICommand ShowDialogCommand { get; }
|
||||
|
||||
public MainViewModel(IDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
|
||||
// RelayCommand - 同步操作
|
||||
IncrementCommand = new RelayCommand(() => {
|
||||
Counter++;
|
||||
Message = $"你点击了 {Counter} 次";
|
||||
});
|
||||
|
||||
// AsyncCommand - 异步操作
|
||||
ShowDialogCommand = new AsyncCommand(async (ct) => {
|
||||
IsBusy = true; // 显示加载状态
|
||||
|
||||
// 模拟耗时操作
|
||||
await Task.Delay(1000, ct);
|
||||
|
||||
_dialogService.ShowMessage($"当前计数: {Counter}", "提示");
|
||||
|
||||
IsBusy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 继承 `ViewModelBase` 获得 INotifyPropertyChanged
|
||||
- 使用 `SetProperty()` 自动触发属性变化通知
|
||||
- `IsBusy` 属性用于显示加载状态
|
||||
- `RelayCommand` 用于同步操作
|
||||
- `AsyncCommand` 用于异步操作(支持取消)
|
||||
|
||||
### 3. MainWindow.xaml - 界面绑定
|
||||
|
||||
```xml
|
||||
<Window ...>
|
||||
<Window.DataContext>
|
||||
<!-- ViewModel 会通过 DI 自动注入 -->
|
||||
</Window.DataContext>
|
||||
|
||||
<StackPanel Margin="20">
|
||||
<!-- 绑定属性 -->
|
||||
<TextBlock Text="{Binding Message}" FontSize="24"/>
|
||||
|
||||
<!-- 绑定命令 -->
|
||||
<Button Content="点击我" Command="{Binding IncrementCommand}"/>
|
||||
|
||||
<!-- 异步命令 -->
|
||||
<Button Content="显示对话框" Command="{Binding ShowDialogCommand}"/>
|
||||
|
||||
<!-- 显示加载状态 -->
|
||||
<TextBlock Text="加载中..." Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 使用 `{Binding}` 绑定 ViewModel 属性
|
||||
- Command 绑定到 ICommand 属性
|
||||
- 使用内置转换器(BoolToVisibilityConverter)
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加新属性
|
||||
在 MainViewModel 中添加一个 `Name` 属性,并在界面上显示。
|
||||
|
||||
### 练习2:添加清零按钮
|
||||
添加一个 `ResetCommand`,点击后将 `Counter` 重置为 0。
|
||||
|
||||
### 练习3:使用对话框确认
|
||||
在 ResetCommand 中,先弹出确认对话框,用户确认后再清零。
|
||||
|
||||
### 练习4:尝试 Result<T>
|
||||
修改 ShowDialogCommand,使用 `Result.Try()` 包装可能出错的代码。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [Sharp.Wpf README](../../src/Sharp/Wpf/README.md) - WPF 模块完整文档
|
||||
- [QUICKSTART.md](../../src/Sharp/QUICKSTART.md) - 快速启动指南
|
||||
- 下一个示例:[02-WpfWithDatabase](../02-WpfWithDatabase/README.md)
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 为什么 MainWindow 不需要手动设置 DataContext?**
|
||||
A: 通过 DI 容器获取 Window 时,会自动注入对应的 ViewModel。
|
||||
|
||||
**Q: IsBusy 是在哪里定义的?**
|
||||
A: `IsBusy` 是 `ViewModelBase` 的内置属性,用于显示加载状态。
|
||||
|
||||
**Q: 可以不用 DI 吗?**
|
||||
A: 可以,但不推荐。使用 DI 可以更好地管理依赖关系,便于测试。
|
||||
|
||||
---
|
||||
|
||||
**开始编码吧!** 🚀
|
||||
@@ -0,0 +1,233 @@
|
||||
# 03 - 表单验证示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的表单验证功能,包括 DataAnnotations 验证、实时错误提示和自定义验证。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `ValidatableViewModelBase` 实现表单验证
|
||||
- 掌握 DataAnnotations 验证特性
|
||||
- 实现实时验证反馈
|
||||
- 自定义验证规则
|
||||
- 显示验证错误消息
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - ValidatableViewModelBase, 验证支持
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/03-FormValidation
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. UserViewModel.cs - 带验证的 ViewModel
|
||||
|
||||
```csharp
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Wpf.Validation;
|
||||
|
||||
public class UserViewModel : ValidatableViewModelBase
|
||||
{
|
||||
private string _username = "";
|
||||
private string _email = "";
|
||||
private string _password = "";
|
||||
private string _confirmPassword = "";
|
||||
private int _age;
|
||||
|
||||
[Required(ErrorMessage = "用户名不能为空")]
|
||||
[StringLength(20, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-20个字符之间")]
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => SetProperty(ref _username, value, validate: true); // 启用验证
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "邮箱不能为空")]
|
||||
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value, validate: true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "密码不能为空")]
|
||||
[StringLength(50, MinimumLength = 6, ErrorMessage = "密码长度必须在6-50个字符之间")]
|
||||
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
|
||||
ErrorMessage = "密码必须包含大小写字母和数字")]
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _password, value, validate: true);
|
||||
// 密码变化时,重新验证确认密码
|
||||
ValidateProperty(ConfirmPassword, nameof(ConfirmPassword));
|
||||
}
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "请确认密码")]
|
||||
public string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _confirmPassword, value, validate: false);
|
||||
// 自定义验证:确认密码必须与密码一致
|
||||
if (value != Password)
|
||||
{
|
||||
AddError(nameof(ConfirmPassword), "两次密码输入不一致");
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearErrors(nameof(ConfirmPassword));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Range(18, 120, ErrorMessage = "年龄必须在18-120之间")]
|
||||
public int Age
|
||||
{
|
||||
get => _age;
|
||||
set => SetProperty(ref _age, value, validate: true);
|
||||
}
|
||||
|
||||
// 提交命令
|
||||
public ICommand SubmitCommand { get; }
|
||||
|
||||
public UserViewModel(IDialogService dialogService)
|
||||
{
|
||||
SubmitCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 验证所有属性
|
||||
if (ValidateAll())
|
||||
{
|
||||
dialogService.ShowMessage(
|
||||
$"注册成功!\n用户名: {Username}\n邮箱: {Email}\n年龄: {Age}",
|
||||
"成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
dialogService.ShowMessage("请修正表单错误后再提交", "验证失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. MainWindow.xaml - 验证错误显示
|
||||
|
||||
```xml
|
||||
<Window x:Class="FormValidationExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="表单验证示例" Height="500" Width="400">
|
||||
<Window.Resources>
|
||||
<!-- 错误模板 -->
|
||||
<ControlTemplate x:Key="ValidationErrorTemplate">
|
||||
<DockPanel>
|
||||
<Border BorderBrush="Red" BorderThickness="1" CornerRadius="2">
|
||||
<AdornedElementPlaceholder/>
|
||||
</Border>
|
||||
<TextBlock Foreground="Red" FontSize="12" Margin="5,0,0,0"
|
||||
Text="{Binding [0].ErrorContent}"
|
||||
DockPanel.Dock="Right"/>
|
||||
</DockPanel>
|
||||
</ControlTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<StackPanel Margin="20">
|
||||
<TextBlock Text="用户注册表单" FontSize="24" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<TextBlock Text="用户名:"/>
|
||||
<TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<TextBlock Text="邮箱:"/>
|
||||
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 密码 -->
|
||||
<TextBlock Text="密码:"/>
|
||||
<PasswordBox x:Name="PasswordBox" Margin="0,5,0,5"/>
|
||||
<TextBlock Text="* 密码必须包含大小写字母和数字" FontSize="10" Foreground="Gray" Margin="0,0,0,15"/>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<TextBlock Text="确认密码:"/>
|
||||
<PasswordBox x:Name="ConfirmPasswordBox" Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 年龄 -->
|
||||
<TextBlock Text="年龄:"/>
|
||||
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,20"/>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<Button Content="提交注册" Command="{Binding SubmitCommand}"
|
||||
Height="40" FontSize="16"/>
|
||||
|
||||
<!-- 验证状态 -->
|
||||
<TextBlock Margin="0,10,0,0" Foreground="Red"
|
||||
Text="表单包含错误,请修正后提交"
|
||||
Visibility="{Binding HasErrors, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 3. 关键点说明
|
||||
|
||||
**ValidatableViewModelBase 的关键特性**:
|
||||
1. **实时验证** - `SetProperty(ref field, value, validate: true)`
|
||||
2. **验证所有属性** - `ValidateAll()` 返回 bool
|
||||
3. **自定义错误** - `AddError(propertyName, errorMessage)`
|
||||
4. **清除错误** - `ClearErrors(propertyName)`
|
||||
5. **错误状态** - `HasErrors` 属性
|
||||
|
||||
**XAML 绑定要点**:
|
||||
1. **UpdateSourceTrigger=PropertyChanged** - 实时触发验证
|
||||
2. **ValidatesOnNotifyDataErrors=True** - 启用错误显示
|
||||
3. **Validation.ErrorTemplate** - 自定义错误样式
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多验证规则
|
||||
添加电话号码字段,使用正则表达式验证手机号格式。
|
||||
|
||||
### 练习2:异步验证
|
||||
实现用户名唯一性检查(模拟调用 API)。
|
||||
|
||||
### 练习3:条件验证
|
||||
当用户勾选"我是学生"时,年龄必须小于25岁。
|
||||
|
||||
### 练习4:自定义验证特性
|
||||
创建 `UniqueUsernameAttribute` 自定义验证特性。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: PasswordBox 无法绑定,怎么办?**
|
||||
A: PasswordBox 的 Password 属性不是依赖属性,不能直接绑定。可以使用附加属性或在代码隐藏中处理。
|
||||
|
||||
**Q: 为什么有时验证不触发?**
|
||||
A: 确保:
|
||||
1. 绑定使用了 `UpdateSourceTrigger=PropertyChanged`
|
||||
2. SetProperty 的 validate 参数设为 true
|
||||
3. XAML 中设置了 `ValidatesOnNotifyDataErrors=True`
|
||||
|
||||
**Q: 如何自定义错误消息样式?**
|
||||
A: 修改 `Validation.ErrorTemplate`,可以自定义边框颜色、图标等。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [DataAnnotations 验证特性](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations)
|
||||
- [INotifyDataErrorInfo 接口](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifydataerrorinfo)
|
||||
- 下一个示例:[04-Navigation](../04-Navigation/README.md)
|
||||
|
||||
---
|
||||
|
||||
**掌握表单验证,构建高质量的用户输入体验!** ✅
|
||||
@@ -0,0 +1,315 @@
|
||||
# 04 - 导航系统示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的导航服务实现多页面 WPF 应用,包括页面跳转、参数传递、前进/后退等功能。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `INavigationService` 实现页面导航
|
||||
- 理解导航历史栈(前进/后退)
|
||||
- 页面间参数传递
|
||||
- 导航生命周期钩子(INavigationAware)
|
||||
- 弹出式导航并获取结果
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - INavigationService, NavigationService
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/04-Navigation
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. MainViewModel.cs - 主导航容器
|
||||
|
||||
```csharp
|
||||
using Wpf.Navigation;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly INavigationService _navigationService;
|
||||
private ViewModelBase? _currentPage;
|
||||
|
||||
public ViewModelBase? CurrentPage
|
||||
{
|
||||
get => _currentPage;
|
||||
set => SetProperty(ref _currentPage, value);
|
||||
}
|
||||
|
||||
public bool CanGoBack => _navigationService.CanGoBack;
|
||||
public bool CanGoForward => _navigationService.CanGoForward;
|
||||
|
||||
public ICommand NavigateToHomeCommand { get; }
|
||||
public ICommand NavigateToProductsCommand { get; }
|
||||
public ICommand NavigateToSettingsCommand { get; }
|
||||
public ICommand GoBackCommand { get; }
|
||||
public ICommand GoForwardCommand { get; }
|
||||
|
||||
public MainViewModel(INavigationService navigationService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
|
||||
// 监听导航事件
|
||||
_navigationService.Navigated += (s, vm) =>
|
||||
{
|
||||
CurrentPage = vm;
|
||||
OnPropertyChanged(nameof(CanGoBack));
|
||||
OnPropertyChanged(nameof(CanGoForward));
|
||||
};
|
||||
|
||||
// 导航命令
|
||||
NavigateToHomeCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<HomeViewModel>();
|
||||
});
|
||||
|
||||
NavigateToProductsCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<ProductsViewModel>();
|
||||
});
|
||||
|
||||
NavigateToSettingsCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<SettingsViewModel>();
|
||||
});
|
||||
|
||||
GoBackCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.GoBackAsync();
|
||||
}, () => CanGoBack);
|
||||
|
||||
GoForwardCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.GoForwardAsync();
|
||||
}, () => CanGoForward);
|
||||
|
||||
// 初始导航到首页
|
||||
Task.Run(async () => await _navigationService.NavigateAsync<HomeViewModel>());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ProductsViewModel.cs - 带参数的页面
|
||||
|
||||
```csharp
|
||||
public class ProductsViewModel : ViewModelBase, INavigationAware
|
||||
{
|
||||
private readonly INavigationService _navigationService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private string _category = "全部";
|
||||
|
||||
public string Category
|
||||
{
|
||||
get => _category;
|
||||
set => SetProperty(ref _category, value);
|
||||
}
|
||||
|
||||
public ICommand ViewProductDetailsCommand { get; }
|
||||
|
||||
public ProductsViewModel(
|
||||
INavigationService navigationService,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
ViewProductDetailsCommand = new AsyncCommand<int>(async (productId, ct) =>
|
||||
{
|
||||
// 导航到产品详情页,传递产品ID
|
||||
await _navigationService.NavigateAsync<ProductDetailViewModel>(productId);
|
||||
});
|
||||
}
|
||||
|
||||
// 导航生命周期:进入页面时
|
||||
public async Task OnNavigatedToAsync(object? parameter)
|
||||
{
|
||||
if (parameter is string category)
|
||||
{
|
||||
Category = category;
|
||||
_dialogService.ShowMessage($"显示分类: {category}", "提示");
|
||||
}
|
||||
|
||||
// 可以在这里加载数据
|
||||
await LoadProductsAsync();
|
||||
}
|
||||
|
||||
// 导航生命周期:离开页面时
|
||||
public async Task OnNavigatedFromAsync()
|
||||
{
|
||||
// 清理资源、保存状态等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadProductsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
await Task.Delay(500); // 模拟加载
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ProductDetailViewModel.cs - 详情页(带结果返回)
|
||||
|
||||
```csharp
|
||||
public class ProductDetailViewModel : ViewModelBase,
|
||||
INavigationAware,
|
||||
INavigationResult<bool> // 返回 bool(是否购买)
|
||||
{
|
||||
private readonly TaskCompletionSource<bool> _resultSource = new();
|
||||
private int _productId;
|
||||
private string _productName = "";
|
||||
|
||||
public int ProductId
|
||||
{
|
||||
get => _productId;
|
||||
set => SetProperty(ref _productId, value);
|
||||
}
|
||||
|
||||
public string ProductName
|
||||
{
|
||||
get => _productName;
|
||||
set => SetProperty(ref _productName, value);
|
||||
}
|
||||
|
||||
public ICommand BuyCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
public ProductDetailViewModel()
|
||||
{
|
||||
BuyCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 返回结果:已购买
|
||||
_resultSource.SetResult(true);
|
||||
});
|
||||
|
||||
CancelCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 返回结果:取消
|
||||
_resultSource.SetResult(false);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task OnNavigatedToAsync(object? parameter)
|
||||
{
|
||||
if (parameter is int productId)
|
||||
{
|
||||
ProductId = productId;
|
||||
// 加载产品详情
|
||||
ProductName = $"产品 #{productId}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnNavigatedFromAsync() => Task.CompletedTask;
|
||||
|
||||
// 实现 INavigationResult<bool>
|
||||
public Task<bool> GetResultAsync()
|
||||
{
|
||||
return _resultSource.Task;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. App.xaml.cs - 注册导航服务
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf(); // 自动注册 INavigationService
|
||||
|
||||
// 注册所有页面 ViewModel
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddViewModel<HomeViewModel>();
|
||||
services.AddViewModel<ProductsViewModel>();
|
||||
services.AddViewModel<ProductDetailViewModel>();
|
||||
services.AddViewModel<SettingsViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. MainWindow.xaml - 导航容器
|
||||
|
||||
```xml
|
||||
<Window x:Class="NavigationExample.MainWindow"
|
||||
Title="导航系统示例" Height="600" Width="800">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Background="#f0f0f0" Padding="10">
|
||||
<Button Content="首页" Command="{Binding NavigateToHomeCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="产品列表" Command="{Binding NavigateToProductsCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="设置" Command="{Binding NavigateToSettingsCommand}" Margin="0,0,20,0"/>
|
||||
|
||||
<Button Content="← 后退" Command="{Binding GoBackCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="前进 →" Command="{Binding GoForwardCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 内容区域 - 显示当前页面 -->
|
||||
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}">
|
||||
<ContentControl.Resources>
|
||||
<!-- 为每个 ViewModel 指定对应的 View -->
|
||||
<DataTemplate DataType="{x:Type local:HomeViewModel}">
|
||||
<local:HomeView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ProductsViewModel}">
|
||||
<local:ProductsView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:SettingsViewModel}">
|
||||
<local:SettingsView/>
|
||||
</DataTemplate>
|
||||
</ContentControl.Resources>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:面包屑导航
|
||||
添加面包屑显示当前导航路径(如:首页 > 产品列表 > 产品详情)。
|
||||
|
||||
### 练习2:导航守卫
|
||||
实现 `INavigationGuard` 接口,在离开页面前检查是否有未保存的更改。
|
||||
|
||||
### 练习3:嵌套导航
|
||||
在产品列表页内部实现子导航(如:全部/热销/新品标签页切换)。
|
||||
|
||||
### 练习4:导航缓存
|
||||
实现页面缓存,避免重复创建 ViewModel(保持页面状态)。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: 导航后如何保持页面状态?**
|
||||
A: 不要后退时,CurrentViewModel 会保留在前进栈中,前进时会恢复状态。如果需要完全保持状态,可以使用 Singleton ViewModel。
|
||||
|
||||
**Q: 如何传递复杂对象作为参数?**
|
||||
A: 使用 `NavigateAsync(object parameter)`,parameter 可以是任何对象。
|
||||
|
||||
**Q: 导航历史栈何时清空?**
|
||||
A: 调用 `_navigationService.ClearHistory()` 手动清空。
|
||||
|
||||
**Q: 能否使用动画过渡?**
|
||||
A: 可以,在 ContentControl 上添加 Storyboard 实现页面切换动画。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [WPF 数据模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/data-templating-overview)
|
||||
- [MVVM 导航模式](https://prismlibrary.com/docs/navigation.html)
|
||||
- 下一个示例:[05-Localization](../05-Localization/README.md)
|
||||
|
||||
---
|
||||
|
||||
**掌握导航系统,构建流畅的多页面应用!** 🧭
|
||||
@@ -0,0 +1,278 @@
|
||||
# 05 - 国际化(i18n)示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的国际化功能实现多语言支持,包括运行时切换语言、资源文件管理等。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `ILocalizationService` 实现多语言
|
||||
- 在 XAML 中使用 `{loc:Localize}` 绑定本地化字符串
|
||||
- 运行时动态切换语言
|
||||
- 管理资源文件(.resx)
|
||||
- 格式化带参数的本地化字符串
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - ILocalizationService, LocalizeExtension
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/05-Localization
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. 创建资源文件
|
||||
|
||||
```
|
||||
Resources/
|
||||
├── Strings.resx # 默认语言(中文)
|
||||
├── Strings.en-US.resx # 英文
|
||||
└── Strings.ja-JP.resx # 日文(可选)
|
||||
```
|
||||
|
||||
**Strings.resx** (中文):
|
||||
```xml
|
||||
<data name="Welcome" xml:space="preserve">
|
||||
<value>欢迎使用 Sharp 框架</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>用户名</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>密码</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>登录</value>
|
||||
</data>
|
||||
<data name="WelcomeUser" xml:space="preserve">
|
||||
<value>欢迎, {0}!</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
**Strings.en-US.resx** (英文):
|
||||
```xml
|
||||
<data name="Welcome" xml:space="preserve">
|
||||
<value>Welcome to Sharp Framework</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>Username</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>Password</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="WelcomeUser" xml:space="preserve">
|
||||
<value>Welcome, {0}!</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
### 2. App.xaml.cs - 注册国际化服务
|
||||
|
||||
```csharp
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using Wpf.Localization;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf();
|
||||
|
||||
// 添加国际化支持
|
||||
var resourceManager = new ResourceManager(
|
||||
"LocalizationExample.Resources.Strings", // 资源文件的完整命名空间
|
||||
Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddLocalization(
|
||||
resourceManager,
|
||||
new CultureInfo("zh-CN"), // 中文
|
||||
new CultureInfo("en-US"), // 英文
|
||||
new CultureInfo("ja-JP") // 日文
|
||||
);
|
||||
|
||||
services.AddViewModel<MainViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. MainViewModel.cs - 语言切换
|
||||
|
||||
```csharp
|
||||
using Wpf.Localization;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
public ILocalizationService Localization => _localization;
|
||||
|
||||
public List<CultureInfo> AvailableLanguages { get; }
|
||||
|
||||
public CultureInfo CurrentLanguage
|
||||
{
|
||||
get => _localization.CurrentCulture;
|
||||
set
|
||||
{
|
||||
_localization.ChangeCulture(value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ChangeLanguageCommand { get; }
|
||||
public ICommand ShowGreetingCommand { get; }
|
||||
|
||||
public MainViewModel(
|
||||
ILocalizationService localization,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_localization = localization;
|
||||
_dialogService = dialogService;
|
||||
|
||||
AvailableLanguages = localization.GetSupportedCultures().ToList();
|
||||
|
||||
// 监听语言变化
|
||||
_localization.CultureChanged += (s, culture) =>
|
||||
{
|
||||
_dialogService.ShowMessage(
|
||||
$"语言已切换到: {culture.NativeName}",
|
||||
"提示");
|
||||
};
|
||||
|
||||
ChangeLanguageCommand = new RelayCommand<CultureInfo>(culture =>
|
||||
{
|
||||
if (culture != null)
|
||||
{
|
||||
CurrentLanguage = culture;
|
||||
}
|
||||
});
|
||||
|
||||
ShowGreetingCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 使用格式化字符串
|
||||
var message = _localization.GetString("WelcomeUser", "张三");
|
||||
_dialogService.ShowMessage(message, "问候");
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. MainWindow.xaml - 使用本地化绑定
|
||||
|
||||
```xml
|
||||
<Window x:Class="LocalizationExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:Wpf.Localization;assembly=Sharp.Wpf"
|
||||
Title="国际化示例" Height="400" Width="500">
|
||||
<StackPanel Margin="20">
|
||||
<!-- 方式1: 使用 LocalizeExtension(推荐) -->
|
||||
<TextBlock Text="{loc:Localize Welcome}" FontSize="24" FontWeight="Bold"/>
|
||||
|
||||
<!-- 语言选择 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,20,0,0">
|
||||
<TextBlock Text="选择语言:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<ComboBox ItemsSource="{Binding AvailableLanguages}"
|
||||
SelectedItem="{Binding CurrentLanguage}"
|
||||
DisplayMemberPath="NativeName"
|
||||
Width="150"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<StackPanel Margin="0,30,0,0">
|
||||
<!-- 用户名 -->
|
||||
<TextBlock Text="{loc:Localize Username}"/>
|
||||
<TextBox Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 密码 -->
|
||||
<TextBlock Text="{loc:Localize Password}"/>
|
||||
<PasswordBox Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<Button Content="{loc:Localize Login}"
|
||||
Height="35" FontSize="14"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 方式2: 直接绑定 Localization 服务 -->
|
||||
<StackPanel Margin="0,30,0,0">
|
||||
<TextBlock Text="{Binding Localization[Welcome]}" FontStyle="Italic"/>
|
||||
<TextBlock Text="{Binding Localization[Username]}" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 测试格式化字符串 -->
|
||||
<Button Content="显示问候" Command="{Binding ShowGreetingCommand}" Margin="0,20,0,0"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 5. 在代码中使用本地化
|
||||
|
||||
```csharp
|
||||
// 方式1: 通过服务获取
|
||||
var welcomeText = _localization.GetString("Welcome");
|
||||
|
||||
// 方式2: 带格式化参数
|
||||
var greeting = _localization.GetString("WelcomeUser", username);
|
||||
|
||||
// 方式3: 索引器(用于绑定)
|
||||
var password = _localization["Password"];
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多语言
|
||||
添加法语(fr-FR)或德语(de-DE)资源文件。
|
||||
|
||||
### 练习2:保存语言偏好
|
||||
将用户选择的语言保存到配置文件,下次启动时自动加载。
|
||||
|
||||
### 练习3:动态加载资源
|
||||
实现从外部 JSON 文件加载翻译资源(而非编译进程序)。
|
||||
|
||||
### 练习4:翻译进度显示
|
||||
显示每种语言的翻译完成度(如:英文 95%,日文 60%)。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: 资源文件修改后不生效?**
|
||||
A: 确保:
|
||||
1. 资源文件的"生成操作"设为"嵌入的资源"
|
||||
2. 重新生成项目(Clean + Rebuild)
|
||||
|
||||
**Q: 如何支持从右到左(RTL)语言?**
|
||||
A: 监听 CultureChanged 事件,设置 `FlowDirection`:
|
||||
```csharp
|
||||
_localization.CultureChanged += (s, culture) => {
|
||||
if (culture.TextInfo.IsRightToLeft) {
|
||||
Application.Current.MainWindow.FlowDirection = FlowDirection.RightToLeft;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Q: 能否热重载资源文件?**
|
||||
A: Sharp.Wpf 当前使用编译时嵌入资源,不支持运行时重载。可以使用外部 JSON 文件实现热重载。
|
||||
|
||||
**Q: 如何处理复数形式?**
|
||||
A: 使用不同的资源键或使用占位符:
|
||||
```
|
||||
"{Count} items" → "{0} 个项目"
|
||||
```
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [.NET 资源文件](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/resources)
|
||||
- [CultureInfo 类](https://learn.microsoft.com/zh-cn/dotnet/api/system.globalization.cultureinfo)
|
||||
- [WPF 本地化](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/globalization-for-wpf)
|
||||
- 下一个示例:[06-Theming](../06-Theming/README.md)
|
||||
|
||||
---
|
||||
|
||||
**构建面向全球用户的应用!** 🌍
|
||||
@@ -0,0 +1,312 @@
|
||||
# 06 - 主题系统示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的主题系统实现浅色/深色主题切换,包括自定义颜色、跟随系统主题等功能。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `IThemeService` 管理应用主题
|
||||
- 实现浅色/深色主题切换
|
||||
- 自定义主题颜色
|
||||
- 跟随系统主题设置
|
||||
- 动态主题切换(无需重启)
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - IThemeService, ThemeService
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/06-Theming
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 注册主题服务
|
||||
|
||||
```csharp
|
||||
using Wpf.Theming;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf(); // 自动注册 IThemeService
|
||||
|
||||
services.AddViewModel<MainViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// 应用初始主题
|
||||
var themeService = provider.GetRequiredService<IThemeService>();
|
||||
themeService.ApplyTheme(Theme.Light); // 或 Theme.System
|
||||
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. MainViewModel.cs - 主题切换逻辑
|
||||
|
||||
```csharp
|
||||
using Wpf.Theming;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private Theme _currentTheme;
|
||||
|
||||
public Theme CurrentTheme
|
||||
{
|
||||
get => _currentTheme;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _currentTheme, value);
|
||||
_themeService.ApplyTheme(value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SwitchToLightCommand { get; }
|
||||
public ICommand SwitchToDarkCommand { get; }
|
||||
public ICommand SwitchToSystemCommand { get; }
|
||||
|
||||
public MainViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_currentTheme = themeService.CurrentTheme;
|
||||
|
||||
// 监听主题变化
|
||||
_themeService.ThemeChanged += (s, theme) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(CurrentTheme));
|
||||
};
|
||||
|
||||
SwitchToLightCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.Light;
|
||||
});
|
||||
|
||||
SwitchToDarkCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.Dark;
|
||||
});
|
||||
|
||||
SwitchToSystemCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.System;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. MainWindow.xaml - 使用主题颜色
|
||||
|
||||
```xml
|
||||
<Window x:Class="ThemingExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="主题系统示例" Height="500" Width="600"
|
||||
Background="{DynamicResource BackgroundColor}"> <!-- 使用主题颜色 -->
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 主题切换栏 -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal"
|
||||
Background="{DynamicResource PrimaryColor}" Padding="15">
|
||||
<TextBlock Text="选择主题:"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<Button Content="☀️ 浅色" Command="{Binding SwitchToLightCommand}"
|
||||
Margin="0,0,10,0" Padding="15,5"/>
|
||||
<Button Content="🌙 深色" Command="{Binding SwitchToDarkCommand}"
|
||||
Margin="0,0,10,0" Padding="15,5"/>
|
||||
<Button Content="🖥️ 跟随系统" Command="{Binding SwitchToSystemCommand}"
|
||||
Padding="15,5"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<ScrollViewer Grid.Row="1" Padding="20">
|
||||
<StackPanel>
|
||||
<TextBlock Text="主题颜色预览" FontSize="24" FontWeight="Bold"
|
||||
Foreground="{DynamicResource ForegroundColor}" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 基础颜色 -->
|
||||
<GroupBox Header="基础颜色" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<Border Background="{DynamicResource PrimaryColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="PrimaryColor" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BackgroundColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="BackgroundColor"
|
||||
Foreground="{DynamicResource ForegroundColor}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 状态颜色 -->
|
||||
<GroupBox Header="状态颜色" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<Border Background="{DynamicResource SuccessColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="✓ Success" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource WarningColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="⚠ Warning" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource ErrorColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="✗ Error" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource InfoColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="ℹ Info" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 控件预览 -->
|
||||
<GroupBox Header="控件预览" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<!-- 按钮 -->
|
||||
<Button Content="普通按钮" Margin="0,5" Padding="10,5"
|
||||
Background="{DynamicResource ButtonBackgroundColor}"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<TextBox Text="输入框示例" Margin="0,5" Padding="5"
|
||||
Background="{DynamicResource InputBackgroundColor}"
|
||||
BorderBrush="{DynamicResource InputBorderColor}"/>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<CheckBox Content="复选框示例" Margin="0,5"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
|
||||
<!-- 单选按钮 -->
|
||||
<RadioButton Content="单选按钮示例" Margin="0,5"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 文本示例 -->
|
||||
<GroupBox Header="文本示例">
|
||||
<StackPanel>
|
||||
<TextBlock Text="这是标题文本" FontSize="20" FontWeight="Bold"
|
||||
Foreground="{DynamicResource ForegroundColor}" Margin="0,5"/>
|
||||
<TextBlock Text="这是正文文本。主题切换后,所有使用 DynamicResource 的颜色都会自动更新,无需重启应用。"
|
||||
TextWrapping="Wrap" Foreground="{DynamicResource ForegroundColor}"
|
||||
Margin="0,5"/>
|
||||
<TextBlock Text="这是次要文本" FontSize="12" FontStyle="Italic"
|
||||
Foreground="Gray" Margin="0,5"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 4. 自定义主题颜色
|
||||
|
||||
```csharp
|
||||
public class CustomThemeViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
|
||||
public ICommand ChangePrimaryColorCommand { get; }
|
||||
|
||||
public CustomThemeViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
|
||||
ChangePrimaryColorCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 打开颜色选择器(可以使用第三方库或自定义)
|
||||
var color = Colors.DeepSkyBlue; // 示例:选择的颜色
|
||||
|
||||
// 更新主题颜色
|
||||
_themeService.SetColor("PrimaryColor", color);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 主题资源字典(可选 - 高级用法)
|
||||
|
||||
可以创建独立的资源字典文件:
|
||||
|
||||
**Themes/LightTheme.xaml**:
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="White"/>
|
||||
<SolidColorBrush x:Key="ForegroundColor" Color="Black"/>
|
||||
<!-- ... 其他颜色 -->
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
**Themes/DarkTheme.xaml**:
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="#202020"/>
|
||||
<SolidColorBrush x:Key="ForegroundColor" Color="White"/>
|
||||
<!-- ... 其他颜色 -->
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多预设主题
|
||||
添加"高对比度"主题,适合视力不佳的用户。
|
||||
|
||||
### 练习2:主题动画过渡
|
||||
在切换主题时添加颜色渐变动画,而非瞬间切换。
|
||||
|
||||
### 练习3:保存主题偏好
|
||||
将用户选择的主题保存到配置文件,下次启动时自动应用。
|
||||
|
||||
### 练习4:自定义主题编辑器
|
||||
创建一个界面,允许用户自定义每种颜色,并导出/导入主题配置。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: StaticResource 和 DynamicResource 的区别?**
|
||||
A:
|
||||
- `StaticResource`: 编译时绑定,主题切换后不会更新
|
||||
- `DynamicResource`: 运行时绑定,主题切换后自动更新(推荐用于主题)
|
||||
|
||||
**Q: 主题切换后部分控件没变化?**
|
||||
A: 确保使用了 `{DynamicResource}` 而非 `{StaticResource}`。
|
||||
|
||||
**Q: 如何跟随 Windows 系统主题?**
|
||||
A: 使用 `Theme.System`,Sharp.Wpf 会自动检测系统主题设置。
|
||||
|
||||
**Q: 能否为不同的窗口设置不同主题?**
|
||||
A: 可以,但不推荐。可以为每个 Window 设置独立的 ResourceDictionary。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [WPF 资源](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/systems/xaml-resources-overview)
|
||||
- [WPF 样式和模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/controls/styles-templates-overview)
|
||||
- [Windows 主题检测](https://learn.microsoft.com/zh-cn/windows/apps/desktop/modernize/apply-windows-themes)
|
||||
- 综合示例:[11-FullStackDemo](../11-FullStackDemo/README.md)
|
||||
|
||||
---
|
||||
|
||||
**打造美观现代的用户界面!** 🎨
|
||||
@@ -0,0 +1,569 @@
|
||||
# 11 - Full Stack Demo - 完整功能演示
|
||||
|
||||
这是一个综合示例,集成了 Sharp 框架的几乎所有功能,演示如何构建一个完整的工业监控系统。
|
||||
|
||||
## 🎯 功能列表
|
||||
|
||||
### 核心功能
|
||||
- ✅ 用户登录(数据库验证)
|
||||
- ✅ 多页面导航
|
||||
- ✅ 主题切换(浅色/深色)
|
||||
- ✅ 多语言支持(中英文)
|
||||
- ✅ 表单验证
|
||||
|
||||
### 数据管理
|
||||
- ✅ 产品 CRUD 操作
|
||||
- ✅ 数据分页显示
|
||||
- ✅ 缓存优化
|
||||
|
||||
### 工业IoT
|
||||
- ✅ **PLC 通信测试** - 可配置 IP 和参数进行测试
|
||||
- ✅ **串口通信测试** - 可选择串口和配置参数
|
||||
|
||||
### 其他
|
||||
- ✅ 配置热重载
|
||||
- ✅ 日志记录
|
||||
- ✅ HTTP 客户端调用
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- Sharp.Common
|
||||
- Sharp.Config
|
||||
- Sharp.Wpf
|
||||
- Sharp.Data
|
||||
- Sharp.Cache
|
||||
- Sharp.Log
|
||||
- Sharp.Plc.Siemens
|
||||
- Sharp.Ports
|
||||
- Sharp.NetClient
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/11-FullStackDemo
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 项目结构
|
||||
|
||||
```
|
||||
11-FullStackDemo/
|
||||
├── App.xaml.cs # 应用启动配置
|
||||
├── appsettings.json # 配置文件
|
||||
├── Models/
|
||||
│ ├── User.cs # 用户实体
|
||||
│ └── Product.cs # 产品实体
|
||||
├── ViewModels/
|
||||
│ ├── LoginViewModel.cs # 登录页
|
||||
│ ├── MainViewModel.cs # 主窗口
|
||||
│ ├── HomeViewModel.cs # 首页(仪表板)
|
||||
│ ├── ProductsViewModel.cs # 产品管理
|
||||
│ ├── PlcTestViewModel.cs # PLC 测试页 ⭐
|
||||
│ ├── PortsTestViewModel.cs# 串口测试页 ⭐
|
||||
│ └── SettingsViewModel.cs # 设置页
|
||||
├── Views/
|
||||
│ ├── LoginWindow.xaml
|
||||
│ ├── MainWindow.xaml
|
||||
│ ├── HomeView.xaml
|
||||
│ ├── ProductsView.xaml
|
||||
│ ├── PlcTestView.xaml # PLC 测试界面 ⭐
|
||||
│ ├── PortsTestView.xaml # 串口测试界面 ⭐
|
||||
│ └── SettingsView.xaml
|
||||
└── Data/
|
||||
└── AppDbContext.cs
|
||||
```
|
||||
|
||||
## 🔧 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 完整启动配置
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 1. 配置管理
|
||||
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||
services.AddSingleton(config);
|
||||
|
||||
// 2. 数据库
|
||||
services.AddSharpDatabase<AppDbContext>(config);
|
||||
|
||||
// 3. WPF 服务(包括导航、主题、国际化)
|
||||
services.AddSharpWpf();
|
||||
services.AddLocalization(Resources.Strings.ResourceManager,
|
||||
new CultureInfo("zh-CN"),
|
||||
new CultureInfo("en-US"));
|
||||
|
||||
// 4. 缓存
|
||||
services.AddSharpCache();
|
||||
|
||||
// 5. 日志
|
||||
services.AddLogging(builder => {
|
||||
builder.AddConsole();
|
||||
builder.AddDebug();
|
||||
});
|
||||
|
||||
// 6. PLC 服务(可选 - 如果配置中启用)
|
||||
var plcEnabled = config.GetValue<bool>("Plc:Enabled", false);
|
||||
if (plcEnabled)
|
||||
{
|
||||
services.AddSiemensPlc(config.GetSection("Plc:Siemens"));
|
||||
}
|
||||
|
||||
// 7. 串口服务
|
||||
services.AddSingleton<ISerialPortService, SerialPortService>();
|
||||
|
||||
// 8. HTTP 客户端
|
||||
services.AddHttpClient();
|
||||
|
||||
// 9. 注册 ViewModels
|
||||
services.AddViewModel<LoginViewModel>();
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddViewModel<HomeViewModel>();
|
||||
services.AddViewModel<ProductsViewModel>();
|
||||
services.AddViewModel<PlcTestViewModel>();
|
||||
services.AddViewModel<PortsTestViewModel>();
|
||||
services.AddViewModel<SettingsViewModel>();
|
||||
|
||||
// 10. 注册 Windows
|
||||
services.AddTransient<LoginWindow>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// 11. 初始化数据库
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
SeedData(db); // 初始化测试数据
|
||||
}
|
||||
|
||||
// 12. 应用主题
|
||||
var themeService = _serviceProvider.GetRequiredService<IThemeService>();
|
||||
var savedTheme = config.GetValue<string>("App:Theme", "Light");
|
||||
themeService.ApplyTheme(Enum.Parse<Theme>(savedTheme));
|
||||
|
||||
// 13. 显示登录窗口
|
||||
ShowLoginWindow();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. PlcTestViewModel.cs - PLC 测试页 ⭐
|
||||
|
||||
```csharp
|
||||
public class PlcTestViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private IPlcService? _plcService;
|
||||
|
||||
// 连接参数
|
||||
private string _ipAddress = "192.168.1.100";
|
||||
private int _rack = 0;
|
||||
private int _slot = 1;
|
||||
private bool _isConnected;
|
||||
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
}
|
||||
|
||||
public int Rack
|
||||
{
|
||||
get => _rack;
|
||||
set => SetProperty(ref _rack, value);
|
||||
}
|
||||
|
||||
public int Slot
|
||||
{
|
||||
get => _slot;
|
||||
set => SetProperty(ref _slot, value);
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
// 读写测试
|
||||
private string _address = "DB1.DBD0";
|
||||
private string _value = "0";
|
||||
private string _readResult = "";
|
||||
|
||||
public string Address
|
||||
{
|
||||
get => _address;
|
||||
set => SetProperty(ref _address, value);
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
public string ReadResult
|
||||
{
|
||||
get => _readResult;
|
||||
set => SetProperty(ref _readResult, value);
|
||||
}
|
||||
|
||||
public ICommand ConnectCommand { get; }
|
||||
public ICommand DisconnectCommand { get; }
|
||||
public ICommand ReadCommand { get; }
|
||||
public ICommand WriteCommand { get; }
|
||||
|
||||
public PlcTestViewModel(IDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
|
||||
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
// 动态创建 PLC 服务
|
||||
var options = new SiemensPlcOptions
|
||||
{
|
||||
IpAddress = IpAddress,
|
||||
Rack = Rack,
|
||||
Slot = Slot,
|
||||
CpuType = CpuType.S71500
|
||||
};
|
||||
|
||||
_plcService = new PlcMonitorService(
|
||||
new SiemensDriver(options),
|
||||
new SiemensOptimizer(),
|
||||
new BigEndianConverter(),
|
||||
Options.Create(new PlcMonitorOptions()),
|
||||
null);
|
||||
|
||||
await _plcService.StartAsync(ct);
|
||||
|
||||
IsConnected = true;
|
||||
_dialogService.ShowMessage("PLC 连接成功!", "成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"连接失败: {ex.Message}", "错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
|
||||
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService != null)
|
||||
{
|
||||
await _plcService.StopAsync();
|
||||
IsConnected = false;
|
||||
_dialogService.ShowMessage("已断开连接", "提示");
|
||||
}
|
||||
});
|
||||
|
||||
ReadCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService == null)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
var result = await _plcService.ReadAsync<int>(Address);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ReadResult = $"读取成功: {result.Value}";
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadResult = $"读取失败: {result.ErrorMessage}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReadResult = $"读取异常: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
|
||||
WriteCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService == null)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
var intValue = int.Parse(Value);
|
||||
var result = await _plcService.WriteAsync(Address, intValue);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_dialogService.ShowMessage("写入成功!", "成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"写入失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"写入异常: {ex.Message}", "错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. PortsTestViewModel.cs - 串口测试页 ⭐
|
||||
|
||||
```csharp
|
||||
public class PortsTestViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ISerialPortService _serialPortService;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
// 可用串口列表
|
||||
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||
|
||||
private string? _selectedPort;
|
||||
private int _baudRate = 9600;
|
||||
private bool _isConnected;
|
||||
|
||||
public string? SelectedPort
|
||||
{
|
||||
get => _selectedPort;
|
||||
set => SetProperty(ref _selectedPort, value);
|
||||
}
|
||||
|
||||
public int BaudRate
|
||||
{
|
||||
get => _baudRate;
|
||||
set => SetProperty(ref _baudRate, value);
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
// 发送/接收
|
||||
private string _sendData = "";
|
||||
private string _receiveData = "";
|
||||
|
||||
public string SendData
|
||||
{
|
||||
get => _sendData;
|
||||
set => SetProperty(ref _sendData, value);
|
||||
}
|
||||
|
||||
public string ReceiveData
|
||||
{
|
||||
get => _receiveData;
|
||||
set => SetProperty(ref _receiveData, value);
|
||||
}
|
||||
|
||||
public ICommand RefreshPortsCommand { get; }
|
||||
public ICommand ConnectCommand { get; }
|
||||
public ICommand DisconnectCommand { get; }
|
||||
public ICommand SendCommand { get; }
|
||||
public ICommand ClearCommand { get; }
|
||||
|
||||
public PortsTestViewModel(
|
||||
ISerialPortService serialPortService,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_serialPortService = serialPortService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
// 监听数据接收
|
||||
_serialPortService.DataReceived += (s, e) =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 接收: {BitConverter.ToString(e.Data)}\n";
|
||||
});
|
||||
};
|
||||
|
||||
RefreshPortsCommand = new RelayCommand(() =>
|
||||
{
|
||||
AvailablePorts.Clear();
|
||||
var ports = SerialPortDiscovery.GetAvailablePorts();
|
||||
foreach (var port in ports)
|
||||
{
|
||||
AvailablePorts.Add(port.PortName);
|
||||
}
|
||||
|
||||
if (AvailablePorts.Count > 0)
|
||||
{
|
||||
SelectedPort = AvailablePorts[0];
|
||||
}
|
||||
});
|
||||
|
||||
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedPort))
|
||||
{
|
||||
_dialogService.ShowMessage("请选择串口", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = new SerialPortOptions
|
||||
{
|
||||
PortName = SelectedPort,
|
||||
BaudRate = BaudRate
|
||||
};
|
||||
|
||||
var result = await _serialPortService.StartListeningAsync(options, autoReconnect: false);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
IsConnected = true;
|
||||
_dialogService.ShowMessage("串口连接成功!", "成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"连接失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"连接异常: {ex.Message}", "错误");
|
||||
}
|
||||
});
|
||||
|
||||
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _serialPortService.StopListeningAsync();
|
||||
IsConnected = false;
|
||||
_dialogService.ShowMessage("已断开连接", "提示");
|
||||
});
|
||||
|
||||
SendCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接串口", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 将字符串转换为字节数组
|
||||
var data = Encoding.UTF8.GetBytes(SendData);
|
||||
var result = await _serialPortService.SendAsync(data);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 发送: {SendData}\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"发送失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"发送异常: {ex.Message}", "错误");
|
||||
}
|
||||
});
|
||||
|
||||
ClearCommand = new RelayCommand(() =>
|
||||
{
|
||||
ReceiveData = "";
|
||||
});
|
||||
|
||||
// 初始化:刷新串口列表
|
||||
RefreshPortsCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. appsettings.json - 配置文件
|
||||
|
||||
```json
|
||||
{
|
||||
"App": {
|
||||
"Theme": "Light",
|
||||
"Language": "zh-CN"
|
||||
},
|
||||
"Database": {
|
||||
"Provider": "Sqlite",
|
||||
"ConnectionString": "Data Source=demo.db"
|
||||
},
|
||||
"Plc": {
|
||||
"Enabled": false,
|
||||
"Siemens": {
|
||||
"IpAddress": "192.168.1.100",
|
||||
"Port": 102,
|
||||
"CpuType": "S71500",
|
||||
"Rack": 0,
|
||||
"Slot": 1
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎓 使用说明
|
||||
|
||||
### 测试 PLC 连接
|
||||
1. 进入"PLC 测试"页面
|
||||
2. 输入 PLC 的 IP 地址、Rack、Slot
|
||||
3. 点击"连接"按钮
|
||||
4. 连接成功后,输入地址(如 `DB1.DBD0`)
|
||||
5. 点击"读取"或"写入"测试通信
|
||||
|
||||
### 测试串口通信
|
||||
1. 进入"串口测试"页面
|
||||
2. 点击"刷新端口"查看可用串口
|
||||
3. 选择串口并设置波特率
|
||||
4. 点击"连接"
|
||||
5. 在发送框输入数据,点击"发送"
|
||||
6. 接收框会显示收到的数据
|
||||
|
||||
## 💡 提示
|
||||
|
||||
- PLC 和串口功能可以独立测试,无需实际设备(会显示连接失败,但不影响其他功能)
|
||||
- 数据库使用 SQLite,无需安装
|
||||
- 配置文件支持热重载,修改后自动生效
|
||||
- 所有功能都有错误处理,不会导致程序崩溃
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- 所有示例项目的 README
|
||||
- Sharp 框架文档
|
||||
|
||||
---
|
||||
|
||||
**这是一个完整的工业监控系统模板,可以直接用于实际项目开发!** 🏭
|
||||
@@ -0,0 +1,273 @@
|
||||
# Sharp Framework Examples
|
||||
|
||||
本目录包含 Sharp 框架各个功能的示例项目,帮助你快速上手。
|
||||
|
||||
## 📚 示例项目列表
|
||||
|
||||
### 基础示例
|
||||
|
||||
#### 1. **01-MinimalWpfApp** - 最小 WPF 应用 ⭐
|
||||
**演示内容**:
|
||||
- 最简单的 Sharp WPF 应用
|
||||
- 仅使用 Common + Wpf 模块
|
||||
- MVVM 基础(ViewModelBase, Commands)
|
||||
- 对话框服务
|
||||
|
||||
**适用场景**:刚接触 Sharp 框架,想快速了解基本用法
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
cd examples/01-MinimalWpfApp
|
||||
dotnet run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. **02-WpfWithDatabase** - WPF + 数据库 ⭐⭐
|
||||
**演示内容**:
|
||||
- WPF + SQLite 数据库
|
||||
- Repository 模式(CRUD操作)
|
||||
- 数据绑定与列表展示
|
||||
- 添加/编辑/删除产品
|
||||
|
||||
**适用场景**:需要本地数据存储的桌面应用
|
||||
|
||||
**技术栈**:Common, Config, Wpf, Data (SQLite)
|
||||
|
||||
---
|
||||
|
||||
#### 3. **03-FormValidation** - 表单验证 ⭐⭐
|
||||
**演示内容**:
|
||||
- ValidatableViewModelBase 使用
|
||||
- DataAnnotations 验证
|
||||
- 实时验证反馈
|
||||
- 错误消息显示
|
||||
|
||||
**适用场景**:需要用户输入验证的应用
|
||||
|
||||
---
|
||||
|
||||
#### 4. **04-Navigation** - 导航系统 ⭐⭐
|
||||
**演示内容**:
|
||||
- INavigationService 使用
|
||||
- 多页面导航
|
||||
- 前进/后退功能
|
||||
- 页面间参数传递
|
||||
- 导航生命周期(INavigationAware)
|
||||
|
||||
**适用场景**:多页面 WPF 应用
|
||||
|
||||
---
|
||||
|
||||
#### 5. **05-Localization** - 国际化 ⭐⭐
|
||||
**演示内容**:
|
||||
- 多语言支持(中文、英文)
|
||||
- LocalizeExtension XAML 绑定
|
||||
- 运行时切换语言
|
||||
- 资源文件管理
|
||||
|
||||
**适用场景**:需要多语言支持的应用
|
||||
|
||||
---
|
||||
|
||||
#### 6. **06-Theming** - 主题系统 ⭐⭐
|
||||
**演示内容**:
|
||||
- 浅色/深色主题切换
|
||||
- 自定义主题颜色
|
||||
- 跟随系统主题
|
||||
- 动态主题切换
|
||||
|
||||
**适用场景**:需要主题切换的现代应用
|
||||
|
||||
---
|
||||
|
||||
### 工业IoT示例
|
||||
|
||||
#### 7. **07-PlcMonitoring** - PLC 监控 ⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 连接西门子 PLC
|
||||
- 实时数据监控
|
||||
- 数据变化订阅
|
||||
- 读写PLC标签
|
||||
- 数据可视化(图表)
|
||||
|
||||
**适用场景**:工业自动化、设备监控
|
||||
|
||||
**技术栈**:Wpf, Plc.Siemens, Cache(缓存历史数据)
|
||||
|
||||
---
|
||||
|
||||
#### 8. **08-SerialPortComm** - 串口通信 ⭐⭐
|
||||
**演示内容**:
|
||||
- 串口自动发现
|
||||
- 数据收发
|
||||
- HEX/ASCII 显示
|
||||
- 数据记录
|
||||
|
||||
**适用场景**:串口设备通信、调试工具
|
||||
|
||||
**技术栈**:Wpf, Ports
|
||||
|
||||
---
|
||||
|
||||
### 网络示例
|
||||
|
||||
#### 9. **09-HttpServer** - 嵌入式 Web 服务器 ⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 在 WPF 应用中嵌入 HTTP 服务器
|
||||
- RESTful API 实现
|
||||
- 中间件使用(CORS, 认证, 日志)
|
||||
- WPF 界面管理服务器
|
||||
|
||||
**适用场景**:设备管理后台、本地Web服务
|
||||
|
||||
**技术栈**:Wpf, NetServer, Log
|
||||
|
||||
---
|
||||
|
||||
#### 10. **10-HttpClient** - HTTP 客户端 ⭐⭐
|
||||
**演示内容**:
|
||||
- 调用外部 API
|
||||
- 重试策略(Polly)
|
||||
- Result<T> 错误处理
|
||||
- 缓存 API 响应
|
||||
|
||||
**适用场景**:需要调用外部服务的应用
|
||||
|
||||
**技术栈**:Wpf, NetClient, Cache
|
||||
|
||||
---
|
||||
|
||||
### 综合示例
|
||||
|
||||
#### 11. **11-FullStackDemo** - 完整功能演示 ⭐⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 用户登录(数据库验证)
|
||||
- 产品管理(CRUD)
|
||||
- PLC 数据监控
|
||||
- 串口通信
|
||||
- 数据报表导出
|
||||
- 多语言
|
||||
- 主题切换
|
||||
- 配置热重载
|
||||
|
||||
**适用场景**:工业监控系统、MES系统、数采系统
|
||||
|
||||
**技术栈**:全部模块
|
||||
|
||||
---
|
||||
|
||||
#### 12. **12-CachingDemo** - 缓存使用 ⭐
|
||||
**演示内容**:
|
||||
- 字典数据缓存
|
||||
- API 响应缓存
|
||||
- 计算结果缓存
|
||||
- 缓存失效策略
|
||||
|
||||
**技术栈**:Wpf, Cache, NetClient
|
||||
|
||||
---
|
||||
|
||||
#### 13. **13-EventBusDemo** - 事件总线 ⭐
|
||||
**演示内容**:
|
||||
- 跨模块通信
|
||||
- 事件发布/订阅
|
||||
- 解耦架构
|
||||
|
||||
**技术栈**:Wpf, EventBus
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 运行示例
|
||||
|
||||
```bash
|
||||
# 1. 克隆仓库
|
||||
git clone https://github.com/your-repo/sharp-basic.git
|
||||
cd sharp-basic/examples
|
||||
|
||||
# 2. 选择一个示例
|
||||
cd 01-MinimalWpfApp
|
||||
|
||||
# 3. 还原依赖并运行
|
||||
dotnet restore
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### 从示例创建新项目
|
||||
|
||||
```bash
|
||||
# 复制示例作为起点
|
||||
cp -r examples/02-WpfWithDatabase MyNewProject
|
||||
cd MyNewProject
|
||||
|
||||
# 修改项目名称
|
||||
# 编辑 .csproj 文件和命名空间
|
||||
# 开始开发!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 学习路径
|
||||
|
||||
### 新手推荐路径
|
||||
1. **01-MinimalWpfApp** - 了解基础
|
||||
2. **02-WpfWithDatabase** - 学习数据库操作
|
||||
3. **03-FormValidation** - 掌握表单验证
|
||||
4. **04-Navigation** - 理解导航系统
|
||||
|
||||
### 工业自动化开发者
|
||||
1. **07-PlcMonitoring** - PLC 通信
|
||||
2. **08-SerialPortComm** - 串口通信
|
||||
3. **11-FullStackDemo** - 完整系统
|
||||
|
||||
### Web 开发者转 WPF
|
||||
1. **09-HttpServer** - HTTP 服务器
|
||||
2. **10-HttpClient** - HTTP 客户端
|
||||
3. **06-Theming** - 主题系统
|
||||
|
||||
---
|
||||
|
||||
## 💡 示例代码说明
|
||||
|
||||
所有示例都遵循:
|
||||
- ✅ **最佳实践**:展示推荐的使用方式
|
||||
- ✅ **完整代码**:可直接运行,无需额外配置
|
||||
- ✅ **详细注释**:中文注释,解释关键代码
|
||||
- ✅ **README**:每个示例都有独立的 README
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术要求
|
||||
|
||||
- .NET 8.0 SDK
|
||||
- Visual Studio 2022 或 VS Code (推荐安装 C# Dev Kit)
|
||||
- Windows 10/11 (WPF 应用需要 Windows)
|
||||
|
||||
---
|
||||
|
||||
## 📝 贡献示例
|
||||
|
||||
欢迎贡献新的示例!请确保:
|
||||
1. 代码简洁明了
|
||||
2. 包含 README 说明
|
||||
3. 注释使用中文
|
||||
4. 可独立运行
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 示例可以直接用于生产吗?**
|
||||
A: 示例代码主要用于学习,生产使用需要根据实际情况调整(如安全性、错误处理、日志等)。
|
||||
|
||||
**Q: 示例使用什么数据库?**
|
||||
A: 大部分示例使用 SQLite(无需安装,开箱即用),也有 SQL Server 示例。
|
||||
|
||||
**Q: 可以在 .NET Framework 上运行吗?**
|
||||
A: 不可以,Sharp 框架基于 .NET 8,需要 .NET 8 运行时。
|
||||
|
||||
---
|
||||
|
||||
**选择一个示例开始你的 Sharp 之旅吧!** 🎉
|
||||
@@ -0,0 +1,341 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Sharp-Basic is a modular C# .NET 8 framework for building WPF and C# applications with reusable components. It provides a comprehensive toolkit with emphasis on:
|
||||
- **Modularity**: Independent, reusable libraries that can be used selectively - "引入即用"
|
||||
- **Quick Start**: Zero-configuration setup with sensible defaults - "约定优于配置"
|
||||
- **Desktop Applications**: WPF-based UI toolkit with MVVM infrastructure
|
||||
- **Industrial IoT**: PLC integration (Siemens) and serial port communication (optional)
|
||||
- **Configuration as Code**: Multi-format (JSON/XML/INI/TOML) with hot-reload
|
||||
- **Functional Programming**: Result<T> pattern for railway-oriented error handling
|
||||
- **Opinionated**: Strong technology choices (EF Core, Serilog, etc.) for team consistency
|
||||
|
||||
**Design Philosophy**: "固执己见的框架" - Makes strong choices about technology stack to enable fast development. If you can't use EF Core, this framework may not be for you.
|
||||
|
||||
**Language**: All comments and documentation are in Chinese (中文).
|
||||
|
||||
## Quick Start
|
||||
|
||||
**New to Sharp?** See `QUICKSTART.md` for zero-configuration setup guide.
|
||||
|
||||
### Minimal WPF Application (3 lines)
|
||||
```csharp
|
||||
var services = new ServiceCollection();
|
||||
services.AddSharpEssentials(); // Config + Logging + Cache
|
||||
services.AddSharpWpf(); // WPF Services (Dialog, Dispatcher)
|
||||
services.AddViewModel<MainViewModel>();
|
||||
```
|
||||
|
||||
### With Database (1 more line)
|
||||
```csharp
|
||||
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Build the entire solution
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet build Sharp.sln
|
||||
```
|
||||
|
||||
### Build specific project
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet build Client/Client.csproj # WPF demo application
|
||||
dotnet build Common/Common.csproj # Core utilities
|
||||
dotnet build Config/Config.csproj # Configuration management
|
||||
```
|
||||
|
||||
### Run tests
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet test Sharp.sln # All tests
|
||||
dotnet test ../../test/Common.Test # Specific test project
|
||||
```
|
||||
|
||||
### Run the WPF Client application
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet run --project Client/Client.csproj
|
||||
```
|
||||
|
||||
### Clean build artifacts
|
||||
```bash
|
||||
cd src/Sharp
|
||||
dotnet clean Sharp.sln
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/Sharp/ # All source projects
|
||||
├── Common/ # Core foundation (Result<T>, Global container, utilities)
|
||||
├── Config/ # Multi-format configuration with hot-reload
|
||||
├── Sharp.Infrastructure/ # Infrastructure abstractions
|
||||
├── Wpf/ # WPF UI toolkit (MVVM, dialogs, converters) - NO UI library dependency
|
||||
├── Wpf.HandyControl/ # HandyControl UI library integration (optional)
|
||||
├── Data/ # EF Core wrapper with repository pattern
|
||||
├── Repository/ # Repository implementations
|
||||
├── Log/ # Serilog-based structured logging
|
||||
├── Cache/ # Memory cache (planned)
|
||||
├── EventBus/ # Event pub/sub (planned)
|
||||
├── NetClient/ # HTTP client with Polly resilience
|
||||
├── NetServer/ # Lightweight HTTP server with middleware
|
||||
├── Ports/ # Serial port abstraction (RS232/RS485)
|
||||
├── Plc/ # PLC abstraction interface
|
||||
├── Plc.Siemens/ # Siemens S7 PLC implementation (S7.NET+)
|
||||
├── Client/ # WPF demo application
|
||||
└── Server/ # WPF server example
|
||||
|
||||
test/ # MSTest test projects
|
||||
├── Common.Test/
|
||||
├── Config.Test/
|
||||
├── Data.Test/
|
||||
├── Log.Test/
|
||||
├── NetClient.Test/
|
||||
└── NetServer.Test/
|
||||
```
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
#### 1. Result<T> Pattern (Railway-Oriented Programming)
|
||||
- Eliminates exceptions as control flow
|
||||
- Provides chainable operations: `Map`, `Bind`, `Match`, `Where`, `OnSuccess`, `OnFailure`
|
||||
- Synchronous and async wrappers: `Result.Try()`, `Result.TryAsync()`
|
||||
- Example usage:
|
||||
```csharp
|
||||
Result.Try(() => int.Parse(input))
|
||||
.Map(x => x * 2)
|
||||
.Where(v => v > 100, "Value too small")
|
||||
.OnSuccess(v => Console.WriteLine(v))
|
||||
.OnFailure((ex, msg) => logger.LogError(msg));
|
||||
```
|
||||
|
||||
#### 2. Dependency Injection (Hybrid Approach)
|
||||
- **Primary**: Microsoft.Extensions.DependencyInjection for standard DI
|
||||
- **Fallback**: Global service locator (Common.Global) for legacy/convenience
|
||||
- Services registered in `DependencyInjection` folders within each module
|
||||
- Client app startup (App.xaml.cs) orchestrates all service registration
|
||||
|
||||
#### 3. Configuration Management
|
||||
- Multi-source configuration builder supporting JSON/XML/INI/TOML
|
||||
- Hot-reload with change notifications
|
||||
- Strong typing with DataAnnotations validation
|
||||
- IOptions<T> pattern integration
|
||||
- Environment variable overrides with optional prefix
|
||||
|
||||
#### 4. Repository Pattern with EF Core
|
||||
- Generic `IRepository<TEntity>` abstraction
|
||||
- Built-in entity types: `Entity`, `AuditableEntity`, `ISoftDeletable`
|
||||
- Auto-audit tracking (CreatedAt, CreatedBy, UpdatedAt, UpdatedBy)
|
||||
- Multi-database support: SQL Server, SQLite, PostgreSQL, MySQL, In-Memory
|
||||
|
||||
#### 5. MVVM for WPF
|
||||
- `ViewModelBase` with INotifyPropertyChanged and IsBusy tracking
|
||||
- `RelayCommand` and `AsyncCommand` implementations
|
||||
- Services: `IDialogService`, `IFileDialogService`, `IDispatcherService`
|
||||
- Converters: BoolToVisibility, InverseBooleanConverter, EnumToDescriptionConverter
|
||||
- Global exception handling for UI/Task/AppDomain exceptions
|
||||
|
||||
#### 6. Middleware Pattern
|
||||
- Custom HTTP server (NetServer) with ASP.NET Core-style pipeline
|
||||
- Middleware: Authentication, CORS, Rate Limiting, Caching, Logging, Exception Handling
|
||||
- Attribute-based routing with `RouteAttribute`
|
||||
- Custom `IMiddleware` interface for extensibility
|
||||
|
||||
## Module Dependencies
|
||||
|
||||
**Core Layer** (no external dependencies):
|
||||
- Common → (no dependencies)
|
||||
|
||||
**Configuration Layer**:
|
||||
- Config → Microsoft.Extensions.Configuration.*, Tomlyn (TOML support)
|
||||
|
||||
**Data Layer**:
|
||||
- Data → EF Core, Common
|
||||
- Repository → Data
|
||||
|
||||
**Network Layer**:
|
||||
- NetClient → Polly, Microsoft.Extensions.Http, Common
|
||||
- NetServer → Common, Log
|
||||
|
||||
**WPF Layer**:
|
||||
- Wpf → (no UI library dependency - pure MVVM)
|
||||
- Wpf.HandyControl → Wpf, HandyControl 3.5.0 (optional UI library integration)
|
||||
- Client → Wpf, Config, NetClient, NetServer, Log, Plc.Siemens, Ports, HandyControl
|
||||
|
||||
**Industrial IoT**:
|
||||
- Ports → (serial port communication)
|
||||
- Plc → (abstraction only)
|
||||
- Plc.Siemens → Plc, S7.NET+ library
|
||||
|
||||
**Logging**:
|
||||
- Log → Serilog, Microsoft.Extensions.Logging
|
||||
|
||||
## Common Development Workflows
|
||||
|
||||
### Quick Start for New Projects (Recommended)
|
||||
1. Install NuGet packages (or reference projects)
|
||||
2. Create `appsettings.json` in project root
|
||||
3. Use Quick Start extensions in `App.xaml.cs`:
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// Option 1: Minimal (Config + Log + Cache + WPF)
|
||||
services.AddSharpEssentials();
|
||||
services.AddSharpWpf();
|
||||
|
||||
// Option 2: With Database
|
||||
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||
services.AddSharpEssentials(config);
|
||||
services.AddSharpDatabase<AppDbContext>(config); // Reads from appsettings.json
|
||||
services.AddSharpWpf();
|
||||
|
||||
// Register ViewModels
|
||||
services.AddViewModel<MainViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
// Show main window...
|
||||
}
|
||||
```
|
||||
4. See `QUICKSTART.md` for complete examples
|
||||
|
||||
### Adding a new module/library
|
||||
1. Create project under `src/Sharp/`
|
||||
2. Target `net8.0` (or `net8.0-windows` for WPF)
|
||||
3. Enable nullable reference types: `<Nullable>enable</Nullable>`
|
||||
4. Create `DependencyInjection` folder for service registration extensions with `AddSharpXxx()` methods
|
||||
5. Create comprehensive `README.md` with Quick Start section
|
||||
6. Add test project under `test/` with MSTest
|
||||
7. Add to Sharp.sln: `dotnet sln add <ProjectPath>`
|
||||
|
||||
### Working with Result<T>
|
||||
- Use `Result.Try()` to wrap exception-prone code
|
||||
- Chain operations with `Map()` for transformations
|
||||
- Use `Bind()` for operations that return Result<T>
|
||||
- Extract values with `GetOrDefault()`, `GetOrThrow()`, or `Match()`
|
||||
- Always handle both success and failure cases
|
||||
|
||||
### Using the Global container
|
||||
- Register singletons: `Global.RegisterSingleton<T>(instance)` or `Global.RegisterSingleton<T>(() => new T())`
|
||||
- Register transients: `Global.RegisterTransient<T>(() => new T())`
|
||||
- Subscribe to changes: `Global.Subscribe<T>(e => { /* handle change */ })`
|
||||
- Retrieve services: `Global.Get<T>()`
|
||||
- **Note**: Prefer standard DI (IServiceCollection) for new code; Global is for legacy/compatibility
|
||||
|
||||
### Configuration workflow (Quick Start)
|
||||
1. Create `appsettings.json` in project root
|
||||
2. Use default configuration:
|
||||
```csharp
|
||||
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||
```
|
||||
3. Configuration automatically loaded with:
|
||||
- Hot-reload enabled
|
||||
- Environment variables (prefix: `APP_`)
|
||||
- Environment-specific files (appsettings.Development.json, etc.)
|
||||
|
||||
### Configuration workflow (Manual)
|
||||
1. Add config files to project root (appsettings.json, etc.)
|
||||
2. Create strongly-typed options class with DataAnnotations
|
||||
3. Build ConfigManager: `ConfigBuilder.BuildFromFiles(...)`
|
||||
4. Register with DI: `services.AddSingleton(cfg)` and `services.AddSingleton(cfg.Configuration)`
|
||||
5. Use hot-reload callbacks: `cfg.OnReload(() => { /* refresh */ })`
|
||||
|
||||
### WPF Client startup flow (Modern Quick Start)
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||
|
||||
services.AddSharpEssentials(config);
|
||||
services.AddSharpWpf();
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
provider.GetRequiredService<MainWindow>().Show();
|
||||
}
|
||||
```
|
||||
|
||||
### Working with PLC (Siemens)
|
||||
- Interface: `IPlcService` in Plc project
|
||||
- Implementation: Plc.Siemens using S7.NET+ library
|
||||
- Supports: S7-1200, S7-1500, S7-300, S7-400
|
||||
- Register with DI: `services.AddSiemensPlc(config.GetSection("Plc:Siemens"))`
|
||||
- Methods: Read/Write memory areas, data blocks, Subscribe for monitoring
|
||||
|
||||
### Serial Port Communication
|
||||
- Service: `ISerialPortService` in Ports module
|
||||
- Discovery: `SerialPortDiscovery` for auto-detection
|
||||
- Event-based data reception: `DataReceivedEventArgs`
|
||||
- Configured with: baud rate, parity, stop bits, data bits
|
||||
|
||||
### Using Cache (When Implemented)
|
||||
```csharp
|
||||
// Dictionary data caching
|
||||
var categories = await _cache.GetOrSetAsync(
|
||||
"categories:all",
|
||||
() => _repository.GetAllAsync(),
|
||||
TimeSpan.FromMinutes(30)
|
||||
);
|
||||
|
||||
// API response caching
|
||||
var weather = await _cache.GetOrSetAsync(
|
||||
$"weather:{city}",
|
||||
() => _apiClient.GetWeatherAsync(city),
|
||||
TimeSpan.FromMinutes(5)
|
||||
);
|
||||
```
|
||||
|
||||
### Using EventBus (When Implemented)
|
||||
```csharp
|
||||
// Publish
|
||||
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||
|
||||
// Subscribe
|
||||
_eventBus.Subscribe<UserLoginEvent>(e => {
|
||||
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||
});
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Target Framework**: .NET 8 (net8.0 / net8.0-windows)
|
||||
- **Nullable**: Enabled (`#nullable enable`)
|
||||
- **Implicit Usings**: Enabled
|
||||
- **Namespace Convention**: `Sharp.*` (Common will migrate to `Sharp.Common`)
|
||||
- **Language**: All comments, docs, and strings in Chinese
|
||||
- **Backwards Compatibility**: Maintain for minor versions; breaking changes only in major versions
|
||||
- **Testing**: MSTest with parallel execution enabled
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **"引入即用" (Plug and Play)**: Every module should work with minimal configuration
|
||||
2. **"约定优于配置" (Convention over Configuration)**: Sensible defaults for everything
|
||||
3. **"固执己见" (Opinionated)**: Strong technology choices (EF Core, Serilog, HandyControl)
|
||||
- If you can't use our stack, this framework is not for you
|
||||
- Designed for team/personal consistency, not maximum flexibility
|
||||
4. **Quick Start First**: Every module README must have a "Quick Start" section showing 1-line setup
|
||||
5. **No Abstraction Overkill**: Don't create interfaces just for flexibility - commit to implementations
|
||||
|
||||
## Module README Template
|
||||
|
||||
Every module must have a README with:
|
||||
1. **Quick Start** section (5-second setup)
|
||||
2. **Why You Need This** section (problem/solution)
|
||||
3. **Core API** section (main interfaces/classes)
|
||||
4. **Typical Scenarios** section (3-5 real-world examples)
|
||||
5. **Configuration** section (if applicable)
|
||||
|
||||
See `EventBus/README.md` and `Cache/README.md` for examples (even though not implemented yet).
|
||||
|
||||
## License
|
||||
|
||||
GNU AGPLv3 - Network services that use this framework must open source modifications. Contact author for alternative commercial licensing.
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Sharp.Cache</AssemblyName>
|
||||
<RootNamespace>Sharp.Cache</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
|
||||
namespace Sharp.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存扩展方法
|
||||
/// </summary>
|
||||
public static class CacheExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取缓存,不存在则执行工厂方法并缓存结果(同步)
|
||||
/// </summary>
|
||||
public static T GetOrSet<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<T> factory,
|
||||
TimeSpan? expiration = null)
|
||||
{
|
||||
if (cache.TryGet<T>(key, out var cached))
|
||||
{
|
||||
return cached!;
|
||||
}
|
||||
|
||||
var value = factory();
|
||||
cache.Set(key, value, expiration);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存,不存在则执行工厂方法并缓存结果(异步)
|
||||
/// </summary>
|
||||
public static async Task<T> GetOrSetAsync<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? expiration = null)
|
||||
{
|
||||
if (cache.TryGet<T>(key, out var cached))
|
||||
{
|
||||
return cached!;
|
||||
}
|
||||
|
||||
var value = await factory();
|
||||
cache.Set(key, value, expiration);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置(滑动过期)
|
||||
/// </summary>
|
||||
public static T GetOrSetSliding<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<T> factory,
|
||||
TimeSpan slidingExpiration)
|
||||
{
|
||||
if (cache.TryGet<T>(key, out var cached))
|
||||
{
|
||||
return cached!;
|
||||
}
|
||||
|
||||
var value = factory();
|
||||
cache.SetSliding(key, value, slidingExpiration);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量移除(支持通配符)
|
||||
/// </summary>
|
||||
public static void RemoveByPattern(this ICacheService cache, string pattern)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pattern))
|
||||
return;
|
||||
|
||||
if (!pattern.EndsWith("*", StringComparison.Ordinal))
|
||||
{
|
||||
cache.Remove(pattern);
|
||||
return;
|
||||
}
|
||||
|
||||
var prefix = pattern.TrimEnd('*');
|
||||
cache.RemoveByPrefix(prefix);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Sharp.Cache.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存服务注册扩展
|
||||
/// </summary>
|
||||
public static class CacheServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp 内存缓存服务
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharpCache(this IServiceCollection services)
|
||||
{
|
||||
// 注册 MemoryCache
|
||||
services.AddMemoryCache();
|
||||
|
||||
// 注册 ICacheService
|
||||
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Sharp 内存缓存服务(带配置)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharpCache(
|
||||
this IServiceCollection services,
|
||||
Action<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions> configure)
|
||||
{
|
||||
// 注册 MemoryCache 并配置
|
||||
services.AddMemoryCache(configure);
|
||||
|
||||
// 注册 ICacheService
|
||||
services.AddSingleton<ICacheService, MemoryCacheService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Sharp.Cache;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存服务接口
|
||||
/// </summary>
|
||||
public interface ICacheService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取缓存值
|
||||
/// </summary>
|
||||
T? Get<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取缓存值
|
||||
/// </summary>
|
||||
bool TryGet<T>(string key, out T? value);
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓存
|
||||
/// </summary>
|
||||
void Set<T>(string key, T value, TimeSpan? expiration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓存(滑动过期)
|
||||
/// </summary>
|
||||
void SetSliding<T>(string key, T value, TimeSpan slidingExpiration);
|
||||
|
||||
/// <summary>
|
||||
/// 移除缓存
|
||||
/// </summary>
|
||||
void Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有缓存
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 按前缀批量移除
|
||||
/// </summary>
|
||||
void RemoveByPrefix(string prefix);
|
||||
|
||||
/// <summary>
|
||||
/// 已缓存键集合(只读)
|
||||
/// </summary>
|
||||
IReadOnlyCollection<string> Keys { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否存在
|
||||
/// </summary>
|
||||
bool Exists(string key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于 MemoryCache 的缓存服务实现
|
||||
/// </summary>
|
||||
public class MemoryCacheService : ICacheService
|
||||
{
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly HashSet<string> _keys = new(); // 跟踪所有键(用于 Clear)
|
||||
private readonly object _lockObj = new();
|
||||
|
||||
public MemoryCacheService(IMemoryCache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public T? Get<T>(string key)
|
||||
{
|
||||
return _cache.Get<T>(key);
|
||||
}
|
||||
|
||||
public bool TryGet<T>(string key, out T? value)
|
||||
{
|
||||
return _cache.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
public void Set<T>(string key, T value, TimeSpan? expiration = null)
|
||||
{
|
||||
var options = new MemoryCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
options.RegisterPostEvictionCallback((k, v, r, s) =>
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_keys.Remove(k.ToString() ?? "");
|
||||
}
|
||||
});
|
||||
|
||||
_cache.Set(key, value, options);
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
_keys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSliding<T>(string key, T value, TimeSpan slidingExpiration)
|
||||
{
|
||||
var options = new MemoryCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = slidingExpiration
|
||||
};
|
||||
|
||||
options.RegisterPostEvictionCallback((k, v, r, s) =>
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
_keys.Remove(k.ToString() ?? "");
|
||||
}
|
||||
});
|
||||
|
||||
_cache.Set(key, value, options);
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
_keys.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
_cache.Remove(key);
|
||||
|
||||
lock (_lockObj)
|
||||
{
|
||||
_keys.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
foreach (var key in _keys.ToList())
|
||||
{
|
||||
_cache.Remove(key);
|
||||
}
|
||||
_keys.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveByPrefix(string prefix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<string> targets;
|
||||
lock (_lockObj)
|
||||
{
|
||||
targets = _keys.Where(k => k.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
foreach (var key in targets)
|
||||
{
|
||||
Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> Keys
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
return _keys.ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Exists(string key)
|
||||
{
|
||||
return _cache.TryGetValue(key, out _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,623 @@
|
||||
# Sharp.Cache
|
||||
|
||||
> **🚧 开发中 / Under Construction**
|
||||
|
||||
轻量级内存缓存模块,基于 `Microsoft.Extensions.Caching.Memory`,提供简洁的缓存API。
|
||||
|
||||
## 📋 为什么需要 Cache?
|
||||
|
||||
### 场景对比
|
||||
|
||||
#### ❌ 不使用 Cache - 重复查询
|
||||
```csharp
|
||||
public class ProductService {
|
||||
private readonly IRepository<Category> _categoryRepo;
|
||||
|
||||
// 问题:每次打开编辑对话框都查询数据库
|
||||
public async Task<ProductEditViewModel> GetEditViewModelAsync(int productId) {
|
||||
var product = await _repository.GetByIdAsync(productId);
|
||||
|
||||
// 分类数据很少变化,但每次都查询
|
||||
var categories = await _categoryRepo.GetAllAsync();
|
||||
|
||||
return new ProductEditViewModel {
|
||||
Product = product,
|
||||
Categories = categories
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 使用 Cache - 智能缓存
|
||||
```csharp
|
||||
public class ProductService {
|
||||
private readonly IRepository<Category> _categoryRepo;
|
||||
private readonly ICacheService _cache;
|
||||
|
||||
public async Task<ProductEditViewModel> GetEditViewModelAsync(int productId) {
|
||||
var product = await _repository.GetByIdAsync(productId);
|
||||
|
||||
// 从缓存获取,不存在则查询并缓存30分钟
|
||||
var categories = await _cache.GetOrSetAsync(
|
||||
"categories:all",
|
||||
() => _categoryRepo.GetAllAsync(),
|
||||
TimeSpan.FromMinutes(30)
|
||||
);
|
||||
|
||||
return new ProductEditViewModel {
|
||||
Product = product,
|
||||
Categories = categories
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- 第一次:查询数据库(耗时 50ms)
|
||||
- 后续访问:从内存读取(耗时 < 1ms)
|
||||
- 用户体验:界面秒开,不再等待
|
||||
|
||||
---
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
1. **极简API** - 一行代码搞定90%的缓存需求
|
||||
2. **类型安全** - 强类型缓存,编译期检查
|
||||
3. **自动过期** - 支持绝对过期和滑动过期
|
||||
4. **引入即用** - 基于 .NET 内置 MemoryCache,零配置
|
||||
5. **线程安全** - 完全线程安全
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始(未来)
|
||||
|
||||
### 1. 注册服务
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 一行注册(基于 Microsoft.Extensions.Caching.Memory)
|
||||
services.AddMemoryCache();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 注入使用
|
||||
```csharp
|
||||
public class WeatherService {
|
||||
private readonly ICacheService _cache;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public WeatherService(ICacheService cache, IHttpClient httpClient) {
|
||||
_cache = cache;
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<Weather> GetWeatherAsync(string city) {
|
||||
// 缓存5分钟,避免频繁调用外部API
|
||||
return await _cache.GetOrSetAsync(
|
||||
$"weather:{city}",
|
||||
() => _httpClient.GetAsync<Weather>($"/api/weather?city={city}"),
|
||||
TimeSpan.FromMinutes(5)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心 API 设计
|
||||
|
||||
### ICacheService 接口
|
||||
```csharp
|
||||
public interface ICacheService {
|
||||
/// <summary>
|
||||
/// 获取缓存值
|
||||
/// </summary>
|
||||
T? Get<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取缓存值
|
||||
/// </summary>
|
||||
bool TryGet<T>(string key, out T? value);
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓存
|
||||
/// </summary>
|
||||
/// <param name="key">缓存键</param>
|
||||
/// <param name="value">缓存值</param>
|
||||
/// <param name="expiration">绝对过期时间(从现在开始计算)</param>
|
||||
void Set<T>(string key, T value, TimeSpan? expiration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 设置缓存(滑动过期)
|
||||
/// </summary>
|
||||
/// <param name="key">缓存键</param>
|
||||
/// <param name="value">缓存值</param>
|
||||
/// <param name="slidingExpiration">滑动过期时间(最后访问后N时间过期)</param>
|
||||
void SetSliding<T>(string key, T value, TimeSpan slidingExpiration);
|
||||
|
||||
/// <summary>
|
||||
/// 移除缓存
|
||||
/// </summary>
|
||||
void Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有缓存
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否存在
|
||||
/// </summary>
|
||||
bool Exists(string key);
|
||||
}
|
||||
```
|
||||
|
||||
### 扩展方法(最常用)
|
||||
```csharp
|
||||
public static class CacheExtensions {
|
||||
/// <summary>
|
||||
/// 获取缓存,不存在则执行工厂方法并缓存结果(同步)
|
||||
/// </summary>
|
||||
public static T GetOrSet<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<T> factory,
|
||||
TimeSpan? expiration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存,不存在则执行工厂方法并缓存结果(异步)
|
||||
/// </summary>
|
||||
public static Task<T> GetOrSetAsync<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<Task<T>> factory,
|
||||
TimeSpan? expiration = null);
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置(滑动过期)
|
||||
/// </summary>
|
||||
public static T GetOrSetSliding<T>(
|
||||
this ICacheService cache,
|
||||
string key,
|
||||
Func<T> factory,
|
||||
TimeSpan slidingExpiration);
|
||||
|
||||
/// <summary>
|
||||
/// 批量移除(支持通配符)
|
||||
/// </summary>
|
||||
public static void RemoveByPattern(
|
||||
this ICacheService cache,
|
||||
string pattern); // 如 "user:*"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 典型使用场景
|
||||
|
||||
### 场景1:字典数据缓存(最常见)
|
||||
```csharp
|
||||
public class UserViewModel : ViewModelBase {
|
||||
private readonly ICacheService _cache;
|
||||
private readonly IRepository<Department> _deptRepo;
|
||||
|
||||
public async Task LoadDepartmentsAsync() {
|
||||
// 部门数据很少变化,缓存1小时
|
||||
Departments = await _cache.GetOrSetAsync(
|
||||
"departments:all",
|
||||
() => _deptRepo.GetAllAsync(),
|
||||
TimeSpan.FromHours(1)
|
||||
);
|
||||
}
|
||||
|
||||
public void OnDepartmentUpdated() {
|
||||
// 手动清除缓存,下次会重新加载
|
||||
_cache.Remove("departments:all");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:API响应缓存
|
||||
```csharp
|
||||
public class WeatherService {
|
||||
private readonly ICacheService _cache;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
// 每个城市的天气数据缓存5分钟
|
||||
public async Task<Weather> GetWeatherAsync(string city) {
|
||||
return await _cache.GetOrSetAsync(
|
||||
$"weather:{city}",
|
||||
async () => {
|
||||
var response = await _httpClient.GetAsync<WeatherApiResponse>(
|
||||
$"https://api.weather.com/v1/current?city={city}");
|
||||
return response.ToWeather();
|
||||
},
|
||||
TimeSpan.FromMinutes(5)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:计算结果缓存
|
||||
```csharp
|
||||
public class ReportService {
|
||||
private readonly ICacheService _cache;
|
||||
|
||||
// 月度报表计算耗时,缓存24小时
|
||||
public async Task<MonthlyReport> GetMonthlyReportAsync(int year, int month) {
|
||||
return await _cache.GetOrSetAsync(
|
||||
$"report:monthly:{year}:{month}",
|
||||
async () => {
|
||||
// 复杂的统计计算
|
||||
var data = await _repository.Query()
|
||||
.Where(x => x.Date.Year == year && x.Date.Month == month)
|
||||
.ToListAsync();
|
||||
|
||||
return CalculateReport(data); // 耗时操作
|
||||
},
|
||||
TimeSpan.FromHours(24)
|
||||
);
|
||||
}
|
||||
|
||||
// 当有新数据时,清除缓存
|
||||
public void OnDataAdded(DateTime date) {
|
||||
_cache.Remove($"report:monthly:{date.Year}:{date.Month}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景4:用户权限缓存
|
||||
```csharp
|
||||
public class AuthorizationService {
|
||||
private readonly ICacheService _cache;
|
||||
|
||||
// 用户权限缓存(滑动过期:30分钟内无访问则过期)
|
||||
public async Task<List<string>> GetUserPermissionsAsync(int userId) {
|
||||
return await _cache.GetOrSetSliding(
|
||||
$"user:permissions:{userId}",
|
||||
async () => {
|
||||
return await _repository.Query<UserPermission>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.PermissionCode)
|
||||
.ToListAsync();
|
||||
},
|
||||
TimeSpan.FromMinutes(30) // 滑动过期
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景5:配置数据缓存
|
||||
```csharp
|
||||
public class SystemConfigService {
|
||||
private readonly ICacheService _cache;
|
||||
private readonly IConfigManager _configManager;
|
||||
|
||||
public T GetConfig<T>(string key) where T : class, new() {
|
||||
return _cache.GetOrSet(
|
||||
$"config:{key}",
|
||||
() => _configManager.Get<T>(key),
|
||||
TimeSpan.FromMinutes(10)
|
||||
);
|
||||
}
|
||||
|
||||
// Config模块有热重载,可以监听变化并清除缓存
|
||||
public void OnConfigChanged(string key) {
|
||||
_cache.Remove($"config:{key}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 缓存键约定
|
||||
```csharp
|
||||
// 推荐的缓存键命名规范
|
||||
public static class CacheKeys {
|
||||
// 格式: {模块}:{实体}:{参数}
|
||||
public static string UserById(int userId) => $"user:profile:{userId}";
|
||||
public static string AllDepartments => "departments:all";
|
||||
public static string WeatherByCity(string city) => $"weather:{city}";
|
||||
public static string ReportMonthly(int year, int month) => $"report:monthly:{year}:{month}";
|
||||
}
|
||||
|
||||
// 使用
|
||||
var user = await _cache.GetOrSetAsync(
|
||||
CacheKeys.UserById(userId),
|
||||
() => _userRepo.GetByIdAsync(userId),
|
||||
TimeSpan.FromMinutes(10)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 缓存依赖(级联清除)
|
||||
```csharp
|
||||
public class ProductService {
|
||||
// 当产品更新时,清除相关缓存
|
||||
public async Task UpdateProductAsync(Product product) {
|
||||
await _repository.UpdateAsync(product);
|
||||
|
||||
// 清除产品详情缓存
|
||||
_cache.Remove($"product:{product.Id}");
|
||||
|
||||
// 清除产品列表缓存(可能包含此产品)
|
||||
_cache.RemoveByPattern("products:list:*");
|
||||
|
||||
// 清除分类统计缓存
|
||||
_cache.Remove($"category:stats:{product.CategoryId}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 缓存预热
|
||||
```csharp
|
||||
public class CacheWarmupService {
|
||||
private readonly ICacheService _cache;
|
||||
|
||||
// 应用启动时预热常用数据
|
||||
public async Task WarmupAsync() {
|
||||
// 加载所有字典数据
|
||||
await _cache.GetOrSetAsync(
|
||||
"departments:all",
|
||||
() => _deptRepo.GetAllAsync(),
|
||||
TimeSpan.FromHours(1)
|
||||
);
|
||||
|
||||
await _cache.GetOrSetAsync(
|
||||
"categories:all",
|
||||
() => _categoryRepo.GetAllAsync(),
|
||||
TimeSpan.FromHours(1)
|
||||
);
|
||||
|
||||
// 加载系统配置
|
||||
_cache.GetOrSet(
|
||||
"system:config",
|
||||
() => _configManager.Get<SystemConfig>(),
|
||||
TimeSpan.FromMinutes(10)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// App.xaml.cs
|
||||
protected override async void OnStartup(StartupEventArgs e) {
|
||||
// ...
|
||||
var warmup = _serviceProvider.GetRequiredService<CacheWarmupService>();
|
||||
await warmup.WarmupAsync(); // 预热缓存
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 缓存统计(可选)
|
||||
```csharp
|
||||
public interface ICacheStatistics {
|
||||
long TotalHits { get; } // 命中次数
|
||||
long TotalMisses { get; } // 未命中次数
|
||||
double HitRate { get; } // 命中率
|
||||
int CurrentItemCount { get; } // 当前缓存项数量
|
||||
}
|
||||
|
||||
// 诊断用
|
||||
public class DiagnosticsViewModel {
|
||||
public void ShowCacheStats() {
|
||||
var stats = _cache.GetStatistics();
|
||||
MessageBox.Show($"缓存命中率: {stats.HitRate:P2}\n" +
|
||||
$"缓存项数量: {stats.CurrentItemCount}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
```csharp
|
||||
services.AddMemoryCache(options => {
|
||||
// 缓存大小限制(单位:MB)
|
||||
options.SizeLimit = 100; // 限制100MB
|
||||
|
||||
// 压缩比例(当达到SizeLimit时,压缩到此比例)
|
||||
options.CompactionPercentage = 0.25; // 压缩到25%
|
||||
|
||||
// 过期扫描频率
|
||||
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 实现方案
|
||||
|
||||
### 基于 Microsoft.Extensions.Caching.Memory
|
||||
- **内置库**:无需额外依赖
|
||||
- **高性能**:基于 ConcurrentDictionary
|
||||
- **线程安全**:完全线程安全
|
||||
- **自动清理**:后台定期清理过期项
|
||||
|
||||
### 架构设计
|
||||
```
|
||||
ICacheService(接口)
|
||||
↓
|
||||
MemoryCacheService(实现)
|
||||
↓ 内部使用
|
||||
Microsoft.Extensions.Caching.Memory.IMemoryCache
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能特点
|
||||
|
||||
- **读取速度**:< 1ms(内存访问)
|
||||
- **线程安全**:无锁并发读取
|
||||
- **内存占用**:可配置上限,自动压缩
|
||||
- **过期清理**:后台自动扫描
|
||||
|
||||
---
|
||||
|
||||
## 🔄 过期策略对比
|
||||
|
||||
### 绝对过期 (Absolute Expiration)
|
||||
```csharp
|
||||
// 从设置时开始,10分钟后过期(无论是否访问)
|
||||
_cache.Set("key", value, TimeSpan.FromMinutes(10));
|
||||
|
||||
// 适用场景:
|
||||
// - API响应缓存(数据有时效性)
|
||||
// - 报表数据(每天更新)
|
||||
// - 字典数据(定期刷新)
|
||||
```
|
||||
|
||||
### 滑动过期 (Sliding Expiration)
|
||||
```csharp
|
||||
// 最后访问后30分钟无访问则过期
|
||||
_cache.SetSliding("key", value, TimeSpan.FromMinutes(30));
|
||||
|
||||
// 适用场景:
|
||||
// - 用户会话数据
|
||||
// - 用户权限缓存
|
||||
// - 热点数据(频繁访问的数据)
|
||||
```
|
||||
|
||||
### 组合使用
|
||||
```csharp
|
||||
// 滑动30分钟,但最多缓存2小时
|
||||
services.AddMemoryCache();
|
||||
|
||||
_cache.Set("key", value, new MemoryCacheEntryOptions {
|
||||
SlidingExpiration = TimeSpan.FromMinutes(30),
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💾 缓存大小管理
|
||||
|
||||
### 设置项大小
|
||||
```csharp
|
||||
_cache.Set("large-data", value, new MemoryCacheEntryOptions {
|
||||
Size = 1, // 占用1个单位(Size单位由SizeLimit定义)
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
|
||||
});
|
||||
```
|
||||
|
||||
### 优先级
|
||||
```csharp
|
||||
// 低优先级:内存不足时优先清除
|
||||
_cache.Set("low-priority", value, new MemoryCacheEntryOptions {
|
||||
Priority = CacheItemPriority.Low
|
||||
});
|
||||
|
||||
// 高优先级:内存不足时最后清除
|
||||
_cache.Set("high-priority", value, new MemoryCacheEntryOptions {
|
||||
Priority = CacheItemPriority.High
|
||||
});
|
||||
|
||||
// 永不移除(谨慎使用)
|
||||
_cache.Set("never-remove", value, new MemoryCacheEntryOptions {
|
||||
Priority = CacheItemPriority.NeverRemove
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 最佳实践
|
||||
|
||||
### 1. 缓存键命名规范
|
||||
```csharp
|
||||
✅ 好的命名
|
||||
"user:profile:123"
|
||||
"product:details:456"
|
||||
"report:monthly:2024:12"
|
||||
|
||||
❌ 不好的命名
|
||||
"user123"
|
||||
"prod"
|
||||
"report1"
|
||||
```
|
||||
|
||||
### 2. 合理设置过期时间
|
||||
```csharp
|
||||
// 根据数据变化频率设置
|
||||
字典数据:1小时 - 1天
|
||||
用户信息:10分钟 - 30分钟
|
||||
API响应:5分钟 - 15分钟
|
||||
统计数据:1小时 - 24小时
|
||||
```
|
||||
|
||||
### 3. 避免缓存过大对象
|
||||
```csharp
|
||||
❌ 不推荐:缓存整个DataTable
|
||||
_cache.Set("all-products", largeDataTable);
|
||||
|
||||
✅ 推荐:缓存DTO列表
|
||||
_cache.Set("all-products", productDtos);
|
||||
```
|
||||
|
||||
### 4. 及时清除失效缓存
|
||||
```csharp
|
||||
public async Task UpdateUserAsync(User user) {
|
||||
await _repository.UpdateAsync(user);
|
||||
|
||||
// 立即清除缓存
|
||||
_cache.Remove($"user:profile:{user.Id}");
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 使用Result<T>模式
|
||||
```csharp
|
||||
public async Task<Result<Product>> GetProductAsync(int id) {
|
||||
return await _cache.GetOrSetAsync(
|
||||
$"product:{id}",
|
||||
async () => {
|
||||
var product = await _repository.GetByIdAsync(id);
|
||||
return product != null
|
||||
? Result<Product>.Success(product)
|
||||
: Result<Product>.Failure("产品不存在");
|
||||
},
|
||||
TimeSpan.FromMinutes(10)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 可能的扩展方向
|
||||
1. **分布式缓存**(基于 Redis)
|
||||
2. **多级缓存**(L1: Memory, L2: Redis)
|
||||
3. **缓存预加载**(后台任务)
|
||||
4. **缓存同步**(多实例间同步)
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考资料
|
||||
|
||||
- [Microsoft.Extensions.Caching.Memory](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/memory) - 官方文档
|
||||
- [IMemoryCache](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.caching.memory.imemorycache) - API文档
|
||||
- [缓存最佳实践](https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching) - Azure架构指南
|
||||
|
||||
---
|
||||
|
||||
**状态**:设计阶段,未实现
|
||||
**预计实现时间**:根据实际需求决定
|
||||
**实现难度**:低(100行代码即可完成核心功能)
|
||||
**依赖**:Microsoft.Extensions.Caching.Memory(.NET内置)
|
||||
@@ -14,6 +14,8 @@ using Sharp.Wpf.Services;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Plc.Siemens.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Sharp.Process.DependencyInjection;
|
||||
using Sharp.Python.DependencyInjection;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
@@ -105,9 +107,13 @@ namespace Client
|
||||
options.CpuType = S7.Net.CpuType.S71200;
|
||||
// 配置监控循环频率 (单位: 毫秒)
|
||||
// 默认 200ms。如果设为 100ms 则频率更高,但需注意网络负载。
|
||||
options.MonitorInterval = 200;
|
||||
options.MonitorInterval = 200;
|
||||
});
|
||||
|
||||
// Process & Python
|
||||
services.AddSharpProcess();
|
||||
services.AddSharpPython();
|
||||
|
||||
// WPF app services
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<IAuthenticationService, DemoAuthenticationService>();
|
||||
@@ -118,10 +124,12 @@ namespace Client
|
||||
services.AddTransient<LoginViewModel>();
|
||||
services.AddTransient<MainViewModel>();
|
||||
services.AddTransient<PlcViewModel>();
|
||||
services.AddTransient<ProcessTestViewModel>();
|
||||
|
||||
services.AddTransient<LoginWindow>();
|
||||
services.AddTransient<MainWindow>();
|
||||
services.AddTransient<PlcWindow>();
|
||||
services.AddTransient<ProcessTestWindow>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
<ProjectReference Include="..\Sharp.Infrastructure\Sharp.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\Plc.Siemens\Plc.Siemens.csproj" />
|
||||
<ProjectReference Include="..\Ports\Ports.csproj" />
|
||||
<ProjectReference Include="..\Process\Process.csproj" />
|
||||
<ProjectReference Include="..\Python\Python.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -49,6 +49,11 @@
|
||||
Padding="18,6"
|
||||
Margin="0,0,12,0"
|
||||
Command="{Binding ThrowCommand}" />
|
||||
<Button Content="Process/Python 测试"
|
||||
Padding="18,6"
|
||||
Margin="0,0,12,0"
|
||||
Style="{StaticResource ButtonWarning}"
|
||||
Command="{Binding OpenProcessTestCommand}" />
|
||||
<Button Content="串口调试"
|
||||
Padding="18,6"
|
||||
Margin="0,0,12,0"
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
<hc:GlowWindow x:Class="Client.ProcessTestWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||
mc:Ignorable="d"
|
||||
Title="Process / Python 测试工具" Height="700" Width="1000"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<hc:GlowWindow.Resources>
|
||||
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
||||
</hc:GlowWindow.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="300"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 主测试区域 -->
|
||||
<TabControl Grid.Column="0" Margin="0,0,10,0">
|
||||
<!-- Process 测试标签页 -->
|
||||
<TabItem Header="Process 测试">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="15">
|
||||
<!-- 状态栏 -->
|
||||
<Border Background="{DynamicResource PrimaryBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||
<TextBlock Text="{Binding ProcessStatus}" Foreground="White" FontWeight="Bold"/>
|
||||
</Border>
|
||||
|
||||
<!-- 命令输入 -->
|
||||
<GroupBox Header="命令配置" Margin="0,0,0,15">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Text="命令:" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Command}" Margin="10,0,0,0"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="参数:" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Arguments}" Margin="10,10,0,0"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="超时(秒):" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||
<hc:NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding TimeoutSeconds}"
|
||||
Minimum="1" Maximum="600" Margin="10,10,0,0"/>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 快捷按钮 -->
|
||||
<GroupBox Header="快速测试" Margin="0,0,0,15">
|
||||
<WrapPanel Margin="10" Orientation="Horizontal">
|
||||
<Button Content="执行命令" Command="{Binding RunProcessCommand}"
|
||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||
<Button Content="Shell 命令" Command="{Binding RunShellCommand}"
|
||||
Margin="0,0,10,10" Padding="15,8"/>
|
||||
<Button Content="测试 dotnet" Command="{Binding TestDotnetCommand}"
|
||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||
<Button Content="测试 Git" Command="{Binding TestGitCommand}"
|
||||
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||
</WrapPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 输出 -->
|
||||
<GroupBox Header="执行输出">
|
||||
<TextBox Text="{Binding ProcessOutput, Mode=OneWay}"
|
||||
Height="300"
|
||||
IsReadOnly="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"
|
||||
Margin="10"/>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- Python 测试标签页 -->
|
||||
<TabItem Header="Python 测试">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="15">
|
||||
<!-- Python 环境信息 -->
|
||||
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="Python 环境" FontSize="16" FontWeight="Bold"/>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<TextBlock Text="路径: " FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding PythonPath}" Margin="5,0,0,0"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5,0,0">
|
||||
<TextBlock Text="版本: " FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding PythonVersion}" Margin="5,0,20,0"/>
|
||||
<Button Content="重新检测" Command="{Binding DetectPythonCommand}" Padding="10,4"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<Border Background="{DynamicResource SuccessBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||
<TextBlock Text="{Binding PythonStatus}" Foreground="White" FontWeight="Bold"/>
|
||||
</Border>
|
||||
|
||||
<!-- 代码执行 -->
|
||||
<GroupBox Header="Python 代码执行" Margin="0,0,0,15">
|
||||
<StackPanel Margin="10">
|
||||
<TextBlock Text="Python 代码:" Margin="0,0,0,5"/>
|
||||
<TextBox Text="{Binding PythonCode}"
|
||||
Height="80"
|
||||
TextWrapping="Wrap"
|
||||
AcceptsReturn="True"
|
||||
FontFamily="Consolas"
|
||||
Margin="0,0,0,10"/>
|
||||
<WrapPanel>
|
||||
<Button Content="执行代码" Command="{Binding RunPythonCodeCommand}"
|
||||
Margin="0,0,10,0" Padding="15,8" Style="{StaticResource ButtonSuccess}"/>
|
||||
<Button Content="测试 JSON 输出" Command="{Binding TestPythonJsonCommand}"
|
||||
Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 脚本执行 -->
|
||||
<GroupBox Header="Python 脚本执行">
|
||||
<StackPanel Margin="10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Text="{Binding PythonScriptPath}"
|
||||
hc:InfoElement.Placeholder="脚本文件路径"/>
|
||||
<Button Grid.Column="1" Content="创建测试脚本"
|
||||
Command="{Binding CreateTestScriptCommand}"
|
||||
Margin="5,0,0,0" Padding="10,4"/>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Text="参数 (格式: key1=value1,key2=value2):" Margin="0,10,0,5"/>
|
||||
<TextBox Text="{Binding PythonArgs}"
|
||||
hc:InfoElement.Placeholder="例如: name=Sharp,count=3"
|
||||
Margin="0,0,0,10"/>
|
||||
|
||||
<Button Content="执行脚本" Command="{Binding RunPythonScriptCommand}"
|
||||
Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- Python 输出 -->
|
||||
<GroupBox Header="Python 输出" Margin="0,15,0,0">
|
||||
<TextBox Text="{Binding PythonOutput, Mode=OneWay}"
|
||||
Height="200"
|
||||
IsReadOnly="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
FontFamily="Consolas"
|
||||
Margin="10"/>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- 日志面板 -->
|
||||
<Border Grid.Column="1" Background="{DynamicResource RegionBrush}"
|
||||
Padding="15" CornerRadius="8">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Text="执行日志" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||
|
||||
<ListBox Grid.Row="1" ItemsSource="{Binding LogMessages}"
|
||||
FontFamily="Consolas" FontSize="10"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="2"/>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
</ListBox>
|
||||
|
||||
<Button Grid.Row="2" Content="清空日志" Command="{Binding ClearLogCommand}"
|
||||
Margin="0,10,0,0" HorizontalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</hc:GlowWindow>
|
||||
@@ -0,0 +1,15 @@
|
||||
using Client.ViewModels;
|
||||
|
||||
namespace Client;
|
||||
|
||||
/// <summary>
|
||||
/// ProcessTestWindow.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ProcessTestWindow
|
||||
{
|
||||
public ProcessTestWindow(ProcessTestViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,13 @@ public sealed class MainViewModel : ViewModelBase
|
||||
win.Show();
|
||||
});
|
||||
|
||||
OpenProcessTestCommand = new RelayCommand(() =>
|
||||
{
|
||||
var sp = Global.Get<IServiceProvider>();
|
||||
var win = sp.GetRequiredService<ProcessTestWindow>();
|
||||
win.Show();
|
||||
});
|
||||
|
||||
// 1. 获取 PLC 服务
|
||||
var plcService = Global.Get<IServiceProvider>().GetRequiredService<IPlcService>();
|
||||
|
||||
@@ -109,6 +116,7 @@ public sealed class MainViewModel : ViewModelBase
|
||||
public ICommand ThrowCommand { get; }
|
||||
public ICommand OpenPlcToolCommand { get; }
|
||||
public ICommand OpenSerialPortToolCommand { get; }
|
||||
public ICommand OpenProcessTestCommand { get; }
|
||||
public ICommand StartMachineCommand { get; }
|
||||
public ICommand StopMachineCommand { get; }
|
||||
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows.Input;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Commands;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Sharp.Process;
|
||||
using Sharp.Python;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Client.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Process/Python 测试工具 ViewModel
|
||||
/// </summary>
|
||||
public class ProcessTestViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IProcessExecutor _processExecutor;
|
||||
private readonly IPythonExecutor _pythonExecutor;
|
||||
private readonly IDispatcher _dispatcher;
|
||||
private readonly ILogger<ProcessTestViewModel> _logger;
|
||||
|
||||
// Process 测试
|
||||
private string _command = "cmd";
|
||||
private string _arguments = "/c echo Hello from Process!";
|
||||
private int _timeoutSeconds = 30;
|
||||
private string _processOutput = "输出将显示在这里...";
|
||||
private string _processStatus = "就绪";
|
||||
|
||||
// Python 测试
|
||||
private string _pythonPath = "(自动检测)";
|
||||
private string _pythonVersion = "未检测";
|
||||
private string _pythonCode = "print('Hello from Python!')";
|
||||
private string _pythonScriptPath = "";
|
||||
private string _pythonArgs = "";
|
||||
private string _pythonOutput = "输出将显示在这里...";
|
||||
private string _pythonStatus = "就绪";
|
||||
|
||||
// 日志
|
||||
public ObservableCollection<string> LogMessages { get; } = new();
|
||||
|
||||
public ProcessTestViewModel(
|
||||
IProcessExecutor processExecutor,
|
||||
IPythonExecutor pythonExecutor,
|
||||
IDispatcher dispatcher,
|
||||
ILogger<ProcessTestViewModel> logger)
|
||||
{
|
||||
_processExecutor = processExecutor;
|
||||
_pythonExecutor = pythonExecutor;
|
||||
_dispatcher = dispatcher;
|
||||
_logger = logger;
|
||||
|
||||
// Process 命令
|
||||
RunProcessCommand = new AsyncCommand(RunProcessAsync);
|
||||
RunShellCommand = new AsyncCommand(RunShellAsync);
|
||||
TestDotnetCommand = new AsyncCommand(TestDotnetAsync);
|
||||
TestGitCommand = new AsyncCommand(TestGitAsync);
|
||||
|
||||
// Python 命令
|
||||
DetectPythonCommand = new AsyncCommand(DetectPythonAsync);
|
||||
RunPythonCodeCommand = new AsyncCommand(RunPythonCodeAsync);
|
||||
RunPythonScriptCommand = new AsyncCommand(RunPythonScriptAsync);
|
||||
TestPythonJsonCommand = new AsyncCommand(TestPythonJsonAsync);
|
||||
CreateTestScriptCommand = new RelayCommand(CreateTestScript);
|
||||
|
||||
// 其他
|
||||
ClearLogCommand = new RelayCommand(() => LogMessages.Clear());
|
||||
|
||||
// 自动检测 Python
|
||||
_ = DetectPythonAsync();
|
||||
}
|
||||
|
||||
#region Properties
|
||||
|
||||
public string Command
|
||||
{
|
||||
get => _command;
|
||||
set => SetProperty(ref _command, value);
|
||||
}
|
||||
|
||||
public string Arguments
|
||||
{
|
||||
get => _arguments;
|
||||
set => SetProperty(ref _arguments, value);
|
||||
}
|
||||
|
||||
public int TimeoutSeconds
|
||||
{
|
||||
get => _timeoutSeconds;
|
||||
set => SetProperty(ref _timeoutSeconds, value);
|
||||
}
|
||||
|
||||
public string ProcessOutput
|
||||
{
|
||||
get => _processOutput;
|
||||
set => SetProperty(ref _processOutput, value);
|
||||
}
|
||||
|
||||
public string ProcessStatus
|
||||
{
|
||||
get => _processStatus;
|
||||
set => SetProperty(ref _processStatus, value);
|
||||
}
|
||||
|
||||
public string PythonPath
|
||||
{
|
||||
get => _pythonPath;
|
||||
set => SetProperty(ref _pythonPath, value);
|
||||
}
|
||||
|
||||
public string PythonVersion
|
||||
{
|
||||
get => _pythonVersion;
|
||||
set => SetProperty(ref _pythonVersion, value);
|
||||
}
|
||||
|
||||
public string PythonCode
|
||||
{
|
||||
get => _pythonCode;
|
||||
set => SetProperty(ref _pythonCode, value);
|
||||
}
|
||||
|
||||
public string PythonScriptPath
|
||||
{
|
||||
get => _pythonScriptPath;
|
||||
set => SetProperty(ref _pythonScriptPath, value);
|
||||
}
|
||||
|
||||
public string PythonArgs
|
||||
{
|
||||
get => _pythonArgs;
|
||||
set => SetProperty(ref _pythonArgs, value);
|
||||
}
|
||||
|
||||
public string PythonOutput
|
||||
{
|
||||
get => _pythonOutput;
|
||||
set => SetProperty(ref _pythonOutput, value);
|
||||
}
|
||||
|
||||
public string PythonStatus
|
||||
{
|
||||
get => _pythonStatus;
|
||||
set => SetProperty(ref _pythonStatus, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Commands
|
||||
|
||||
public IAsyncCommand RunProcessCommand { get; }
|
||||
public IAsyncCommand RunShellCommand { get; }
|
||||
public IAsyncCommand TestDotnetCommand { get; }
|
||||
public IAsyncCommand TestGitCommand { get; }
|
||||
|
||||
public IAsyncCommand DetectPythonCommand { get; }
|
||||
public IAsyncCommand RunPythonCodeCommand { get; }
|
||||
public IAsyncCommand RunPythonScriptCommand { get; }
|
||||
public IAsyncCommand TestPythonJsonCommand { get; }
|
||||
public ICommand CreateTestScriptCommand { get; }
|
||||
|
||||
public ICommand ClearLogCommand { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Process 测试
|
||||
|
||||
private async Task RunProcessAsync()
|
||||
{
|
||||
ProcessStatus = "执行中...";
|
||||
ProcessOutput = "";
|
||||
AddLog($"[Process] 执行命令: {Command} {Arguments}");
|
||||
|
||||
try
|
||||
{
|
||||
var options = new ProcessOptions
|
||||
{
|
||||
Command = Command,
|
||||
Arguments = Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
||||
OnOutputReceived = line =>
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
ProcessOutput += line + "\n";
|
||||
OnPropertyChanged(nameof(ProcessOutput));
|
||||
AddLog($"[输出] {line}");
|
||||
});
|
||||
},
|
||||
OnErrorReceived = line =>
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
ProcessOutput += $"[错误] {line}\n";
|
||||
OnPropertyChanged(nameof(ProcessOutput));
|
||||
AddLog($"[错误] {line}");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _processExecutor.ExecuteAsync(options);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ProcessStatus = $"✓ 完成 (退出码: {result.Value.ExitCode}, 耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||
ProcessOutput += $"\n=== 执行成功 ===\n退出码: {result.Value.ExitCode}\n耗时: {result.Value.Duration.TotalSeconds:F2}s";
|
||||
AddLog($"[Process] 执行成功 (耗时: {result.Value.Duration.TotalMilliseconds}ms)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessStatus = $"✗ 失败: {result.ErrorMessage}";
|
||||
ProcessOutput += $"\n=== 执行失败 ===\n{result.ErrorMessage}";
|
||||
AddLog($"[Process] 执行失败: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||
ProcessOutput = $"异常: {ex.Message}\n{ex.StackTrace}";
|
||||
AddLog($"[Process] 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunShellAsync()
|
||||
{
|
||||
ProcessStatus = "执行中...";
|
||||
ProcessOutput = "";
|
||||
|
||||
var shellCmd = OperatingSystem.IsWindows()
|
||||
? "dir | findstr .cs"
|
||||
: "ls | grep .cs";
|
||||
|
||||
AddLog($"[Shell] 执行: {shellCmd}");
|
||||
|
||||
try
|
||||
{
|
||||
var result = await IProcessExecutor.RunShellAsync(shellCmd);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ProcessOutput = result.Value.StandardOutput;
|
||||
ProcessStatus = $"✓ Shell 命令执行成功";
|
||||
AddLog($"[Shell] 成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessOutput = result.ErrorMessage ?? "未知错误";
|
||||
ProcessStatus = $"✗ Shell 命令失败";
|
||||
AddLog($"[Shell] 失败");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||
ProcessOutput = ex.Message;
|
||||
AddLog($"[Shell] 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestDotnetAsync()
|
||||
{
|
||||
Command = "dotnet";
|
||||
Arguments = "--version";
|
||||
await RunProcessAsync();
|
||||
}
|
||||
|
||||
private async Task TestGitAsync()
|
||||
{
|
||||
Command = "git";
|
||||
Arguments = "--version";
|
||||
await RunProcessAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Python 测试
|
||||
|
||||
private async Task DetectPythonAsync()
|
||||
{
|
||||
PythonStatus = "检测中...";
|
||||
AddLog("[Python] 正在检测 Python 环境...");
|
||||
|
||||
try
|
||||
{
|
||||
var env = PythonEnvironment.Discover();
|
||||
if (env != null)
|
||||
{
|
||||
PythonPath = env.PythonPath;
|
||||
PythonVersion = env.Version ?? "未知版本";
|
||||
PythonStatus = "✓ Python 环境已检测";
|
||||
AddLog($"[Python] 找到 Python: {env.PythonPath}");
|
||||
AddLog($"[Python] 版本: {env.Version}");
|
||||
}
|
||||
else
|
||||
{
|
||||
PythonPath = "未找到";
|
||||
PythonVersion = "未安装";
|
||||
PythonStatus = "✗ 未找到 Python";
|
||||
AddLog("[Python] 未找到 Python 环境,请安装 Python 或配置 PATH");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PythonStatus = $"✗ 检测失败: {ex.Message}";
|
||||
AddLog($"[Python] 检测异常: {ex.Message}");
|
||||
}
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RunPythonCodeAsync()
|
||||
{
|
||||
PythonStatus = "执行中...";
|
||||
PythonOutput = "";
|
||||
AddLog($"[Python] 执行代码: {PythonCode}");
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _pythonExecutor.RunCodeAsync(PythonCode, new PythonOptions
|
||||
{
|
||||
OnOutputReceived = line =>
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
PythonOutput += line + "\n";
|
||||
OnPropertyChanged(nameof(PythonOutput));
|
||||
AddLog($"[Python 输出] {line}");
|
||||
});
|
||||
},
|
||||
OnErrorReceived = line =>
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
PythonOutput += $"[错误] {line}\n";
|
||||
OnPropertyChanged(nameof(PythonOutput));
|
||||
AddLog($"[Python 错误] {line}");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PythonOutput))
|
||||
{
|
||||
PythonOutput = result.Value.StandardOutput;
|
||||
}
|
||||
PythonStatus = $"✓ 执行成功 (耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||
AddLog($"[Python] 执行成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||
PythonStatus = $"✗ 执行失败";
|
||||
AddLog($"[Python] 执行失败: {result.ErrorMessage}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||
PythonOutput = ex.Message;
|
||||
AddLog($"[Python] 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunPythonScriptAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PythonScriptPath) || !File.Exists(PythonScriptPath))
|
||||
{
|
||||
PythonStatus = "✗ 请选择有效的脚本文件";
|
||||
return;
|
||||
}
|
||||
|
||||
PythonStatus = "执行中...";
|
||||
PythonOutput = "";
|
||||
AddLog($"[Python] 执行脚本: {PythonScriptPath}");
|
||||
|
||||
try
|
||||
{
|
||||
// 解析参数(简单的 key=value 格式)
|
||||
object? args = null;
|
||||
if (!string.IsNullOrWhiteSpace(PythonArgs))
|
||||
{
|
||||
var argsDict = new Dictionary<string, string>();
|
||||
var pairs = PythonArgs.Split(',', StringSplitOptions.TrimEntries);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
var kv = pair.Split('=', 2);
|
||||
if (kv.Length == 2)
|
||||
{
|
||||
argsDict[kv[0].Trim()] = kv[1].Trim();
|
||||
}
|
||||
}
|
||||
args = argsDict;
|
||||
}
|
||||
|
||||
var result = await _pythonExecutor.RunScriptAsync(
|
||||
PythonScriptPath,
|
||||
args,
|
||||
new PythonOptions
|
||||
{
|
||||
OnOutputReceived = line =>
|
||||
{
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
PythonOutput += line + "\n";
|
||||
OnPropertyChanged(nameof(PythonOutput));
|
||||
AddLog($"[Script] {line}");
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
if (string.IsNullOrEmpty(PythonOutput))
|
||||
{
|
||||
PythonOutput = result.Value.StandardOutput;
|
||||
}
|
||||
PythonStatus = $"✓ 脚本执行成功";
|
||||
AddLog($"[Python] 脚本执行成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||
PythonStatus = $"✗ 脚本执行失败";
|
||||
AddLog($"[Python] 脚本执行失败");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||
PythonOutput = ex.Message;
|
||||
AddLog($"[Python] 异常: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TestPythonJsonAsync()
|
||||
{
|
||||
PythonCode = @"import json
|
||||
result = {'status': 'ok', 'value': 42, 'message': '测试成功'}
|
||||
print(json.dumps(result, ensure_ascii=False))";
|
||||
await RunPythonCodeAsync();
|
||||
}
|
||||
|
||||
private void CreateTestScript()
|
||||
{
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), "sharp_test.py");
|
||||
|
||||
var scriptContent = @"import sys
|
||||
import json
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--name', default='World')
|
||||
parser.add_argument('--count', type=int, default=1)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 日志输出到 stderr
|
||||
print(f'参数: name={args.name}, count={args.count}', file=sys.stderr)
|
||||
|
||||
# 处理
|
||||
for i in range(args.count):
|
||||
print(f'Hello, {args.name}! ({i+1})', file=sys.stderr)
|
||||
|
||||
# 结果输出到 stdout(JSON)
|
||||
result = {
|
||||
'message': f'Greeted {args.name} {args.count} times',
|
||||
'success': True,
|
||||
'timestamp': str(__import__('datetime').datetime.now())
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
";
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tempPath, scriptContent);
|
||||
PythonScriptPath = tempPath;
|
||||
PythonArgs = "name=Sharp,count=3";
|
||||
PythonStatus = $"✓ 测试脚本已创建: {tempPath}";
|
||||
AddLog($"[Python] 测试脚本已创建: {tempPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PythonStatus = $"✗ 创建脚本失败: {ex.Message}";
|
||||
AddLog($"[Python] 创建脚本失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void AddLog(string message)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||
_dispatcher.Invoke(() =>
|
||||
{
|
||||
LogMessages.Add($"[{timestamp}] {message}");
|
||||
|
||||
// 保持日志数量在合理范围
|
||||
while (LogMessages.Count > 200)
|
||||
{
|
||||
LogMessages.RemoveAt(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+de1a29c522c231e011e0b795cb1494b1dd7282bf")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+433e206d456d0ef3b2fc57c458ff3fc030643be6")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
b775c782e822e9da489917907ac5dc67ababf8e2b94747f6176fcf3d21ac9454
|
||||
de45fc2ddda59d691d6bfd3f2673675ae92a6f2f53cc261c9aeefe56c546545d
|
||||
@@ -6,4 +6,11 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 快速启动扩展,提供"零配置"的便捷注册方法
|
||||
/// </summary>
|
||||
public static class QuickStartExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp 框架的所有基础服务(一行搞定)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置(可选,不传则自动创建默认配置)</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpEssentials(
|
||||
this IServiceCollection services,
|
||||
IConfiguration? configuration = null)
|
||||
{
|
||||
// 1. 配置管理(如果没传就用默认)
|
||||
// Note: ConfigBuilder.CreateDefault() 在 Config 模块中
|
||||
// 这里假设用户已经传入 configuration 或在外部创建
|
||||
if (configuration != null)
|
||||
{
|
||||
services.AddSingleton(configuration);
|
||||
}
|
||||
|
||||
// 2. 缓存服务(使用 Cache 模块的扩展方法)
|
||||
// Note: AddSharpCache() 在 Cache.DependencyInjection 中定义
|
||||
services.AddMemoryCache();
|
||||
|
||||
// 3. 全局服务定位器(兼容性)
|
||||
if (configuration != null)
|
||||
{
|
||||
RegisterGlobalServices(services, configuration);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册到 Global 容器(向后兼容)
|
||||
/// </summary>
|
||||
private static void RegisterGlobalServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// 构建临时 ServiceProvider
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// 注册到 Global
|
||||
Global.RegisterSingleton(configuration);
|
||||
Global.RegisterSingleton(provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Config;
|
||||
|
||||
public static class ConfigBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建默认配置(约定优于配置)
|
||||
/// 自动查找 appsettings.json,启用热重载和环境变量
|
||||
/// </summary>
|
||||
/// <param name="basePath">基础路径,默认为应用程序目录</param>
|
||||
/// <param name="environmentPrefix">环境变量前缀,默认为 "APP_"</param>
|
||||
/// <returns>配置管理器</returns>
|
||||
public static ConfigManager CreateDefault(
|
||||
string? basePath = null,
|
||||
string? environmentPrefix = "APP_")
|
||||
{
|
||||
basePath ??= AppContext.BaseDirectory;
|
||||
|
||||
// 查找配置文件(appsettings.json, appsettings.Development.json 等)
|
||||
var configFiles = new List<string> { "appsettings.json" };
|
||||
|
||||
// 根据环境添加特定配置
|
||||
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
|
||||
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
|
||||
?? "Production";
|
||||
|
||||
var envConfigFile = $"appsettings.{environment}.json";
|
||||
var envConfigPath = Path.Combine(basePath, envConfigFile);
|
||||
if (File.Exists(envConfigPath))
|
||||
{
|
||||
configFiles.Add(envConfigFile);
|
||||
}
|
||||
|
||||
// 构建配置
|
||||
return ConfigBuilder.BuildFromFiles(
|
||||
configFiles.ToArray(),
|
||||
reloadOnChange: true,
|
||||
includeEnvironment: true,
|
||||
environmentPrefix: environmentPrefix,
|
||||
basePath: basePath
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建最小化配置(只有 appsettings.json)
|
||||
/// </summary>
|
||||
public static ConfigManager CreateMinimal(string? basePath = null)
|
||||
{
|
||||
basePath ??= AppContext.BaseDirectory;
|
||||
|
||||
return ConfigBuilder.BuildFromFiles(
|
||||
new[] { "appsettings.json" },
|
||||
reloadOnChange: false,
|
||||
includeEnvironment: false,
|
||||
basePath: basePath
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- EF Core -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||
|
||||
<!-- 数据库提供者 -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||
|
||||
<!-- 依赖注入 -->
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data.Repository;
|
||||
|
||||
namespace Data.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 数据访问层便捷注册扩展
|
||||
/// </summary>
|
||||
public static class DataServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp 数据库支持(约定优于配置)
|
||||
/// 自动从配置读取数据库连接信息
|
||||
/// </summary>
|
||||
/// <typeparam name="TContext">DbContext 类型</typeparam>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
/// <param name="configSection">配置节名称,默认为 "Database"</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configSection = "Database")
|
||||
where TContext : DbContext
|
||||
{
|
||||
var provider = configuration[$"{configSection}:Provider"] ?? "SqlServer";
|
||||
var connectionString = configuration[$"{configSection}:ConnectionString"]
|
||||
?? throw new InvalidOperationException(
|
||||
$"未找到数据库连接字符串配置: {configSection}:ConnectionString");
|
||||
|
||||
return AddSharpDatabase<TContext>(services, provider, connectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Sharp 数据库支持(显式指定提供程序和连接字符串)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||
this IServiceCollection services,
|
||||
string provider,
|
||||
string connectionString)
|
||||
where TContext : DbContext
|
||||
{
|
||||
services.AddDbContext<TContext>(options =>
|
||||
{
|
||||
switch (provider.ToLower())
|
||||
{
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
options.UseSqlServer(connectionString, sqlOptions =>
|
||||
{
|
||||
sqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 3,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(5),
|
||||
errorNumbersToAdd: null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "sqlite":
|
||||
options.UseSqlite(connectionString);
|
||||
break;
|
||||
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "mysql":
|
||||
var serverVersion = ServerVersion.AutoDetect(connectionString);
|
||||
options.UseMySql(connectionString, serverVersion, mysqlOptions =>
|
||||
{
|
||||
mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "inmemory":
|
||||
options.UseInMemoryDatabase(connectionString);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"不支持的数据库提供程序: {provider}");
|
||||
}
|
||||
|
||||
// 开发环境启用详细错误
|
||||
if (Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Development")
|
||||
{
|
||||
options.EnableDetailedErrors();
|
||||
options.EnableSensitiveDataLogging();
|
||||
}
|
||||
});
|
||||
|
||||
// 注册仓储
|
||||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace EventBus
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Sharp.EventBus.DependencyInjection;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddEventBus(this IServiceCollection services, Action<EventBusOptions>? configure = null)
|
||||
{
|
||||
var options = new EventBusOptions();
|
||||
configure?.Invoke(options);
|
||||
services.AddSingleton(options);
|
||||
services.AddSingleton<IEventBus, InMemoryEventBus>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,12 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Sharp.EventBus</AssemblyName>
|
||||
<RootNamespace>Sharp.EventBus</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
|
||||
namespace Sharp.EventBus;
|
||||
|
||||
public class EventBusOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 事件队列容量,默认 1024;满时 Publish 会等待。
|
||||
/// </summary>
|
||||
public int ChannelCapacity { get; set; } = 1024;
|
||||
|
||||
/// <summary>
|
||||
/// 全局错误回调。
|
||||
/// </summary>
|
||||
public Action<Exception, object>? OnError { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sharp.EventBus;
|
||||
|
||||
internal sealed class EventSubscription : IDisposable
|
||||
{
|
||||
public Type EventType { get; }
|
||||
public int Priority { get; }
|
||||
public Func<object, Task> Handler { get; }
|
||||
public Func<object, bool>? Filter { get; }
|
||||
private readonly Action<EventSubscription> _onDispose;
|
||||
private bool _disposed;
|
||||
|
||||
public EventSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority, Action<EventSubscription> onDispose)
|
||||
{
|
||||
EventType = eventType;
|
||||
Handler = handler;
|
||||
Filter = filter;
|
||||
Priority = priority;
|
||||
_onDispose = onDispose;
|
||||
}
|
||||
|
||||
public bool CanHandle(object evt)
|
||||
{
|
||||
if (evt.GetType() != EventType) return false;
|
||||
return Filter == null || Filter(evt);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_onDispose(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Sharp.EventBus;
|
||||
|
||||
public interface IEventBus : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步发布事件(入队后立即返回)。
|
||||
/// </summary>
|
||||
Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 同步包装,遇到队列满或取消会抛出。
|
||||
/// </summary>
|
||||
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(同步处理器)。
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(异步处理器)。
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace Sharp.EventBus;
|
||||
|
||||
/// <summary>
|
||||
/// 基于 Channel 的进程内事件总线,线程安全,按优先级顺序分发。
|
||||
/// </summary>
|
||||
public sealed class InMemoryEventBus : IEventBus
|
||||
{
|
||||
private readonly Channel<object> _channel;
|
||||
private readonly ConcurrentDictionary<Type, List<EventSubscription>> _subscriptions = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _worker;
|
||||
private readonly EventBusOptions _options;
|
||||
|
||||
public InMemoryEventBus(EventBusOptions? options = null)
|
||||
{
|
||||
_options = options ?? new EventBusOptions();
|
||||
_channel = Channel.CreateBounded<object>(new BoundedChannelOptions(_options.ChannelCapacity)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.Wait
|
||||
});
|
||||
_worker = Task.Run(ProcessAsync, _cts.Token);
|
||||
}
|
||||
|
||||
public void Publish<TEvent>(TEvent @event) where TEvent : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_channel.Writer.WriteAsync(@event, _cts.Token).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public Task PublishAsync<TEvent>(TEvent @event, CancellationToken cancellationToken = default) where TEvent : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
return _channel.Writer.WriteAsync(@event, cancellationToken).AsTask();
|
||||
}
|
||||
|
||||
public IDisposable Subscribe<TEvent>(Action<TEvent> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
Task Wrapper(object evt)
|
||||
{
|
||||
handler((TEvent)evt);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||
}
|
||||
|
||||
public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler, Func<TEvent, bool>? filter = null, int priority = 0) where TEvent : class
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
if (handler == null) throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
Task Wrapper(object evt) => handler((TEvent)evt);
|
||||
|
||||
return AddSubscription(typeof(TEvent), Wrapper, evt => filter?.Invoke((TEvent)evt) ?? true, priority);
|
||||
}
|
||||
|
||||
private IDisposable AddSubscription(Type eventType, Func<object, Task> handler, Func<object, bool>? filter, int priority)
|
||||
{
|
||||
var sub = new EventSubscription(eventType, handler, filter, priority, RemoveSubscription);
|
||||
var list = _subscriptions.GetOrAdd(eventType, _ => new List<EventSubscription>());
|
||||
lock (list)
|
||||
{
|
||||
list.Add(sub);
|
||||
list.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
private void RemoveSubscription(EventSubscription subscription)
|
||||
{
|
||||
if (_subscriptions.TryGetValue(subscription.EventType, out var list))
|
||||
{
|
||||
lock (list)
|
||||
{
|
||||
list.Remove(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
while (await _channel.Reader.WaitToReadAsync(_cts.Token).ConfigureAwait(false))
|
||||
{
|
||||
while (_channel.Reader.TryRead(out var evt))
|
||||
{
|
||||
await DispatchAsync(evt).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常关闭
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DispatchAsync(object evt)
|
||||
{
|
||||
if (_subscriptions.TryGetValue(evt.GetType(), out var list))
|
||||
{
|
||||
List<EventSubscription> snapshot;
|
||||
lock (list)
|
||||
{
|
||||
snapshot = list.ToList();
|
||||
}
|
||||
|
||||
foreach (var sub in snapshot)
|
||||
{
|
||||
if (!sub.CanHandle(evt)) continue;
|
||||
try
|
||||
{
|
||||
await sub.Handler(evt).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_options.OnError?.Invoke(ex, evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_cts.IsCancellationRequested) throw new ObjectDisposedException(nameof(InMemoryEventBus));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cts.Cancel();
|
||||
try
|
||||
{
|
||||
_channel.Writer.TryComplete();
|
||||
_worker.Wait(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,456 @@
|
||||
|
||||
> **🚧 开发中 / Under Construction**
|
||||
|
||||
这是一个预留的事件总线模块,计划用于实现进程内(In-Process)或分布式(Distributed)的消息发布/订阅模式。
|
||||
轻量级进程内事件总线,用于解耦应用程序中的模块通信。
|
||||
|
||||
## 计划功能
|
||||
- 定义统一的 `IEventBus` 接口。
|
||||
- 提供基于内存(Channel/MemoryCache)的默认实现。
|
||||
- 提供集成 RabbitMQ / Redis 的扩展实现。
|
||||
## 📋 为什么需要 EventBus?
|
||||
|
||||
目前该目录仅包含占位符。
|
||||
### 场景对比
|
||||
|
||||
#### ❌ 不使用 EventBus - 紧耦合
|
||||
```csharp
|
||||
public class PlcMonitorService {
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRepository<PlcData> _repository;
|
||||
|
||||
// 需要注入所有订阅者,耦合严重
|
||||
public PlcMonitorService(MainViewModel vm, ILogger logger, IRepository<PlcData> repo) { }
|
||||
|
||||
private void OnDataChanged(PlcTag tag, object value) {
|
||||
_mainViewModel.UpdateValue(tag.Id, value); // UI更新
|
||||
_logger.LogInformation($"Tag {tag.Id} changed"); // 日志
|
||||
_repository.AddAsync(new PlcData { ... }); // 存储
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 使用 EventBus - 解耦
|
||||
```csharp
|
||||
// 发布者:只管发布,不关心谁处理
|
||||
public class PlcMonitorService {
|
||||
private readonly IEventBus _eventBus;
|
||||
|
||||
private void OnDataChanged(PlcTag tag, object value) {
|
||||
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅者1:UI
|
||||
public class MainViewModel {
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(e => UpdateValue(e.TagId, e.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅者2:日志(后续添加,无需修改发布者)
|
||||
public class PlcDataLogger {
|
||||
public PlcDataLogger(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(e =>
|
||||
_logger.LogInformation($"Tag {e.TagId} changed"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 核心优势
|
||||
|
||||
1. **解耦** - 发布者不需要知道订阅者
|
||||
2. **可扩展** - 运行时动态添加/移除订阅者
|
||||
3. **类型安全** - 强类型事件,编译期检查
|
||||
4. **跨层通信** - ViewModels、Services、Repositories 之间自由通信
|
||||
|
||||
---
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
1. **简单易用** - 一行注册,一行订阅
|
||||
2. **类型安全** - 强类型事件,避免字符串魔法值
|
||||
3. **性能优先** - 基于 `System.Threading.Channels`,高性能异步
|
||||
4. **线程安全** - 支持多线程发布/订阅
|
||||
5. **生命周期管理** - 自动清理订阅,防止内存泄漏
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始(未来)
|
||||
|
||||
### 1. 注册服务
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 一行注册
|
||||
services.AddEventBus();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 定义事件
|
||||
```csharp
|
||||
// 事件:简单的 POCO 类
|
||||
public record UserLoginEvent(string Username, DateTime LoginTime);
|
||||
|
||||
public record PlcDataChangedEvent(string TagId, object Value);
|
||||
|
||||
public record OrderCreatedEvent(int OrderId, decimal TotalAmount);
|
||||
```
|
||||
|
||||
### 3. 发布事件
|
||||
```csharp
|
||||
public class LoginService {
|
||||
private readonly IEventBus _eventBus;
|
||||
|
||||
public LoginService(IEventBus eventBus) => _eventBus = eventBus;
|
||||
|
||||
public async Task LoginAsync(string username, string password) {
|
||||
// ... 登录逻辑
|
||||
|
||||
// 发布事件
|
||||
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 订阅事件
|
||||
```csharp
|
||||
// 方式1:在构造函数中订阅
|
||||
public class MainViewModel : ViewModelBase {
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
_subscription = eventBus.Subscribe<UserLoginEvent>(OnUserLogin);
|
||||
}
|
||||
|
||||
private void OnUserLogin(UserLoginEvent e) {
|
||||
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
_subscription?.Dispose(); // 取消订阅
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
// 方式2:异步订阅
|
||||
public class DataSyncService {
|
||||
public DataSyncService(IEventBus eventBus) {
|
||||
eventBus.Subscribe<OrderCreatedEvent>(async e => {
|
||||
await SyncToRemoteAsync(e.OrderId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 方式3:条件过滤
|
||||
public class HighValueOrderMonitor {
|
||||
public HighValueOrderMonitor(IEventBus eventBus) {
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => e.TotalAmount > 10000, e => {
|
||||
// 只处理大额订单
|
||||
NotifyManager(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心 API 设计
|
||||
|
||||
### IEventBus 接口
|
||||
```csharp
|
||||
public interface IEventBus {
|
||||
/// <summary>
|
||||
/// 发布事件(同步)
|
||||
/// </summary>
|
||||
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 发布事件(异步)
|
||||
/// </summary>
|
||||
Task PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(同步处理器)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(异步处理器)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(带过滤条件)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(
|
||||
Func<TEvent, bool> filter,
|
||||
Action<TEvent> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(带优先级,数字越小优先级越高)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(
|
||||
Action<TEvent> handler,
|
||||
int priority = 0) where TEvent : class;
|
||||
}
|
||||
```
|
||||
|
||||
### EventSubscription 类(流式API)
|
||||
```csharp
|
||||
public class EventSubscription<TEvent> : IDisposable where TEvent : class {
|
||||
/// <summary>
|
||||
/// 添加过滤条件
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> Where(Func<TEvent, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// 设置优先级
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> WithPriority(int priority);
|
||||
|
||||
/// <summary>
|
||||
/// 在指定线程上执行(如UI线程)
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> OnThread(TaskScheduler scheduler);
|
||||
|
||||
/// <summary>
|
||||
/// 延迟执行
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> Delay(TimeSpan delay);
|
||||
|
||||
/// <summary>
|
||||
/// 最终处理器
|
||||
/// </summary>
|
||||
public IDisposable Handle(Action<TEvent> handler);
|
||||
|
||||
public IDisposable HandleAsync(Func<TEvent, Task> handler);
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
eventBus.CreateSubscription<PlcDataChangedEvent>()
|
||||
.Where(e => e.TagId.StartsWith("Temperature"))
|
||||
.Delay(TimeSpan.FromSeconds(1)) // 防抖
|
||||
.OnThread(TaskScheduler.FromCurrentSynchronizationContext()) // UI线程
|
||||
.Handle(e => UpdateChart(e.Value));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 事件优先级
|
||||
```csharp
|
||||
// 优先级:数字越小越先执行
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => ValidateOrder(e), priority: 1);
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => SendEmail(e), priority: 10);
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => UpdateStatistics(e), priority: 20);
|
||||
```
|
||||
|
||||
### 2. 弱引用订阅(防止内存泄漏)
|
||||
```csharp
|
||||
// 自动模式:订阅者被GC回收时自动取消订阅
|
||||
eventBus.SubscribeWeak<UserLoginEvent>(viewModel, (vm, e) => vm.OnLogin(e));
|
||||
```
|
||||
|
||||
### 3. 延迟发布
|
||||
```csharp
|
||||
// 延迟1秒发布(防抖场景)
|
||||
eventBus.PublishDelayed(new SearchTextChangedEvent(text), TimeSpan.FromSeconds(1));
|
||||
```
|
||||
|
||||
### 4. 错误处理
|
||||
```csharp
|
||||
// 全局错误处理器
|
||||
services.AddEventBus(options => {
|
||||
options.OnError = (exception, @event) => {
|
||||
logger.LogError(exception, "事件处理失败: {@Event}", @event);
|
||||
};
|
||||
});
|
||||
|
||||
// 单个订阅的错误处理
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => {
|
||||
try {
|
||||
ProcessOrder(e);
|
||||
} catch (Exception ex) {
|
||||
logger.LogError(ex, "订单处理失败");
|
||||
// 不影响其他订阅者
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 事件日志(可选)
|
||||
```csharp
|
||||
services.AddEventBus(options => {
|
||||
options.EnableEventLogging = true; // 记录所有事件发布
|
||||
options.LogEventData = true; // 记录事件数据(调试用)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 典型使用场景
|
||||
|
||||
### 场景1:跨 ViewModel 通信
|
||||
```csharp
|
||||
// LoginViewModel 发布登录成功事件
|
||||
public class LoginViewModel {
|
||||
private async Task LoginAsync() {
|
||||
// ... 登录逻辑
|
||||
_eventBus.Publish(new UserLoginSuccessEvent(user));
|
||||
}
|
||||
}
|
||||
|
||||
// MainViewModel 订阅并更新UI
|
||||
public class MainViewModel {
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<UserLoginSuccessEvent>(e => {
|
||||
CurrentUser = e.User;
|
||||
LoadUserData();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2:PLC数据监控
|
||||
```csharp
|
||||
// PLC监控服务发布数据变化
|
||||
public class PlcMonitorService {
|
||||
private void OnTagChanged(PlcTag tag, object value) {
|
||||
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
// UI订阅更新界面
|
||||
public class DashboardViewModel {
|
||||
public DashboardViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(
|
||||
e => e.TagId == "Temperature1",
|
||||
e => Temperature = (double)e.Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据记录服务订阅存储数据
|
||||
public class DataLoggerService {
|
||||
public DataLoggerService(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(async e => {
|
||||
await _repository.AddAsync(new DataLog {
|
||||
TagId = e.TagId,
|
||||
Value = e.Value,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3:报警系统
|
||||
```csharp
|
||||
// 报警事件
|
||||
public record AlarmTriggeredEvent(string AlarmId, AlarmLevel Level, string Message);
|
||||
|
||||
// 多个订阅者处理报警
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(e => e.Level >= AlarmLevel.High, e => {
|
||||
PlayAlarmSound(); // UI线程
|
||||
});
|
||||
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(async e => {
|
||||
await SendEmailToManager(e); // 异步发送邮件
|
||||
});
|
||||
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(e => {
|
||||
LogAlarm(e); // 记录日志
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
```csharp
|
||||
services.AddEventBus(options => {
|
||||
// 通道容量(默认1000)
|
||||
options.ChannelCapacity = 10000;
|
||||
|
||||
// 错误处理
|
||||
options.OnError = (ex, evt) => logger.LogError(ex, "事件错误");
|
||||
|
||||
// 事件日志
|
||||
options.EnableEventLogging = true;
|
||||
|
||||
// 超时配置(异步处理器超时时间)
|
||||
options.HandlerTimeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 实现方案
|
||||
|
||||
### 基于 System.Threading.Channels
|
||||
- **高性能**:异步无锁队列
|
||||
- **线程安全**:内置并发支持
|
||||
- **背压支持**:通道容量控制
|
||||
|
||||
### 架构设计
|
||||
```
|
||||
IEventBus(接口)
|
||||
↓
|
||||
InMemoryEventBus(默认实现)
|
||||
↓ 内部使用
|
||||
System.Threading.Channels.Channel<EventEnvelope>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能目标
|
||||
|
||||
- **发布延迟**:< 1ms(同步),< 10ms(异步)
|
||||
- **吞吐量**:> 100,000 事件/秒
|
||||
- **内存占用**:最小化,弱引用订阅防止泄漏
|
||||
- **线程安全**:完全线程安全
|
||||
|
||||
---
|
||||
|
||||
## 🔄 与其他模式对比
|
||||
|
||||
| 模式 | 优点 | 缺点 | 适用场景 |
|
||||
|-----|------|------|---------|
|
||||
| **EventBus** | 完全解耦、多订阅者 | 略微增加复杂度 | 跨模块通信、插件架构 |
|
||||
| **直接回调** | 简单直接 | 紧耦合、单订阅者 | 简单父子组件通信 |
|
||||
| **事件聚合器** | 解耦 | 需要手动管理 | 中等复杂度应用 |
|
||||
| **消息队列** | 跨进程、持久化 | 重量级、需要中间件 | 分布式系统 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **不要滥用**:简单的父子组件通信用直接回调即可
|
||||
2. **事件命名**:使用过去式(如 `UserLoggedInEvent` 而非 `UserLoginEvent`)
|
||||
3. **事件不可变**:推荐使用 `record` 定义事件
|
||||
4. **及时取消订阅**:避免内存泄漏
|
||||
5. **异常处理**:订阅者内部处理异常,不要抛出
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 可能的扩展方向
|
||||
1. **分布式 EventBus**(基于 Redis Pub/Sub)
|
||||
2. **事件溯源**(Event Sourcing)支持
|
||||
3. **事件重放**(Replay)功能
|
||||
4. **事件转换**(Transform)中间件
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考资料
|
||||
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - .NET 消息中介库
|
||||
- [Prism EventAggregator](https://prismlibrary.com/docs/event-aggregator.html) - WPF MVVM 事件聚合器
|
||||
- [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) - .NET 高性能通道
|
||||
|
||||
---
|
||||
|
||||
**状态**:设计阶段,未实现
|
||||
**预计实现时间**:根据实际需求决定
|
||||
**当前替代方案**:使用 Common 模块的 Global 容器 + 委托事件
|
||||
@@ -0,0 +1,82 @@
|
||||
# Sharp Project Context
|
||||
|
||||
## Project Overview
|
||||
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF-based client/server applications. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
|
||||
|
||||
## Key Modules
|
||||
|
||||
### Core Infrastructure
|
||||
- **Common**: A utility library providing a lightweight DI container (`Global`), functional `Result<T>` pattern, and extensions for Strings, DateTime, Encryption, and Reflection.
|
||||
- **Config**: Configuration management.
|
||||
- **Log**: Integration with `Microsoft.Extensions.Logging`.
|
||||
- **EventBus**: Event-driven architecture components.
|
||||
|
||||
### Networking & Communications
|
||||
- **NetServer**: A lightweight, attribute-based HTTP server framework built on `HttpListener`. Features include:
|
||||
- Routing (`[Route]`, `[HttpGet]`), Authentication (`[Authorize]`), Rate Limiting, and Middleware support.
|
||||
- **NetClient**: Typed HTTP client abstractions.
|
||||
- **Plc**: Industrial communication abstraction layer.
|
||||
- **Plc.Siemens**: Implementation for Siemens S7 CPUs using optimized batch reading strategies (merging adjacent tags to reduce network overhead).
|
||||
- **Ports**: Serial/COM port abstractions.
|
||||
|
||||
### Applications (UI)
|
||||
- **Client**: Main WPF Client application.
|
||||
- **Server**: WPF Server application (likely a dashboard or local control interface).
|
||||
- **Wpf**: Shared WPF UI logic, behaviors, and converters.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Prerequisites
|
||||
- .NET 8 SDK
|
||||
- Visual Studio 2022 or VS Code (with C# Dev Kit)
|
||||
|
||||
### Building & Running
|
||||
- **Restore Dependencies**:
|
||||
```bash
|
||||
dotnet restore
|
||||
```
|
||||
- **Build Solution**:
|
||||
```bash
|
||||
dotnet build Sharp.sln -c Debug
|
||||
```
|
||||
- **Run WPF Client**:
|
||||
```bash
|
||||
dotnet run --project Client/Client.csproj
|
||||
```
|
||||
- **Run NetServer Example**:
|
||||
```bash
|
||||
dotnet run --project NetServer/Examples
|
||||
# Note: Check specific project path if Examples is a folder or project
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Tests are located in the `../../test/` directory (relative to `src/Sharp`).
|
||||
- **Run All Tests**:
|
||||
```bash
|
||||
dotnet test Sharp.sln
|
||||
```
|
||||
- **Run Specific Module Tests**:
|
||||
```bash
|
||||
dotnet test ../../test/Common.Test/Common.Test.csproj
|
||||
```
|
||||
|
||||
## Code Conventions & Standards
|
||||
|
||||
- **Language**: C# 12 / .NET 8 (`net8.0-windows` for UI).
|
||||
- **Naming**:
|
||||
- `PascalCase` for public members and types.
|
||||
- `camelCase` for private fields and local variables.
|
||||
- Async methods must end with `Async`.
|
||||
- Interfaces prefixed with `I`.
|
||||
- **Architecture**:
|
||||
- Prefer **Dependency Injection** (DI) via constructor injection.
|
||||
- Use the **Result Pattern** (`Result<T>`) from `Common` for flow control instead of exceptions for business logic.
|
||||
- **Plc Communication**: Use `IPlcService` and `PlcTag` for managed, optimized data access.
|
||||
- **Git Commit Messages**:
|
||||
- Concise and imperative (e.g., `feat: add plc reconnection logic` or `修复: 优化批量读取算法`).
|
||||
- Chinese commit messages are acceptable and common in history.
|
||||
|
||||
## Important Implementation Details
|
||||
- **Plc Optimization**: The `SiemensOptimizer` automatically batches read requests based on `MaxGapBytes` (default 20 bytes) and PDU size to maximize throughput.
|
||||
- **NetServer Middleware**: Middleware order matters. Default pipeline: Exception -> Logging -> Cors -> Auth -> RateLimit -> Cache -> Route.
|
||||
- **Common Utilities**: extensively use `Global` for service location in legacy/static contexts, but prefer DI in modern components.
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace NetClient
|
||||
{
|
||||
public class Class1
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using Sharp.NetServer.Abstractions;
|
||||
@@ -45,31 +46,48 @@ public class ResponseCacheMiddleware : IMiddleware
|
||||
}
|
||||
|
||||
// 捕获响应
|
||||
var originalStream = context.Response.OutputStream;
|
||||
using var memoryStream = new MemoryStream();
|
||||
|
||||
// 临时替换输出流
|
||||
var response = context.Response;
|
||||
var originalContentType = response.ContentType;
|
||||
var originalStatusCode = response.StatusCode;
|
||||
using var captured = new MemoryStream();
|
||||
var originalStream = response.SwapOutputStream(captured);
|
||||
|
||||
// 执行下一个中间件
|
||||
await next();
|
||||
|
||||
// 如果响应成功,缓存结果
|
||||
if (response.StatusCode == 200)
|
||||
try
|
||||
{
|
||||
// 注意:由于 HttpListenerResponse 的限制,我们无法直接拦截输出流
|
||||
// 这里提供一个简化的实现,实际使用中可能需要在控制器层面配合
|
||||
var cachedData = new CachedResponse
|
||||
{
|
||||
ContentType = response.ContentType ?? "application/json",
|
||||
StatusCode = response.StatusCode,
|
||||
Headers = new Dictionary<string, string>(),
|
||||
// Body 需要在控制器中通过特殊处理来缓存
|
||||
};
|
||||
await next();
|
||||
|
||||
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
||||
// 只处理捕获后的数据
|
||||
var bodyBytes = captured.ToArray();
|
||||
response.ContentLength = bodyBytes.Length;
|
||||
|
||||
// MISS 标记
|
||||
response.AddHeader("X-Cache", "MISS");
|
||||
|
||||
// 写回真实输出
|
||||
await response.FlushCapturedBodyAsync(captured, context.CancellationToken);
|
||||
|
||||
if (response.StatusCode == 200)
|
||||
{
|
||||
var headers = new Dictionary<string, string>();
|
||||
foreach (var key in response.Headers.AllKeys)
|
||||
{
|
||||
if (key is null) continue;
|
||||
headers[key] = response.Headers[key]!;
|
||||
}
|
||||
|
||||
var cachedData = new CachedResponse
|
||||
{
|
||||
ContentType = response.ContentType ?? "application/json",
|
||||
StatusCode = response.StatusCode,
|
||||
Headers = headers,
|
||||
Body = bodyBytes
|
||||
};
|
||||
|
||||
await _cache.SetAsync(cacheKey, cachedData, TimeSpan.FromSeconds(config.Duration));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 还原输出流,避免影响后续逻辑
|
||||
response.SwapOutputStream(originalStream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,11 +129,9 @@ public class ResponseCacheMiddleware : IMiddleware
|
||||
context.Response.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (cachedResponse.Body != null)
|
||||
{
|
||||
context.Response.ContentLength = cachedResponse.Body.Length;
|
||||
await context.Response.OutputStream.WriteAsync(cachedResponse.Body);
|
||||
}
|
||||
var body = cachedResponse.Body ?? Array.Empty<byte>();
|
||||
context.Response.ContentLength = body.Length;
|
||||
await context.Response.OutputStream.WriteAsync(body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
|
||||
namespace Sharp.NetServer.Models;
|
||||
|
||||
@@ -101,10 +102,12 @@ public class HttpRequestWrapper
|
||||
public class HttpResponseWrapper
|
||||
{
|
||||
private readonly HttpListenerResponse _response;
|
||||
private Stream _outputStream;
|
||||
|
||||
public HttpResponseWrapper(HttpListenerResponse response)
|
||||
{
|
||||
_response = response;
|
||||
_outputStream = response.OutputStream;
|
||||
}
|
||||
|
||||
public int StatusCode
|
||||
@@ -125,7 +128,7 @@ public class HttpResponseWrapper
|
||||
set => _response.ContentLength64 = value;
|
||||
}
|
||||
|
||||
public Stream OutputStream => _response.OutputStream;
|
||||
public Stream OutputStream => _outputStream;
|
||||
public WebHeaderCollection Headers => _response.Headers;
|
||||
public CookieCollection Cookies => _response.Cookies;
|
||||
|
||||
@@ -134,6 +137,28 @@ public class HttpResponseWrapper
|
||||
_response.AddHeader(name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 替换输出流(用于中间件包装),返回之前的流。
|
||||
/// </summary>
|
||||
public Stream SwapOutputStream(Stream stream)
|
||||
{
|
||||
var previous = _outputStream;
|
||||
_outputStream = stream;
|
||||
return previous;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将捕获的内容写回到底层 HttpListener 输出流。
|
||||
/// </summary>
|
||||
public async Task FlushCapturedBodyAsync(Stream captured, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (captured.CanSeek)
|
||||
{
|
||||
captured.Position = 0;
|
||||
}
|
||||
await captured.CopyToAsync(_response.OutputStream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Redirect(string url)
|
||||
{
|
||||
_response.Redirect(url);
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,9 +11,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -12,9 +12,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
|
||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,57 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Sharp.Process.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行服务注册扩展
|
||||
/// </summary>
|
||||
public static class ProcessServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Process 服务
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpProcess(this IServiceCollection services)
|
||||
{
|
||||
// 注册进程执行器
|
||||
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Process 服务(带配置)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configure">配置选项</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpProcess(
|
||||
this IServiceCollection services,
|
||||
Action<ProcessGlobalOptions> configure)
|
||||
{
|
||||
// 配置全局选项
|
||||
services.Configure(configure);
|
||||
|
||||
// 注册进程执行器
|
||||
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行全局选项
|
||||
/// </summary>
|
||||
public class ProcessGlobalOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认超时时长
|
||||
/// </summary>
|
||||
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>
|
||||
/// 是否默认启用自动日志记录
|
||||
/// </summary>
|
||||
public bool AutoLogOutputByDefault { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Common.Results;
|
||||
|
||||
namespace Sharp.Process;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行器接口
|
||||
/// </summary>
|
||||
public interface IProcessExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行进程(异步)
|
||||
/// </summary>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
Task<Result<ProcessResult>> ExecuteAsync(
|
||||
ProcessOptions options,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程(同步)
|
||||
/// </summary>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <returns>执行结果</returns>
|
||||
Result<ProcessResult> Execute(ProcessOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// 快速执行命令(静态便捷方法)
|
||||
/// </summary>
|
||||
/// <param name="command">命令</param>
|
||||
/// <param name="arguments">参数</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
static Task<Result<ProcessResult>> RunAsync(
|
||||
string command,
|
||||
string arguments = "",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var executor = new ProcessExecutor();
|
||||
var options = new ProcessOptions
|
||||
{
|
||||
Command = command,
|
||||
Arguments = string.IsNullOrEmpty(arguments)
|
||||
? new List<string>()
|
||||
: arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
|
||||
};
|
||||
return executor.ExecuteAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速执行 Shell 命令
|
||||
/// </summary>
|
||||
/// <param name="shellCommand">Shell 命令(支持管道、重定向等)</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
static Task<Result<ProcessResult>> RunShellAsync(
|
||||
string shellCommand,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var executor = new ProcessExecutor();
|
||||
var isWindows = OperatingSystem.IsWindows();
|
||||
var options = new ProcessOptions
|
||||
{
|
||||
Command = isWindows ? "cmd.exe" : "/bin/bash",
|
||||
Arguments = isWindows
|
||||
? new List<string> { "/c", shellCommand }
|
||||
: new List<string> { "-c", shellCommand },
|
||||
UseShellExecute = false
|
||||
};
|
||||
return executor.ExecuteAsync(options, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Sharp.Process</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,263 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Common.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Sharp.Process;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行器实现
|
||||
/// </summary>
|
||||
public class ProcessExecutor : IProcessExecutor
|
||||
{
|
||||
private readonly ILogger<ProcessExecutor>? _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(支持DI)
|
||||
/// </summary>
|
||||
public ProcessExecutor(ILogger<ProcessExecutor>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程(异步)
|
||||
/// </summary>
|
||||
public async Task<Result<ProcessResult>> ExecuteAsync(
|
||||
ProcessOptions options,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await Result.TryAsync(async () =>
|
||||
{
|
||||
ValidateOptions(options);
|
||||
|
||||
using var process = CreateProcess(options);
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
var outputLock = new object();
|
||||
var errorLock = new object();
|
||||
|
||||
// 配置输出接收
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data == null) return;
|
||||
|
||||
lock (outputLock)
|
||||
{
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
}
|
||||
|
||||
// 触发实时输出事件
|
||||
options.OnOutputReceived?.Invoke(e.Data);
|
||||
|
||||
// 自动日志记录
|
||||
if (options.AutoLogOutput && _logger != null)
|
||||
{
|
||||
_logger.LogInformation("[{Command}] {Output}", options.Command, e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
process.ErrorDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data == null) return;
|
||||
|
||||
lock (errorLock)
|
||||
{
|
||||
errorBuilder.AppendLine(e.Data);
|
||||
}
|
||||
|
||||
// 触发实时错误事件
|
||||
options.OnErrorReceived?.Invoke(e.Data);
|
||||
|
||||
// 自动日志记录
|
||||
if (options.AutoLogOutput && _logger != null)
|
||||
{
|
||||
_logger.LogWarning("[{Command}] {Error}", options.Command, e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
// 启动进程
|
||||
var startTime = DateTime.Now;
|
||||
var started = process.Start();
|
||||
|
||||
if (!started)
|
||||
throw new InvalidOperationException($"无法启动进程: {options.Command}");
|
||||
|
||||
var processId = process.Id;
|
||||
|
||||
// 设置进程优先级
|
||||
if (options.Priority.HasValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.PriorityClass = options.Priority.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "无法设置进程优先级");
|
||||
}
|
||||
}
|
||||
|
||||
// 写入标准输入
|
||||
if (!string.IsNullOrEmpty(options.StandardInput))
|
||||
{
|
||||
await process.StandardInput.WriteAsync(options.StandardInput);
|
||||
await process.StandardInput.FlushAsync();
|
||||
process.StandardInput.Close();
|
||||
}
|
||||
|
||||
// 开始异步读取输出
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
// 等待进程退出(带超时和取消支持)
|
||||
var isTimeout = false;
|
||||
var wasKilled = false;
|
||||
|
||||
using var timeoutCts = new CancellationTokenSource(options.Timeout);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
||||
cancellationToken, timeoutCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 检查是超时还是用户取消
|
||||
if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
isTimeout = true;
|
||||
_logger?.LogWarning("进程执行超时: {Command} (超时时长: {Timeout})",
|
||||
options.Command, options.Timeout);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogInformation("进程执行被取消: {Command}", options.Command);
|
||||
}
|
||||
|
||||
// 强制终止进程
|
||||
wasKilled = true;
|
||||
try
|
||||
{
|
||||
if (!process.HasExited)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
await process.WaitForExitAsync(CancellationToken.None);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "终止进程失败");
|
||||
}
|
||||
|
||||
// 如果是用户取消,重新抛出异常
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
throw;
|
||||
}
|
||||
|
||||
var exitTime = DateTime.Now;
|
||||
var duration = exitTime - startTime;
|
||||
|
||||
// 确保所有输出都被读取
|
||||
await Task.Delay(100); // 给一点时间让输出事件完成
|
||||
|
||||
var result = new ProcessResult
|
||||
{
|
||||
ExitCode = process.ExitCode,
|
||||
StandardOutput = outputBuilder.ToString().TrimEnd(),
|
||||
StandardError = errorBuilder.ToString().TrimEnd(),
|
||||
Duration = duration,
|
||||
ProcessId = processId,
|
||||
StartTime = startTime,
|
||||
ExitTime = exitTime,
|
||||
IsTimeout = isTimeout,
|
||||
WasKilled = wasKilled,
|
||||
CommandLine = options.BuildCommandLine()
|
||||
};
|
||||
|
||||
// 记录执行结果
|
||||
if (_logger != null)
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_logger.LogInformation("进程执行成功: {Command} (耗时: {Duration}ms, 退出码: {ExitCode})",
|
||||
options.Command, duration.TotalMilliseconds, result.ExitCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("进程执行失败: {Command} (耗时: {Duration}ms, 退出码: {ExitCode}, 超时: {IsTimeout}, 被终止: {WasKilled})",
|
||||
options.Command, duration.TotalMilliseconds, result.ExitCode, isTimeout, wasKilled);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程(同步)
|
||||
/// </summary>
|
||||
public Result<ProcessResult> Execute(ProcessOptions options)
|
||||
{
|
||||
return ExecuteAsync(options).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建进程对象
|
||||
/// </summary>
|
||||
private System.Diagnostics.Process CreateProcess(ProcessOptions options)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = options.Command,
|
||||
UseShellExecute = options.UseShellExecute,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardInput = !string.IsNullOrEmpty(options.StandardInput),
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = options.OutputEncoding,
|
||||
StandardErrorEncoding = options.ErrorEncoding
|
||||
};
|
||||
|
||||
// 添加参数
|
||||
foreach (var arg in options.Arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(arg);
|
||||
}
|
||||
|
||||
// 设置工作目录
|
||||
if (!string.IsNullOrEmpty(options.WorkingDirectory))
|
||||
{
|
||||
if (!Directory.Exists(options.WorkingDirectory))
|
||||
throw new DirectoryNotFoundException($"工作目录不存在: {options.WorkingDirectory}");
|
||||
|
||||
startInfo.WorkingDirectory = options.WorkingDirectory;
|
||||
}
|
||||
|
||||
// 添加环境变量
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
startInfo.Environment[key] = value;
|
||||
}
|
||||
|
||||
return new System.Diagnostics.Process
|
||||
{
|
||||
StartInfo = startInfo,
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证选项
|
||||
/// </summary>
|
||||
private void ValidateOptions(ProcessOptions options)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(options.Command))
|
||||
throw new ArgumentException("命令不能为空", nameof(options));
|
||||
|
||||
if (options.Timeout <= TimeSpan.Zero)
|
||||
throw new ArgumentException("超时时长必须大于0", nameof(options));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Common.Results;
|
||||
|
||||
namespace Sharp.Process;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行器的 Fluent API 封装
|
||||
/// </summary>
|
||||
public class ProcessExecutorBuilder
|
||||
{
|
||||
private readonly IProcessExecutor _executor;
|
||||
private readonly ProcessOptions _options = new();
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder(IProcessExecutor? executor = null)
|
||||
{
|
||||
_executor = executor ?? new ProcessExecutor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置命令和参数
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithCommand(string command, params string[] arguments)
|
||||
{
|
||||
_options.Command = command;
|
||||
_options.Arguments = arguments.ToList();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加单个参数
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithArgument(string argument)
|
||||
{
|
||||
_options.Arguments.Add(argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置工作目录
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithWorkingDirectory(string workingDirectory)
|
||||
{
|
||||
_options.WorkingDirectory = workingDirectory;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加环境变量
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithEnvironmentVariable(string key, string value)
|
||||
{
|
||||
_options.EnvironmentVariables[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加环境变量
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithEnvironmentVariables(Dictionary<string, string> variables)
|
||||
{
|
||||
foreach (var (key, value) in variables)
|
||||
{
|
||||
_options.EnvironmentVariables[key] = value;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置超时时长
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithTimeout(TimeSpan timeout)
|
||||
{
|
||||
_options.Timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置标准输入
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithStandardInput(string input)
|
||||
{
|
||||
_options.StandardInput = input;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置输出编码
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithOutputEncoding(Encoding encoding)
|
||||
{
|
||||
_options.OutputEncoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置错误输出编码
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithErrorEncoding(Encoding encoding)
|
||||
{
|
||||
_options.ErrorEncoding = encoding;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Shell 执行
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithShellExecute(bool useShell = true)
|
||||
{
|
||||
_options.UseShellExecute = useShell;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置进程优先级
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithPriority(ProcessPriorityClass priority)
|
||||
{
|
||||
_options.Priority = priority;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置标准输出接收事件
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder OnOutputReceived(Action<string> handler)
|
||||
{
|
||||
_options.OnOutputReceived = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置标准错误接收事件
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder OnErrorReceived(Action<string> handler)
|
||||
{
|
||||
_options.OnErrorReceived = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启用/禁用自动日志记录
|
||||
/// </summary>
|
||||
public ProcessExecutorBuilder WithLogging(bool autoLog = true)
|
||||
{
|
||||
_options.AutoLogOutput = autoLog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程(异步)
|
||||
/// </summary>
|
||||
public Task<Result<ProcessResult>> ExecuteAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _executor.ExecuteAsync(_options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行进程(同步)
|
||||
/// </summary>
|
||||
public Result<ProcessResult> Execute()
|
||||
{
|
||||
return _executor.Execute(_options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行器扩展方法
|
||||
/// </summary>
|
||||
public static class ProcessExecutorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 Fluent API 构建器
|
||||
/// </summary>
|
||||
public static ProcessExecutorBuilder CreateBuilder(this IProcessExecutor executor)
|
||||
{
|
||||
return new ProcessExecutorBuilder(executor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速执行命令(扩展方法)
|
||||
/// </summary>
|
||||
public static Task<Result<ProcessResult>> RunAsync(
|
||||
this IProcessExecutor executor,
|
||||
string command,
|
||||
params string[] arguments)
|
||||
{
|
||||
return executor.CreateBuilder()
|
||||
.WithCommand(command, arguments)
|
||||
.ExecuteAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Sharp.Process;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行选项
|
||||
/// </summary>
|
||||
public class ProcessOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 命令或可执行文件路径
|
||||
/// </summary>
|
||||
public string Command { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 命令参数列表
|
||||
/// </summary>
|
||||
public List<string> Arguments { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 工作目录
|
||||
/// </summary>
|
||||
public string? WorkingDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 环境变量
|
||||
/// </summary>
|
||||
public Dictionary<string, string> EnvironmentVariables { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 超时时长(默认2分钟)
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>
|
||||
/// 标准输入内容
|
||||
/// </summary>
|
||||
public string? StandardInput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 输出编码(默认 UTF-8)
|
||||
/// </summary>
|
||||
public Encoding OutputEncoding { get; set; } = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// 错误输出编码(默认 UTF-8)
|
||||
/// </summary>
|
||||
public Encoding ErrorEncoding { get; set; } = Encoding.UTF8;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用 Shell 执行(默认 false,更安全)
|
||||
/// </summary>
|
||||
public bool UseShellExecute { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 进程优先级(可选)
|
||||
/// </summary>
|
||||
public ProcessPriorityClass? Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准输出接收事件
|
||||
/// </summary>
|
||||
public Action<string>? OnOutputReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准错误接收事件
|
||||
/// </summary>
|
||||
public Action<string>? OnErrorReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动记录日志到 ILogger
|
||||
/// </summary>
|
||||
public bool AutoLogOutput { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构建完整的命令行字符串(用于显示)
|
||||
/// </summary>
|
||||
internal string BuildCommandLine()
|
||||
{
|
||||
if (Arguments.Count == 0)
|
||||
return Command;
|
||||
|
||||
var args = string.Join(" ", Arguments.Select(EscapeArgument));
|
||||
return $"{Command} {args}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转义命令行参数(处理空格)
|
||||
/// </summary>
|
||||
private static string EscapeArgument(string arg)
|
||||
{
|
||||
if (string.IsNullOrEmpty(arg))
|
||||
return "\"\"";
|
||||
|
||||
if (arg.Contains(' ') || arg.Contains('"'))
|
||||
return $"\"{arg.Replace("\"", "\\\"")}\"";
|
||||
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace Sharp.Process;
|
||||
|
||||
/// <summary>
|
||||
/// 进程执行结果
|
||||
/// </summary>
|
||||
public class ProcessResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 退出码
|
||||
/// </summary>
|
||||
public int ExitCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准输出
|
||||
/// </summary>
|
||||
public string StandardOutput { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 标准错误
|
||||
/// </summary>
|
||||
public string StandardError { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 执行时长
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 进程ID
|
||||
/// </summary>
|
||||
public int ProcessId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 启动时间
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 退出时间
|
||||
/// </summary>
|
||||
public DateTime ExitTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否超时
|
||||
/// </summary>
|
||||
public bool IsTimeout { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否被强制终止
|
||||
/// </summary>
|
||||
public bool WasKilled { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否成功执行(退出码为0)
|
||||
/// </summary>
|
||||
public bool IsSuccess => ExitCode == 0 && !IsTimeout && !WasKilled;
|
||||
|
||||
/// <summary>
|
||||
/// 命令行(用于调试)
|
||||
/// </summary>
|
||||
public string CommandLine { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
# Sharp.Process
|
||||
|
||||
通用进程执行抽象层,封装 `System.Diagnostics.Process` 的复杂性,提供简洁、强大的 API。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- ✅ **同步/异步执行** - 支持 `Execute()` 和 `ExecuteAsync()`
|
||||
- ✅ **实时输出流** - `OnOutputReceived` 和 `OnErrorReceived` 事件
|
||||
- ✅ **超时控制** - 可配置超时时长,自动终止超时进程
|
||||
- ✅ **取消支持** - 完整的 `CancellationToken` 集成
|
||||
- ✅ **日志集成** - 自动对接 Serilog/ILogger
|
||||
- ✅ **Result<T> 模式** - 统一的错误处理
|
||||
- ✅ **Fluent API** - 链式调用,直观易用
|
||||
- ✅ **Shell 模式** - 支持管道、重定向等 Shell 特性
|
||||
- ✅ **编码处理** - 可配置输出编码,解决中文乱码
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. 安装
|
||||
|
||||
```bash
|
||||
dotnet add reference ../Process/Process.csproj
|
||||
```
|
||||
|
||||
### 2. 注册服务
|
||||
|
||||
```csharp
|
||||
// Program.cs 或 App.xaml.cs
|
||||
services.AddSharpProcess();
|
||||
```
|
||||
|
||||
### 3. 使用
|
||||
|
||||
#### 最简单的调用
|
||||
|
||||
```csharp
|
||||
// 静态方法 - 零配置
|
||||
var result = await IProcessExecutor.RunAsync("python", "script.py");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
Console.WriteLine(result.Value.StandardOutput);
|
||||
}
|
||||
```
|
||||
|
||||
#### 实时输出流
|
||||
|
||||
```csharp
|
||||
var executor = new ProcessExecutor();
|
||||
var options = new ProcessOptions
|
||||
{
|
||||
Command = "dotnet",
|
||||
Arguments = new List<string> { "build" },
|
||||
OnOutputReceived = line => Console.WriteLine($"[输出] {line}"),
|
||||
OnErrorReceived = line => Console.WriteLine($"[错误] {line}"),
|
||||
AutoLogOutput = true // 自动记录到 ILogger
|
||||
};
|
||||
|
||||
var result = await executor.ExecuteAsync(options);
|
||||
```
|
||||
|
||||
#### Fluent API
|
||||
|
||||
```csharp
|
||||
var result = await new ProcessExecutor()
|
||||
.CreateBuilder()
|
||||
.WithCommand("python", "train.py")
|
||||
.WithWorkingDirectory(@"C:\AI\models")
|
||||
.WithTimeout(TimeSpan.FromMinutes(30))
|
||||
.WithEnvironmentVariable("CUDA_VISIBLE_DEVICES", "0,1")
|
||||
.OnOutputReceived(line => _logger.LogInfo(line))
|
||||
.ExecuteAsync();
|
||||
```
|
||||
|
||||
#### Shell 命令
|
||||
|
||||
```csharp
|
||||
// Windows
|
||||
var result = await IProcessExecutor.RunShellAsync("dir | findstr .txt");
|
||||
|
||||
// Linux/macOS
|
||||
var result = await IProcessExecutor.RunShellAsync("ls | grep .log");
|
||||
```
|
||||
|
||||
## 典型场景
|
||||
|
||||
### 场景1: 调用外部工具(ffmpeg)
|
||||
|
||||
```csharp
|
||||
var result = await new ProcessExecutor()
|
||||
.CreateBuilder()
|
||||
.WithCommand("ffmpeg")
|
||||
.WithArgument("-i").WithArgument("input.mp4")
|
||||
.WithArgument("output.avi")
|
||||
.WithTimeout(TimeSpan.FromHours(1))
|
||||
.ExecuteAsync();
|
||||
|
||||
if (result.IsSuccess && result.Value.ExitCode == 0)
|
||||
{
|
||||
Console.WriteLine("视频转换成功");
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 长时间任务,实时显示进度
|
||||
|
||||
```csharp
|
||||
var progressBar = new ProgressBar();
|
||||
|
||||
var result = await new ProcessExecutor()
|
||||
.CreateBuilder()
|
||||
.WithCommand("npm", "install")
|
||||
.WithWorkingDirectory(@"C:\MyProject")
|
||||
.OnOutputReceived(line => {
|
||||
progressBar.Update(line);
|
||||
_logger.LogInformation(line);
|
||||
})
|
||||
.ExecuteAsync();
|
||||
```
|
||||
|
||||
### 场景3: 超时控制和取消
|
||||
|
||||
```csharp
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(30));
|
||||
|
||||
try
|
||||
{
|
||||
var result = await executor.ExecuteAsync(options, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("任务已取消或超时");
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4: WPF 中实时显示日志
|
||||
|
||||
```csharp
|
||||
// ViewModel
|
||||
public async Task RunBuildAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
var result = await _processExecutor.RunAsync("dotnet", "build")
|
||||
.CreateBuilder()
|
||||
.OnOutputReceived(line => {
|
||||
_dispatcher.Invoke(() => {
|
||||
LogOutput += $"{DateTime.Now:HH:mm:ss} {line}\n";
|
||||
});
|
||||
})
|
||||
.ExecuteAsync();
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
```
|
||||
|
||||
## ProcessResult 详解
|
||||
|
||||
```csharp
|
||||
public class ProcessResult
|
||||
{
|
||||
int ExitCode; // 退出码(0 = 成功)
|
||||
string StandardOutput; // 标准输出
|
||||
string StandardError; // 标准错误
|
||||
TimeSpan Duration; // 执行时长
|
||||
int ProcessId; // 进程ID
|
||||
DateTime StartTime; // 启动时间
|
||||
DateTime ExitTime; // 退出时间
|
||||
bool IsTimeout; // 是否超时
|
||||
bool WasKilled; // 是否被强制终止
|
||||
bool IsSuccess; // 是否成功(ExitCode == 0 && !IsTimeout && !WasKilled)
|
||||
string CommandLine; // 完整命令行(用于调试)
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"Process": {
|
||||
"DefaultTimeout": 120,
|
||||
"AutoLogOutputByDefault": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码配置
|
||||
|
||||
```csharp
|
||||
services.AddSharpProcess(options => {
|
||||
options.DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||
options.AutoLogOutputByDefault = true;
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **Shell 模式安全性** - 默认 `UseShellExecute = false`,更安全。只在需要 Shell 特性时启用。
|
||||
2. **中文乱码** - 使用 `WithOutputEncoding(Encoding.GetEncoding("GBK"))` 处理 Windows 中文。
|
||||
3. **长时间任务** - 调整 `Timeout`,避免被意外终止。
|
||||
4. **进程清理** - 超时或取消时会自动终止进程树(`Kill(entireProcessTree: true)`)。
|
||||
|
||||
## 依赖
|
||||
|
||||
- Common (Result<T>)
|
||||
- Microsoft.Extensions.Logging.Abstractions
|
||||
- Microsoft.Extensions.DependencyInjection.Abstractions
|
||||
|
||||
## License
|
||||
|
||||
GNU AGPLv3
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Sharp.Process.DependencyInjection;
|
||||
|
||||
namespace Sharp.Python.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// Python 服务注册扩展
|
||||
/// </summary>
|
||||
public static class PythonServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Python 服务
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpPython(this IServiceCollection services)
|
||||
{
|
||||
// 确保 Process 服务已注册
|
||||
services.AddSharpProcess();
|
||||
|
||||
// 注册 Python 执行器
|
||||
services.AddSingleton<IPythonExecutor, PythonExecutor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Python 服务(带配置)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configure">配置选项</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpPython(
|
||||
this IServiceCollection services,
|
||||
Action<PythonGlobalOptions> configure)
|
||||
{
|
||||
// 配置全局选项
|
||||
services.Configure(configure);
|
||||
|
||||
// 确保 Process 服务已注册
|
||||
services.AddSharpProcess();
|
||||
|
||||
// 注册 Python 执行器
|
||||
services.AddSingleton<IPythonExecutor, PythonExecutor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Python 全局选项
|
||||
/// </summary>
|
||||
public class PythonGlobalOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认 Python 解释器路径
|
||||
/// </summary>
|
||||
public string? DefaultPythonPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认虚拟环境路径
|
||||
/// </summary>
|
||||
public string? DefaultVirtualEnvPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认超时时长
|
||||
/// </summary>
|
||||
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 是否默认启用自动日志记录
|
||||
/// </summary>
|
||||
public bool AutoLogOutputByDefault { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Common.Results;
|
||||
using Sharp.Process;
|
||||
|
||||
namespace Sharp.Python;
|
||||
|
||||
/// <summary>
|
||||
/// Python 执行器接口
|
||||
/// </summary>
|
||||
public interface IPythonExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行 Python 脚本文件
|
||||
/// </summary>
|
||||
/// <param name="scriptPath">脚本文件路径</param>
|
||||
/// <param name="args">参数对象(将序列化为 JSON 传递)</param>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
Task<Result<ProcessResult>> RunScriptAsync(
|
||||
string scriptPath,
|
||||
object? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 脚本文件并解析 JSON 结果
|
||||
/// </summary>
|
||||
/// <typeparam name="T">结果类型</typeparam>
|
||||
/// <param name="scriptPath">脚本文件路径</param>
|
||||
/// <param name="args">参数对象(将序列化为 JSON 传递)</param>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>解析后的结果</returns>
|
||||
Task<Result<T>> RunScriptAsync<T>(
|
||||
string scriptPath,
|
||||
object? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 代码片段
|
||||
/// </summary>
|
||||
/// <param name="code">Python 代码</param>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
Task<Result<ProcessResult>> RunCodeAsync(
|
||||
string code,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 模块
|
||||
/// </summary>
|
||||
/// <param name="moduleName">模块名</param>
|
||||
/// <param name="args">参数</param>
|
||||
/// <param name="options">执行选项</param>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>执行结果</returns>
|
||||
Task<Result<ProcessResult>> RunModuleAsync(
|
||||
string moduleName,
|
||||
string[]? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Sharp.Python</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Process\Process.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Sharp.Python;
|
||||
|
||||
/// <summary>
|
||||
/// Python 环境信息
|
||||
/// </summary>
|
||||
public class PythonEnvironment
|
||||
{
|
||||
/// <summary>
|
||||
/// Python 解释器路径
|
||||
/// </summary>
|
||||
public string PythonPath { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Python 版本
|
||||
/// </summary>
|
||||
public string? Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 虚拟环境路径(如果有)
|
||||
/// </summary>
|
||||
public string? VirtualEnvPath { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否是虚拟环境
|
||||
/// </summary>
|
||||
public bool IsVirtualEnv => !string.IsNullOrEmpty(VirtualEnvPath);
|
||||
|
||||
/// <summary>
|
||||
/// 自动发现系统中的 Python 解释器
|
||||
/// </summary>
|
||||
/// <returns>Python 环境信息,如果未找到则返回 null</returns>
|
||||
public static PythonEnvironment? Discover()
|
||||
{
|
||||
// 尝试常见的 Python 命令
|
||||
var pythonCommands = new[] { "python", "python3", "python.exe", "python3.exe" };
|
||||
|
||||
foreach (var command in pythonCommands)
|
||||
{
|
||||
var pythonPath = FindPythonInPath(command);
|
||||
if (pythonPath != null)
|
||||
{
|
||||
return new PythonEnvironment
|
||||
{
|
||||
PythonPath = pythonPath,
|
||||
Version = GetPythonVersion(pythonPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Windows: 尝试从注册表查找
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var registryPaths = FindPythonInRegistry();
|
||||
if (registryPaths.Any())
|
||||
{
|
||||
var pythonPath = registryPaths.First();
|
||||
return new PythonEnvironment
|
||||
{
|
||||
PythonPath = pythonPath,
|
||||
Version = GetPythonVersion(pythonPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在 PATH 环境变量中查找 Python
|
||||
/// </summary>
|
||||
private static string? FindPythonInPath(string command)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||||
if (string.IsNullOrEmpty(pathEnv))
|
||||
return null;
|
||||
|
||||
var paths = pathEnv.Split(Path.PathSeparator);
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullPath = Path.Combine(path, command);
|
||||
if (File.Exists(fullPath))
|
||||
return fullPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略无效路径
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Windows 注册表查找 Python(Windows only)
|
||||
/// </summary>
|
||||
private static List<string> FindPythonInRegistry()
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return results;
|
||||
|
||||
try
|
||||
{
|
||||
// 尝试查找常见的安装路径
|
||||
var commonPaths = new[]
|
||||
{
|
||||
@"C:\Python39\python.exe",
|
||||
@"C:\Python310\python.exe",
|
||||
@"C:\Python311\python.exe",
|
||||
@"C:\Python312\python.exe",
|
||||
@"C:\Program Files\Python39\python.exe",
|
||||
@"C:\Program Files\Python310\python.exe",
|
||||
@"C:\Program Files\Python311\python.exe",
|
||||
@"C:\Program Files\Python312\python.exe"
|
||||
};
|
||||
|
||||
results.AddRange(commonPaths.Where(File.Exists));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Python 版本
|
||||
/// </summary>
|
||||
private static string? GetPythonVersion(string pythonPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = pythonPath,
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
|
||||
if (process == null)
|
||||
return null;
|
||||
|
||||
process.WaitForExit(3000);
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
|
||||
var versionText = string.IsNullOrEmpty(output) ? error : output;
|
||||
return versionText.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从虚拟环境路径创建 Python 环境
|
||||
/// </summary>
|
||||
/// <param name="venvPath">虚拟环境路径</param>
|
||||
/// <returns>Python 环境信息</returns>
|
||||
public static PythonEnvironment FromVirtualEnv(string venvPath)
|
||||
{
|
||||
if (!Directory.Exists(venvPath))
|
||||
throw new DirectoryNotFoundException($"虚拟环境目录不存在: {venvPath}");
|
||||
|
||||
// 查找虚拟环境中的 Python 解释器
|
||||
var pythonPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? Path.Combine(venvPath, "Scripts", "python.exe")
|
||||
: Path.Combine(venvPath, "bin", "python");
|
||||
|
||||
if (!File.Exists(pythonPath))
|
||||
throw new FileNotFoundException($"虚拟环境中未找到 Python 解释器: {pythonPath}");
|
||||
|
||||
return new PythonEnvironment
|
||||
{
|
||||
PythonPath = pythonPath,
|
||||
VirtualEnvPath = venvPath,
|
||||
Version = GetPythonVersion(pythonPath)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
using System.Text.Json;
|
||||
using Common.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sharp.Process;
|
||||
|
||||
namespace Sharp.Python;
|
||||
|
||||
/// <summary>
|
||||
/// Python 执行器实现
|
||||
/// </summary>
|
||||
public class PythonExecutor : IPythonExecutor
|
||||
{
|
||||
private readonly IProcessExecutor _processExecutor;
|
||||
private readonly ILogger<PythonExecutor>? _logger;
|
||||
private PythonEnvironment? _cachedEnvironment;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public PythonExecutor(
|
||||
IProcessExecutor processExecutor,
|
||||
ILogger<PythonExecutor>? logger = null)
|
||||
{
|
||||
_processExecutor = processExecutor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 脚本文件
|
||||
/// </summary>
|
||||
public async Task<Result<ProcessResult>> RunScriptAsync(
|
||||
string scriptPath,
|
||||
object? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(scriptPath))
|
||||
return Result.Failure<ProcessResult>($"脚本文件不存在: {scriptPath}");
|
||||
|
||||
options ??= new PythonOptions();
|
||||
|
||||
var pythonEnv = GetPythonEnvironment(options);
|
||||
if (pythonEnv == null)
|
||||
return Result.Failure<ProcessResult>("未找到 Python 解释器,请确保已安装 Python 或配置 PythonPath");
|
||||
|
||||
var processOptions = new ProcessOptions
|
||||
{
|
||||
Command = pythonEnv.PythonPath,
|
||||
Arguments = BuildScriptArguments(scriptPath, args),
|
||||
WorkingDirectory = options.WorkingDirectory,
|
||||
Timeout = options.Timeout,
|
||||
AutoLogOutput = options.AutoLogOutput,
|
||||
OnOutputReceived = options.OnOutputReceived,
|
||||
OnErrorReceived = options.OnErrorReceived
|
||||
};
|
||||
|
||||
// 添加环境变量
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
processOptions.EnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
// 如果使用虚拟环境,添加 VIRTUAL_ENV 环境变量
|
||||
if (pythonEnv.IsVirtualEnv)
|
||||
{
|
||||
processOptions.EnvironmentVariables["VIRTUAL_ENV"] = pythonEnv.VirtualEnvPath!;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("执行 Python 脚本: {Script} (Python: {PythonPath})",
|
||||
scriptPath, pythonEnv.PythonPath);
|
||||
|
||||
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 脚本文件并解析 JSON 结果
|
||||
/// </summary>
|
||||
public async Task<Result<T>> RunScriptAsync<T>(
|
||||
string scriptPath,
|
||||
object? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await RunScriptAsync(scriptPath, args, options, cancellationToken);
|
||||
|
||||
return result.Bind(processResult =>
|
||||
{
|
||||
if (!processResult.IsSuccess)
|
||||
{
|
||||
return Result.Failure<T>(
|
||||
$"Python 脚本执行失败 (退出码: {processResult.ExitCode}): {processResult.StandardError}");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(processResult.StandardOutput))
|
||||
{
|
||||
return Result.Failure<T>("Python 脚本没有输出");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<T>(processResult.StandardOutput);
|
||||
if (parsed == null)
|
||||
return Result.Failure<T>("无法解析 Python 输出为 JSON");
|
||||
|
||||
return Result.Success(parsed);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
return Result.Failure<T>($"JSON 解析失败: {ex.Message}\n输出内容: {processResult.StandardOutput}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 代码片段
|
||||
/// </summary>
|
||||
public async Task<Result<ProcessResult>> RunCodeAsync(
|
||||
string code,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new PythonOptions();
|
||||
|
||||
var pythonEnv = GetPythonEnvironment(options);
|
||||
if (pythonEnv == null)
|
||||
return Result.Failure<ProcessResult>("未找到 Python 解释器");
|
||||
|
||||
var processOptions = new ProcessOptions
|
||||
{
|
||||
Command = pythonEnv.PythonPath,
|
||||
Arguments = new List<string> { "-c", code },
|
||||
WorkingDirectory = options.WorkingDirectory,
|
||||
Timeout = options.Timeout,
|
||||
AutoLogOutput = options.AutoLogOutput,
|
||||
OnOutputReceived = options.OnOutputReceived,
|
||||
OnErrorReceived = options.OnErrorReceived
|
||||
};
|
||||
|
||||
// 添加环境变量
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
processOptions.EnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("执行 Python 代码片段 (Python: {PythonPath})", pythonEnv.PythonPath);
|
||||
|
||||
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 Python 模块
|
||||
/// </summary>
|
||||
public async Task<Result<ProcessResult>> RunModuleAsync(
|
||||
string moduleName,
|
||||
string[]? args = null,
|
||||
PythonOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
options ??= new PythonOptions();
|
||||
|
||||
var pythonEnv = GetPythonEnvironment(options);
|
||||
if (pythonEnv == null)
|
||||
return Result.Failure<ProcessResult>("未找到 Python 解释器");
|
||||
|
||||
var arguments = new List<string> { "-m", moduleName };
|
||||
if (args != null)
|
||||
{
|
||||
arguments.AddRange(args);
|
||||
}
|
||||
|
||||
var processOptions = new ProcessOptions
|
||||
{
|
||||
Command = pythonEnv.PythonPath,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = options.WorkingDirectory,
|
||||
Timeout = options.Timeout,
|
||||
AutoLogOutput = options.AutoLogOutput,
|
||||
OnOutputReceived = options.OnOutputReceived,
|
||||
OnErrorReceived = options.OnErrorReceived
|
||||
};
|
||||
|
||||
// 添加环境变量
|
||||
foreach (var (key, value) in options.EnvironmentVariables)
|
||||
{
|
||||
processOptions.EnvironmentVariables[key] = value;
|
||||
}
|
||||
|
||||
_logger?.LogInformation("执行 Python 模块: {Module} (Python: {PythonPath})",
|
||||
moduleName, pythonEnv.PythonPath);
|
||||
|
||||
return await _processExecutor.ExecuteAsync(processOptions, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Python 环境
|
||||
/// </summary>
|
||||
private PythonEnvironment? GetPythonEnvironment(PythonOptions options)
|
||||
{
|
||||
// 优先使用虚拟环境
|
||||
if (!string.IsNullOrEmpty(options.VirtualEnvPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
return PythonEnvironment.FromVirtualEnv(options.VirtualEnvPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "无法加载虚拟环境: {VenvPath}", options.VirtualEnvPath);
|
||||
}
|
||||
}
|
||||
|
||||
// 使用指定的 Python 路径
|
||||
if (!string.IsNullOrEmpty(options.PythonPath))
|
||||
{
|
||||
if (File.Exists(options.PythonPath))
|
||||
{
|
||||
return new PythonEnvironment { PythonPath = options.PythonPath };
|
||||
}
|
||||
|
||||
_logger?.LogWarning("指定的 Python 路径不存在: {PythonPath}", options.PythonPath);
|
||||
}
|
||||
|
||||
// 自动发现(使用缓存)
|
||||
_cachedEnvironment ??= PythonEnvironment.Discover();
|
||||
|
||||
if (_cachedEnvironment != null)
|
||||
{
|
||||
_logger?.LogDebug("使用 Python 解释器: {PythonPath} (版本: {Version})",
|
||||
_cachedEnvironment.PythonPath, _cachedEnvironment.Version);
|
||||
}
|
||||
|
||||
return _cachedEnvironment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建脚本参数列表
|
||||
/// </summary>
|
||||
private List<string> BuildScriptArguments(string scriptPath, object? args)
|
||||
{
|
||||
var arguments = new List<string> { scriptPath };
|
||||
|
||||
if (args == null)
|
||||
return arguments;
|
||||
|
||||
// 如果是字典或匿名对象,转换为命令行参数
|
||||
if (args is IDictionary<string, object> dict)
|
||||
{
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
arguments.Add($"--{key.Replace("_", "-")}");
|
||||
arguments.Add(value?.ToString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用反射提取属性
|
||||
var properties = args.GetType().GetProperties();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var name = prop.Name.Replace("_", "-").ToLowerInvariant();
|
||||
var value = prop.GetValue(args);
|
||||
|
||||
arguments.Add($"--{name}");
|
||||
arguments.Add(value?.ToString() ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace Sharp.Python;
|
||||
|
||||
/// <summary>
|
||||
/// Python 执行选项
|
||||
/// </summary>
|
||||
public class PythonOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Python 解释器路径(可选,不设置则自动发现)
|
||||
/// </summary>
|
||||
public string? PythonPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 虚拟环境路径(可选)
|
||||
/// </summary>
|
||||
public string? VirtualEnvPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工作目录(可选)
|
||||
/// </summary>
|
||||
public string? WorkingDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时时长(默认5分钟,Python脚本通常比较耗时)
|
||||
/// </summary>
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 环境变量
|
||||
/// </summary>
|
||||
public Dictionary<string, string> EnvironmentVariables { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动记录日志
|
||||
/// </summary>
|
||||
public bool AutoLogOutput { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 标准输出接收事件
|
||||
/// </summary>
|
||||
public Action<string>? OnOutputReceived { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准错误接收事件
|
||||
/// </summary>
|
||||
public Action<string>? OnErrorReceived { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
# Sharp.Python
|
||||
|
||||
Python 脚本执行封装,基于 Sharp.Process 构建,专为 C# 调用 Python 优化。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- ✅ **解释器自动发现** - 自动查找系统 Python(PATH、注册表)
|
||||
- ✅ **虚拟环境支持** - 自动激活 venv/conda 环境
|
||||
- ✅ **参数序列化** - C# 对象 → Python 命令行参数
|
||||
- ✅ **JSON 结果解析** - Python 输出 → C# 强类型对象
|
||||
- ✅ **三种执行模式** - 脚本文件 / 代码片段 / 模块
|
||||
- ✅ **实时输出流** - 同步 Python 日志到 C#
|
||||
- ✅ **Result<T> 模式** - 统一的错误处理
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. 安装
|
||||
|
||||
```bash
|
||||
dotnet add reference ../Python/Python.csproj
|
||||
```
|
||||
|
||||
### 2. 注册服务
|
||||
|
||||
```csharp
|
||||
// Program.cs 或 App.xaml.cs
|
||||
services.AddSharpPython();
|
||||
```
|
||||
|
||||
### 3. 使用
|
||||
|
||||
#### 最简单的调用
|
||||
|
||||
```csharp
|
||||
var pythonExecutor = serviceProvider.GetRequiredService<IPythonExecutor>();
|
||||
|
||||
// 执行 Python 脚本
|
||||
var result = await pythonExecutor.RunScriptAsync("train.py");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
Console.WriteLine(result.Value.StandardOutput);
|
||||
}
|
||||
```
|
||||
|
||||
#### 传递参数(C# 对象 → Python)
|
||||
|
||||
```csharp
|
||||
// C# 代码
|
||||
var result = await pythonExecutor.RunScriptAsync("process.py", new {
|
||||
input_file = "data.csv",
|
||||
output_dir = "results",
|
||||
batch_size = 32
|
||||
});
|
||||
|
||||
// 等价于执行:
|
||||
// python process.py --input-file data.csv --output-dir results --batch-size 32
|
||||
```
|
||||
|
||||
Python 脚本中接收参数:
|
||||
|
||||
```python
|
||||
# process.py
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--input-file', required=True)
|
||||
parser.add_argument('--output-dir', required=True)
|
||||
parser.add_argument('--batch-size', type=int, default=16)
|
||||
|
||||
args = parser.parse_args()
|
||||
print(f"处理文件: {args.input_file}")
|
||||
```
|
||||
|
||||
#### JSON 结果解析(Python → C# 对象)
|
||||
|
||||
```csharp
|
||||
// C# 定义结果类型
|
||||
public class PredictionResult
|
||||
{
|
||||
public double Confidence { get; set; }
|
||||
public string Label { get; set; }
|
||||
}
|
||||
|
||||
// 执行脚本并解析结果
|
||||
var result = await pythonExecutor.RunScriptAsync<PredictionResult>("predict.py", new {
|
||||
model = "gpt",
|
||||
temperature = 0.7
|
||||
});
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var prediction = result.Value;
|
||||
Console.WriteLine($"预测: {prediction.Label} (置信度: {prediction.Confidence})");
|
||||
}
|
||||
```
|
||||
|
||||
Python 脚本输出 JSON:
|
||||
|
||||
```python
|
||||
# predict.py
|
||||
import json
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--model')
|
||||
parser.add_argument('--temperature', type=float)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 执行预测...
|
||||
result = {
|
||||
"confidence": 0.95,
|
||||
"label": "cat"
|
||||
}
|
||||
|
||||
# 输出 JSON(必须是标准输出的唯一内容)
|
||||
print(json.dumps(result))
|
||||
```
|
||||
|
||||
## 典型场景
|
||||
|
||||
### 场景1: AI 模型推理
|
||||
|
||||
```csharp
|
||||
public class AIService
|
||||
{
|
||||
private readonly IPythonExecutor _python;
|
||||
|
||||
public async Task<string> ClassifyImageAsync(string imagePath)
|
||||
{
|
||||
var result = await _python.RunScriptAsync<ClassificationResult>(
|
||||
@"C:\AI\models\classify.py",
|
||||
new { image_path = imagePath, model = "resnet50" },
|
||||
new PythonOptions
|
||||
{
|
||||
WorkingDirectory = @"C:\AI\models",
|
||||
VirtualEnvPath = @"C:\AI\models\.venv",
|
||||
Timeout = TimeSpan.FromMinutes(5)
|
||||
}
|
||||
);
|
||||
|
||||
return result.Match(
|
||||
onSuccess: r => r.Label,
|
||||
onFailure: (ex, msg) => $"分类失败: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2: 数据处理脚本
|
||||
|
||||
```csharp
|
||||
public async Task ProcessCsvAsync(string inputFile)
|
||||
{
|
||||
var result = await _python.RunScriptAsync("process_csv.py", new {
|
||||
input = inputFile,
|
||||
output = "processed.csv",
|
||||
remove_duplicates = true,
|
||||
normalize = true
|
||||
}, new PythonOptions
|
||||
{
|
||||
OnOutputReceived = line => _logger.LogInformation("[Python] {Line}", line),
|
||||
AutoLogOutput = true
|
||||
});
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
MessageBox.Show("数据处理完成!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3: 执行 Python 代码片段
|
||||
|
||||
```csharp
|
||||
// 快速执行简单的 Python 代码
|
||||
var result = await _python.RunCodeAsync("print('Hello from Python!')");
|
||||
|
||||
// 计算表达式
|
||||
var calcResult = await _python.RunCodeAsync("print(2 ** 10)");
|
||||
Console.WriteLine(calcResult.Value.StandardOutput); // 1024
|
||||
```
|
||||
|
||||
### 场景4: 执行 Python 模块
|
||||
|
||||
```csharp
|
||||
// 执行 pip install
|
||||
var result = await _python.RunModuleAsync("pip", new[] { "install", "numpy" });
|
||||
|
||||
// 执行 http.server
|
||||
var result = await _python.RunModuleAsync("http.server", new[] { "8000" });
|
||||
```
|
||||
|
||||
### 场景5: WPF 中实时显示 Python 训练日志
|
||||
|
||||
```csharp
|
||||
public class TrainingViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IPythonExecutor _python;
|
||||
private string _trainingLog = "";
|
||||
|
||||
public string TrainingLog
|
||||
{
|
||||
get => _trainingLog;
|
||||
set => SetProperty(ref _trainingLog, value);
|
||||
}
|
||||
|
||||
public async Task TrainModelAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
TrainingLog = "";
|
||||
|
||||
var result = await _python.RunScriptAsync("train.py", new {
|
||||
epochs = 100,
|
||||
learning_rate = 0.001
|
||||
}, new PythonOptions
|
||||
{
|
||||
VirtualEnvPath = @"C:\AI\.venv",
|
||||
OnOutputReceived = line => {
|
||||
_dispatcher.Invoke(() => {
|
||||
TrainingLog += $"[{DateTime.Now:HH:mm:ss}] {line}\n";
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
IsBusy = false;
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_dialogService.ShowMessage("训练完成!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Python 环境管理
|
||||
|
||||
### 自动发现
|
||||
|
||||
```csharp
|
||||
var env = PythonEnvironment.Discover();
|
||||
if (env != null)
|
||||
{
|
||||
Console.WriteLine($"找到 Python: {env.PythonPath}");
|
||||
Console.WriteLine($"版本: {env.Version}");
|
||||
}
|
||||
```
|
||||
|
||||
### 使用虚拟环境
|
||||
|
||||
```csharp
|
||||
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||
{
|
||||
VirtualEnvPath = @"C:\MyProject\.venv" // 自动激活虚拟环境
|
||||
});
|
||||
```
|
||||
|
||||
### 指定 Python 路径
|
||||
|
||||
```csharp
|
||||
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||
{
|
||||
PythonPath = @"C:\Python311\python.exe"
|
||||
});
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"Python": {
|
||||
"DefaultPythonPath": "C:\\Python311\\python.exe",
|
||||
"DefaultVirtualEnvPath": ".venv",
|
||||
"DefaultTimeout": 300,
|
||||
"AutoLogOutputByDefault": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码配置
|
||||
|
||||
```csharp
|
||||
services.AddSharpPython(options => {
|
||||
options.DefaultPythonPath = @"C:\Python311\python.exe";
|
||||
options.DefaultVirtualEnvPath = ".venv";
|
||||
options.DefaultTimeout = TimeSpan.FromMinutes(10);
|
||||
options.AutoLogOutputByDefault = true;
|
||||
});
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 1. JSON 输出格式
|
||||
|
||||
Python 脚本输出 JSON 时,**必须确保 JSON 是标准输出的唯一内容**:
|
||||
|
||||
```python
|
||||
# ❌ 错误 - 包含额外输出
|
||||
print("开始处理...")
|
||||
result = {"status": "ok"}
|
||||
print(json.dumps(result))
|
||||
|
||||
# ✅ 正确 - 只输出 JSON
|
||||
import sys
|
||||
print("开始处理...", file=sys.stderr) # 日志输出到 stderr
|
||||
result = {"status": "ok"}
|
||||
print(json.dumps(result)) # JSON 输出到 stdout
|
||||
```
|
||||
|
||||
### 2. 编码问题
|
||||
|
||||
Windows 中文环境可能需要设置编码:
|
||||
|
||||
```csharp
|
||||
var result = await _python.RunScriptAsync("script.py", null, new PythonOptions
|
||||
{
|
||||
// 继承自 ProcessOptions
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
["PYTHONIOENCODING"] = "utf-8"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 长时间运行任务
|
||||
|
||||
调整超时时长,避免被意外终止:
|
||||
|
||||
```csharp
|
||||
var result = await _python.RunScriptAsync("long_task.py", null, new PythonOptions
|
||||
{
|
||||
Timeout = TimeSpan.FromHours(2)
|
||||
});
|
||||
```
|
||||
|
||||
### 4. 依赖包检查
|
||||
|
||||
执行前确保 Python 环境已安装所需的包:
|
||||
|
||||
```bash
|
||||
# 在虚拟环境中安装依赖
|
||||
cd C:\MyProject
|
||||
.venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 对比其他方案
|
||||
|
||||
| 方案 | 优势 | 劣势 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| **Sharp.Python** | 简单、轻量、跨平台 | 单向调用,无内存共享 | 脚本调用、数据处理、AI推理 |
|
||||
| **Python.NET** | 双向互操作,内存共享 | 仅 Windows,环境配置复杂 | 深度集成,频繁交互 |
|
||||
| **IronPython** | 完全嵌入 | Python 2.x,生态落后 | 已不推荐 |
|
||||
|
||||
## 依赖
|
||||
|
||||
- Sharp.Process
|
||||
- Common (Result<T>)
|
||||
- System.Text.Json
|
||||
- Microsoft.Extensions.Logging.Abstractions
|
||||
|
||||
## License
|
||||
|
||||
GNU AGPLv3
|
||||
@@ -0,0 +1,81 @@
|
||||
# Sharp Basic
|
||||
|
||||
一个基于 .NET 8 的现代化工业自动化与企业级应用开发套件,采用模块化架构设计,提供了从底层通信、基础设施到上层 UI 的完整解决方案。
|
||||
|
||||
## 🏗️ 架构概览
|
||||
|
||||
本项目包含以下核心模块,旨在实现高内聚、低耦合的开发体验:
|
||||
|
||||
### 🔌 核心基础设施
|
||||
- **[Sharp.Common](Common/README.md)**: 基础工具库。提供轻量级 DI 容器 (`Global`)、函数式结果模式 (`Result<T>`)、以及丰富的扩展方法(String, DateTime, Encryption, Reflection)。
|
||||
- **[Sharp.Config](Config/README.md)**: 强大的配置管理。支持 JSON/XML/INI/TOML 多源聚合、强类型绑定和数据注解验证。
|
||||
- **[Sharp.Log](Log/README.md)**: 统一日志门面。基于 Serilog 封装,支持一行代码接入,提供结构化日志和文件滚动策略。
|
||||
- **[Sharp.EventBus](EventBus/README.md)**: (开发中) 轻量级事件总线。
|
||||
|
||||
### 💾 数据与存储
|
||||
- **[Sharp.Data](Data/README.md)**: 极简数据访问层。基于 EF Core,提供开箱即用的仓储模式、自动审计(创建人/时间)、软删除和分页支持。
|
||||
- **[Sharp.Repository](Repository/README.md)**: 数据库集成模块。通过配置切换 SQL Server/MySQL/PostgreSQL/Sqlite,支持自动迁移。
|
||||
|
||||
### 🌐 网络与通信
|
||||
- **[Sharp.NetServer](NetServer/README.md)**: 轻量级 Web 框架。基于 `HttpListener`,提供类似 ASP.NET Core 的 Controller/Route/Middleware 开发体验,适合嵌入式或桌面应用内嵌 Web 服务。
|
||||
- **[Sharp.NetClient](NetClient/README.md)**: 强类型 HTTP 客户端。集成 Polly 重试策略、Bearer Token 认证和 multipart 上传。
|
||||
- **[Sharp.Ports](Ports/README.md)**: 串口通信库。提供串口发现、连接管理、数据监听和诊断工具。
|
||||
|
||||
### 🏭 工业互联 (IIoT)
|
||||
- **[Sharp.Plc](Plc/README.md)**: PLC 通信抽象层。定义了统一的读写、订阅接口。
|
||||
- **[Sharp.Plc.Siemens](Plc.Siemens/README.md)**: 西门子驱动实现。支持 S7-200/300/400/1200/1500,内置智能批量读取优化算法(自动合并相邻地址请求)。
|
||||
|
||||
### 🖥️ 桌面应用 UI
|
||||
- **[Sharp.Wpf](Wpf/README.md)**: WPF 基础库。提供 MVVM 基类、全局异常捕获、常用转换器、对话框服务和行为扩展。
|
||||
- **[Sharp.Client](Client/README.md)**: 综合客户端示例。集成 PLC 监控、串口调试和用户登录功能的工业控制台应用。
|
||||
- **[Sharp.Server](Server/README.md)**: 服务器控制台示例。展示了如何在一个 WPF 应用中托管 NetServer Web 服务,并提供可视化管理界面。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
- .NET 8.0 SDK
|
||||
- Visual Studio 2022 或 VS Code
|
||||
|
||||
### 构建项目
|
||||
```bash
|
||||
# 还原依赖
|
||||
dotnet restore
|
||||
|
||||
# 编译解决方案
|
||||
dotnet build Sharp.sln -c Debug
|
||||
```
|
||||
|
||||
### 运行示例
|
||||
|
||||
**1. 运行 WPF 客户端:**
|
||||
```bash
|
||||
dotnet run --project Client/Client.csproj
|
||||
```
|
||||
|
||||
**2. 运行嵌入式 Web 服务器:**
|
||||
```bash
|
||||
dotnet run --project Server/Server.csproj
|
||||
```
|
||||
|
||||
**3. 运行 NetServer 纯控制台示例:**
|
||||
```bash
|
||||
dotnet run --project NetServer/Examples
|
||||
```
|
||||
|
||||
## 🧪 测试
|
||||
|
||||
项目包含完整的单元测试,位于解决方案同级的 `test` 目录中。
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
dotnet test Sharp.sln
|
||||
```
|
||||
|
||||
## 🤝 贡献指南
|
||||
欢迎提交 Pull Request 或 Issue。请确保:
|
||||
1. 代码风格符合 .NET 规范。
|
||||
2. 新增功能包含对应的单元测试。
|
||||
3. 提交信息简洁明了。
|
||||
|
||||
## 📄 许可证
|
||||
MIT License
|
||||
@@ -1,9 +1,13 @@
|
||||
<Application x:Class="Server.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Server"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:local="clr-namespace:Server">
|
||||
<Application.Resources>
|
||||
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/Theme.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
+120
-83
@@ -1,12 +1,19 @@
|
||||
using System.Windows;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Config;
|
||||
using Sharp.Log;
|
||||
using Sharp.Log.Extensions;
|
||||
using Sharp.Data;
|
||||
using Sharp.Data.Context;
|
||||
using Sharp.Wpf.Services;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Server.Config;
|
||||
using Server.Services;
|
||||
using Server.Data;
|
||||
using Server.ViewModels;
|
||||
using Server.Views;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
@@ -15,32 +22,124 @@ namespace Server
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
public ConfigManager? ConfigManager { get; private set; }
|
||||
public AppConfig? AppConfig { get; private set; }
|
||||
public ILoggerFactory? LoggerFactory { get; private set; }
|
||||
public ServerService? ServerService { get; private set; }
|
||||
public IDataContext? DataContext { get; private set; }
|
||||
private ServiceProvider? _serviceProvider;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
// 1. Global Exception Handling
|
||||
DispatcherUnhandledException += (s, args) =>
|
||||
{
|
||||
// Safe logging: handle case where DI failed
|
||||
if (_serviceProvider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
||||
logger?.LogError(args.Exception, "UI线程未处理异常");
|
||||
}
|
||||
catch { /* Ignore logging failure during crash */ }
|
||||
}
|
||||
|
||||
MessageBox.Show($"发生未处理异常: {args.Exception.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
args.Handled = true; // Prevent crash
|
||||
};
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (s, args) =>
|
||||
{
|
||||
if (_serviceProvider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
||||
logger?.LogError(args.Exception, "任务未观察到的异常");
|
||||
}
|
||||
catch { /* Ignore */ }
|
||||
}
|
||||
args.SetObserved();
|
||||
};
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (s, args) =>
|
||||
{
|
||||
var ex = args.ExceptionObject as Exception;
|
||||
if (_serviceProvider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var logger = _serviceProvider.GetService<ILogger<App>>();
|
||||
logger?.LogCritical(ex, "AppDomain未处理异常 (即将终止: {IsTerminating})", args.IsTerminating);
|
||||
}
|
||||
catch { /* Ignore */ }
|
||||
}
|
||||
|
||||
if (args.IsTerminating)
|
||||
{
|
||||
MessageBox.Show($"发生严重错误,程序即将退出: {ex?.Message}", "严重错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// 1. 初始化配置
|
||||
InitializeConfiguration();
|
||||
// 2. 构建配置
|
||||
var cm = ConfigDefaults.BuildDefault(defaults =>
|
||||
{
|
||||
defaults.MainFile = "appsettings.json";
|
||||
defaults.IncludeEnvironmentVariables = true;
|
||||
}, configure =>
|
||||
{
|
||||
configure.ReloadOnChange = true;
|
||||
});
|
||||
|
||||
// 2. 初始化日志系统
|
||||
InitializeLogging();
|
||||
// 3. 构建依赖注入容器
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 3. 初始化数据库
|
||||
InitializeDatabase();
|
||||
// 配置管理
|
||||
services.AddSingleton(cm);
|
||||
services.AddSingleton<IConfiguration>(cm.Configuration);
|
||||
var appConfig = cm.Get<AppConfig>(validate: true);
|
||||
services.AddSingleton(appConfig);
|
||||
services.AddSingleton(appConfig.Server);
|
||||
|
||||
// 4. 初始化服务器服务
|
||||
InitializeServerService();
|
||||
// 日志
|
||||
services.AddLogging(b => b.AddSharpLog());
|
||||
|
||||
var logger = LoggerFactory?.CreateLogger<App>();
|
||||
logger?.LogInformation("应用程序启动成功");
|
||||
// 数据库
|
||||
services.AddSingleton<IDataContext>(sp =>
|
||||
{
|
||||
var builder = new DataContextBuilder<AppDbContext>();
|
||||
builder.UseSqlite("Data Source=server.db")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("system")
|
||||
.EnableDetailedErrors(true);
|
||||
return builder.Build();
|
||||
});
|
||||
|
||||
// 服务器服务
|
||||
services.AddSingleton<ServerService>();
|
||||
|
||||
// WPF服务
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
|
||||
|
||||
// ViewModels
|
||||
services.AddTransient<MainViewModel>();
|
||||
services.AddTransient<UserManagementViewModel>();
|
||||
|
||||
// Windows
|
||||
services.AddTransient<MainWindow>();
|
||||
services.AddTransient<UserManagementWindow>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// 4. 显示主窗口
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
Current.MainWindow = mainWindow;
|
||||
mainWindow.Show();
|
||||
|
||||
var logger = _serviceProvider.GetRequiredService<ILogger<App>>();
|
||||
logger.LogInformation("应用程序启动成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -54,75 +153,14 @@ namespace Server
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeConfiguration()
|
||||
{
|
||||
ConfigManager = ConfigDefaults.BuildDefault();
|
||||
AppConfig = ConfigManager.Get<AppConfig>(validate: true);
|
||||
}
|
||||
|
||||
private void InitializeLogging()
|
||||
{
|
||||
if (ConfigManager == null)
|
||||
{
|
||||
throw new InvalidOperationException("配置未初始化");
|
||||
}
|
||||
|
||||
LoggerFactory = SharpLogDefaults.CreateLoggerFactory(
|
||||
configuration: ConfigManager.Configuration,
|
||||
configure: options =>
|
||||
{
|
||||
options.Enrich.Application ??= "SharpServer";
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeDatabase()
|
||||
{
|
||||
if (LoggerFactory == null)
|
||||
{
|
||||
throw new InvalidOperationException("日志系统未初始化");
|
||||
}
|
||||
|
||||
var logger = LoggerFactory.CreateLogger<App>();
|
||||
logger.LogInformation("正在初始化数据库...");
|
||||
|
||||
try
|
||||
{
|
||||
var builder = new DataContextBuilder<AppDbContext>();
|
||||
builder.UseSqlite("Data Source=server.db")
|
||||
.UseAudit(true)
|
||||
.UseSoftDelete(true)
|
||||
.UseCurrentUser("system")
|
||||
.EnableDetailedErrors(true);
|
||||
|
||||
DataContext = builder.Build();
|
||||
|
||||
logger.LogInformation("数据库初始化成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "数据库初始化失败");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeServerService()
|
||||
{
|
||||
if (AppConfig == null || LoggerFactory == null)
|
||||
{
|
||||
throw new InvalidOperationException("配置或日志系统未初始化");
|
||||
}
|
||||
|
||||
ServerService = new ServerService(AppConfig, LoggerFactory);
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
ServerService?.Dispose();
|
||||
DataContext?.Dispose();
|
||||
_serviceProvider?.GetService<ServerService>()?.Dispose();
|
||||
_serviceProvider?.GetService<IDataContext>()?.Dispose();
|
||||
|
||||
var logger = LoggerFactory?.CreateLogger<App>();
|
||||
var logger = _serviceProvider?.GetService<ILogger<App>>();
|
||||
logger?.LogInformation("应用程序正常退出");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -132,11 +170,10 @@ namespace Server
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoggerFactory?.Dispose();
|
||||
_serviceProvider?.Dispose();
|
||||
}
|
||||
|
||||
base.OnExit(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<Window x:Class="Server.MainWindow"
|
||||
<hc:GlowWindow x:Class="Server.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:Server"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
mc:Ignorable="d"
|
||||
Title="Sharp HTTP Server" Height="600" Width="900"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
@@ -38,25 +39,33 @@
|
||||
|
||||
<!-- 服务器信息 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="服务器地址:" VerticalAlignment="Center" Margin="0,0,10,5"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="ServerUrlTextBlock" Text="http://localhost:8080"
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ServerUrl}"
|
||||
FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="服务器状态:" VerticalAlignment="Center" Margin="0,0,10,5"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="StatusTextBlock" Text="未启动"
|
||||
Foreground="Gray" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding StatusText}"
|
||||
Foreground="{Binding StatusColor}" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,0,5"/>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<StackPanel Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
|
||||
<Button x:Name="StartButton" Content="启动服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StartButton_Click"
|
||||
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button x:Name="StopButton" Content="停止服务器" Width="100" Height="32" Margin="0,0,10,0" Click="StopButton_Click"
|
||||
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
|
||||
<Button x:Name="RestartButton" Content="重启服务器" Width="100" Height="32" Margin="0,0,10,0" Click="RestartButton_Click"
|
||||
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand" IsEnabled="False"/>
|
||||
<Button x:Name="DataManagementButton" Content="数据管理" Width="100" Height="32" Margin="0,0,10,0" Click="DataManagementButton_Click"
|
||||
Background="#673AB7" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button x:Name="ClearLogButton" Content="清空日志" Width="100" Height="32" Click="ClearLogButton_Click"
|
||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="➕ 启动服务器" Width="120" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding StartCommand}"
|
||||
IsEnabled="{Binding CanStart}"
|
||||
Style="{StaticResource ButtonSuccess}"/>
|
||||
<Button Content="⏹ 停止服务器" Width="120" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding StopCommand}"
|
||||
IsEnabled="{Binding IsRunning}"
|
||||
Style="{StaticResource ButtonDanger}"/>
|
||||
<Button Content="🔄 重启服务器" Width="120" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding RestartCommand}"
|
||||
IsEnabled="{Binding IsRunning}"
|
||||
Style="{StaticResource ButtonWarning}"/>
|
||||
<Button Content="📊 数据管理" Width="120" Height="32" Margin="0,0,10,0"
|
||||
Command="{Binding OpenDataManagementCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
<Button Content="🗑 清空日志" Width="100" Height="32"
|
||||
Command="{Binding ClearLogCommand}"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -72,7 +81,7 @@
|
||||
<TextBlock Grid.Row="0" Text="日志信息" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<TextBox x:Name="LogTextBox"
|
||||
<TextBox Text="{Binding LogText, Mode=OneWay}"
|
||||
IsReadOnly="True"
|
||||
TextWrapping="Wrap"
|
||||
Background="#F5F5F5"
|
||||
@@ -110,5 +119,4 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
</hc:GlowWindow>
|
||||
@@ -1,203 +1,24 @@
|
||||
using System.Windows;
|
||||
using Server.Services;
|
||||
using Server.Config;
|
||||
using Server.Views;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using HandyControl.Controls;
|
||||
using Server.ViewModels;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
public partial class MainWindow : GlowWindow
|
||||
{
|
||||
private ServerService? _serverService;
|
||||
private AppConfig? _config;
|
||||
private ILogger<MainWindow>? _logger;
|
||||
|
||||
public MainWindow()
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += MainWindow_Loaded;
|
||||
Closing += MainWindow_Closing;
|
||||
}
|
||||
DataContext = viewModel;
|
||||
|
||||
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
// Handle window closing event
|
||||
Closing += (s, e) =>
|
||||
{
|
||||
// 从App获取依赖
|
||||
var app = (App)Application.Current;
|
||||
_config = app.AppConfig;
|
||||
_logger = app.LoggerFactory?.CreateLogger<MainWindow>();
|
||||
_serverService = app.ServerService;
|
||||
|
||||
if (_config != null && _serverService != null)
|
||||
{
|
||||
// 更新UI显示服务器地址
|
||||
ServerUrlTextBlock.Text = $"http://{_config.Server.Host}:{_config.Server.Port}";
|
||||
|
||||
// 订阅服务器状态变化事件
|
||||
_serverService.StatusChanged += OnServerStatusChanged;
|
||||
|
||||
AppendLog("应用程序已启动");
|
||||
AppendLog($"配置已加载: {_config.Server.Host}:{_config.Server.Port}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AppendLog("错误: 无法初始化服务器服务");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "MainWindow初始化失败");
|
||||
AppendLog($"初始化失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_serverService?.IsRunning == true)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"服务器正在运行,是否停止服务器并退出?",
|
||||
"确认退出",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
_serverService.Stop();
|
||||
AppendLog("服务器已停止");
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "关闭窗口时出错");
|
||||
AppendLog($"关闭出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void StartButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService?.Start();
|
||||
UpdateButtonStates(isRunning: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "启动服务器失败");
|
||||
AppendLog($"启动失败: {ex.Message}");
|
||||
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService?.Stop();
|
||||
UpdateButtonStates(isRunning: false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "停止服务器失败");
|
||||
AppendLog($"停止失败: {ex.Message}");
|
||||
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestartButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService?.Restart();
|
||||
UpdateButtonStates(isRunning: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "重启服务器失败");
|
||||
AppendLog($"重启失败: {ex.Message}");
|
||||
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearLogButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LogTextBox.Clear();
|
||||
}
|
||||
|
||||
private void DataManagementButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger?.LogInformation("打开数据管理窗口");
|
||||
var window = new UserManagementWindow
|
||||
{
|
||||
Owner = this
|
||||
};
|
||||
window.ShowDialog();
|
||||
_logger?.LogInformation("数据管理窗口已关闭");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "打开数据管理窗口失败");
|
||||
AppendLog($"打开数据管理窗口失败: {ex.Message}");
|
||||
MessageBox.Show($"打开数据管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerStatusChanged(object? sender, string status)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusTextBlock.Text = status;
|
||||
|
||||
// 根据状态更新颜色
|
||||
if (status.Contains("已启动"))
|
||||
{
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Green;
|
||||
}
|
||||
else if (status.Contains("已停止"))
|
||||
{
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Gray;
|
||||
}
|
||||
else if (status.Contains("失败") || status.Contains("错误"))
|
||||
{
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Red;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusTextBlock.Foreground = System.Windows.Media.Brushes.Orange;
|
||||
}
|
||||
|
||||
AppendLog(status);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateButtonStates(bool isRunning)
|
||||
{
|
||||
StartButton.IsEnabled = !isRunning;
|
||||
StopButton.IsEnabled = isRunning;
|
||||
RestartButton.IsEnabled = isRunning;
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
LogTextBox.AppendText($"[{timestamp}] {message}\n");
|
||||
LogTextBox.ScrollToEnd();
|
||||
});
|
||||
// Check if we need to stop the server before closing
|
||||
e.Cancel = !viewModel.CheckBeforeClose();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Commands;
|
||||
using Server.Config;
|
||||
using Server.Services;
|
||||
using Server.Views;
|
||||
|
||||
namespace Server.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 主窗口ViewModel
|
||||
/// </summary>
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ServerService _serverService;
|
||||
private readonly ILogger<MainViewModel> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
private string _statusText = "未启动";
|
||||
private Brush _statusColor = Brushes.Gray;
|
||||
private string _logText = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public MainViewModel(
|
||||
ServerService serverService,
|
||||
AppConfig config,
|
||||
ILogger<MainViewModel> logger,
|
||||
IServiceProvider serviceProvider)
|
||||
{
|
||||
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
|
||||
ServerUrl = $"http://{config.Server.Host}:{config.Server.Port}";
|
||||
|
||||
// 初始化命令
|
||||
StartCommand = new RelayCommand(_ => Start(), _ => CanStart);
|
||||
StopCommand = new RelayCommand(_ => Stop(), _ => IsRunning);
|
||||
RestartCommand = new RelayCommand(_ => Restart(), _ => IsRunning);
|
||||
ClearLogCommand = new RelayCommand(_ => ClearLog());
|
||||
OpenDataManagementCommand = new RelayCommand(_ => OpenDataManagement());
|
||||
|
||||
// 订阅服务器状态变化事件
|
||||
_serverService.StatusChanged += OnServerStatusChanged;
|
||||
|
||||
AppendLog("应用程序已启动");
|
||||
AppendLog($"配置已加载: {config.Server.Host}:{config.Server.Port}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务器URL
|
||||
/// </summary>
|
||||
public string ServerUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态颜色
|
||||
/// </summary>
|
||||
public Brush StatusColor
|
||||
{
|
||||
get => _statusColor;
|
||||
set => SetProperty(ref _statusColor, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志文本
|
||||
/// </summary>
|
||||
public string LogText
|
||||
{
|
||||
get => _logText;
|
||||
set => SetProperty(ref _logText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务器是否正在运行
|
||||
/// </summary>
|
||||
public bool IsRunning => _serverService.IsRunning;
|
||||
|
||||
/// <summary>
|
||||
/// 能否启动服务器
|
||||
/// </summary>
|
||||
public bool CanStart => !IsRunning;
|
||||
|
||||
/// <summary>
|
||||
/// 启动命令
|
||||
/// </summary>
|
||||
public ICommand StartCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 停止命令
|
||||
/// </summary>
|
||||
public ICommand StopCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 重启命令
|
||||
/// </summary>
|
||||
public ICommand RestartCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 清除日志命令
|
||||
/// </summary>
|
||||
public ICommand ClearLogCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打开数据管理命令
|
||||
/// </summary>
|
||||
public ICommand OpenDataManagementCommand { get; }
|
||||
|
||||
private void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService.Start();
|
||||
UpdateButtonStates();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "启动服务器失败");
|
||||
AppendLog($"启动失败: {ex.Message}");
|
||||
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService.Stop();
|
||||
UpdateButtonStates();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "停止服务器失败");
|
||||
AppendLog($"停止失败: {ex.Message}");
|
||||
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Restart()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serverService.Restart();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "重启服务器失败");
|
||||
AppendLog($"重启失败: {ex.Message}");
|
||||
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearLog()
|
||||
{
|
||||
LogText = string.Empty;
|
||||
}
|
||||
|
||||
private void OpenDataManagement()
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("打开数据管理窗口");
|
||||
var window = _serviceProvider.GetRequiredService<UserManagementWindow>();
|
||||
window.Owner = Application.Current.MainWindow;
|
||||
window.ShowDialog();
|
||||
_logger.LogInformation("数据管理窗口已关闭");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "打开数据管理窗口失败");
|
||||
AppendLog($"打开数据管理窗口失败: {ex.Message}");
|
||||
MessageBox.Show($"打开数据管理窗口失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnServerStatusChanged(object? sender, string status)
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
StatusText = status;
|
||||
|
||||
// 根据状态更新颜色
|
||||
if (status.Contains("已启动"))
|
||||
{
|
||||
StatusColor = Brushes.Green;
|
||||
}
|
||||
else if (status.Contains("已停止"))
|
||||
{
|
||||
StatusColor = Brushes.Gray;
|
||||
}
|
||||
else if (status.Contains("失败") || status.Contains("错误"))
|
||||
{
|
||||
StatusColor = Brushes.Red;
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusColor = Brushes.Orange;
|
||||
}
|
||||
|
||||
UpdateButtonStates();
|
||||
AppendLog(status);
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateButtonStates()
|
||||
{
|
||||
OnPropertyChanged(nameof(IsRunning));
|
||||
OnPropertyChanged(nameof(CanStart));
|
||||
}
|
||||
|
||||
private void AppendLog(string message)
|
||||
{
|
||||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
|
||||
LogText += $"[{timestamp}] {message}\n";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否需要停止服务器
|
||||
/// </summary>
|
||||
/// <returns>true表示可以关闭,false表示用户取消</returns>
|
||||
public bool CheckBeforeClose()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_serverService.IsRunning)
|
||||
{
|
||||
var result = MessageBox.Show(
|
||||
"服务器正在运行,是否停止服务器并退出?",
|
||||
"确认退出",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
_serverService.Stop();
|
||||
AppendLog("服务器已停止");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "关闭窗口时出错");
|
||||
AppendLog($"关闭出错: {ex.Message}");
|
||||
return true; // 即使出错也允许关闭
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
_serverService.StatusChanged -= OnServerStatusChanged;
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sharp.Data.Context;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Commands;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Sharp.Wpf.Services;
|
||||
using Server.Data;
|
||||
using Server.Views;
|
||||
|
||||
namespace Server.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户管理ViewModel
|
||||
/// </summary>
|
||||
public class UserManagementViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDataContext _dataContext;
|
||||
private readonly ILogger<UserManagementViewModel> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
private User? _selectedUser;
|
||||
private string _searchText = string.Empty;
|
||||
private string _statusText = string.Empty;
|
||||
private string _recordCount = "记录数: 0";
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
public UserManagementViewModel(
|
||||
IDataContext dataContext,
|
||||
ILogger<UserManagementViewModel> logger,
|
||||
IServiceProvider serviceProvider,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||
|
||||
Users = new ObservableCollection<User>();
|
||||
|
||||
// 初始化命令
|
||||
LoadDataCommand = new AsyncCommand(async () => await LoadDataAsync());
|
||||
SearchCommand = new AsyncCommand(async () => await LoadDataAsync(SearchText.Trim()));
|
||||
ClearSearchCommand = new AsyncCommand(async () =>
|
||||
{
|
||||
SearchText = string.Empty;
|
||||
await LoadDataAsync();
|
||||
});
|
||||
AddCommand = new RelayCommand(_ => Add());
|
||||
EditCommand = new RelayCommand(_ => Edit(), _ => SelectedUser != null);
|
||||
DeleteCommand = new AsyncCommand(async () => await DeleteAsync(), () => SelectedUser != null);
|
||||
RefreshCommand = new AsyncCommand(async () => await LoadDataAsync(SearchText.Trim()));
|
||||
|
||||
// 加载初始数据
|
||||
_ = LoadDataAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户列表
|
||||
/// </summary>
|
||||
public ObservableCollection<User> Users { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的用户
|
||||
/// </summary>
|
||||
public User? SelectedUser
|
||||
{
|
||||
get => _selectedUser;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedUser, value))
|
||||
{
|
||||
((RelayCommand)EditCommand).RaiseCanExecuteChanged();
|
||||
((AsyncCommand)DeleteCommand).RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 搜索文本
|
||||
/// </summary>
|
||||
public string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set => SetProperty(ref _searchText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态文本
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set => SetProperty(ref _statusText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录数
|
||||
/// </summary>
|
||||
public string RecordCount
|
||||
{
|
||||
get => _recordCount;
|
||||
set => SetProperty(ref _recordCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载数据命令
|
||||
/// </summary>
|
||||
public ICommand LoadDataCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 搜索命令
|
||||
/// </summary>
|
||||
public ICommand SearchCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 清除搜索命令
|
||||
/// </summary>
|
||||
public ICommand ClearSearchCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加命令
|
||||
/// </summary>
|
||||
public ICommand AddCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 编辑命令
|
||||
/// </summary>
|
||||
public ICommand EditCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除命令
|
||||
/// </summary>
|
||||
public ICommand DeleteCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 刷新命令
|
||||
/// </summary>
|
||||
public ICommand RefreshCommand { get; }
|
||||
|
||||
private async Task LoadDataAsync(string? searchText = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在加载数据...";
|
||||
_logger.LogInformation("开始加载用户数据,搜索条件: {SearchText}", searchText ?? "无");
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
|
||||
List<User> users;
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
users = await repo.GetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
users = await repo.FindAsync(u =>
|
||||
u.UserName.Contains(searchText) ||
|
||||
u.Email.Contains(searchText) ||
|
||||
(u.Remarks != null && u.Remarks.Contains(searchText)));
|
||||
}
|
||||
|
||||
Users.Clear();
|
||||
foreach (var user in users.OrderByDescending(u => u.CreatedAt))
|
||||
{
|
||||
Users.Add(user);
|
||||
}
|
||||
|
||||
RecordCount = $"记录数: {users.Count}";
|
||||
StatusText = string.IsNullOrWhiteSpace(searchText)
|
||||
? $"加载完成,共 {users.Count} 条记录"
|
||||
: $"搜索完成,找到 {users.Count} 条记录";
|
||||
|
||||
_logger.LogInformation("加载完成,共 {Count} 条记录", users.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载数据失败");
|
||||
StatusText = "加载失败";
|
||||
await _dialogService.ShowErrorAsync($"加载数据失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private void Add()
|
||||
{
|
||||
var dialog = new UserEditDialog();
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = SaveUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private void Edit()
|
||||
{
|
||||
if (SelectedUser == null)
|
||||
{
|
||||
_ = _dialogService.ShowInfoAsync("请选择要编辑的用户", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new UserEditDialog(SelectedUser);
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = UpdateUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
if (SelectedUser == null)
|
||||
{
|
||||
await _dialogService.ShowInfoAsync("请选择要删除的用户", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmed = await _dialogService.ShowConfirmAsync(
|
||||
$"确定要删除用户 '{SelectedUser.UserName}' 吗?\n\n(注意:这是软删除,数据会被标记为已删除但不会真正删除)",
|
||||
"确认删除");
|
||||
|
||||
if (confirmed)
|
||||
{
|
||||
await DeleteUserAsync(SelectedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在保存...";
|
||||
_logger.LogInformation("新增用户: {UserName}", user.UserName);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.AddAsync(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户新增成功: {UserId}", user.Id);
|
||||
await _dialogService.ShowInfoAsync("用户新增成功!", "成功");
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "新增用户失败");
|
||||
StatusText = "保存失败";
|
||||
await _dialogService.ShowErrorAsync($"新增用户失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在更新...";
|
||||
_logger.LogInformation("更新用户: {UserId}", user.Id);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
repo.Update(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户更新成功: {UserId}", user.Id);
|
||||
await _dialogService.ShowInfoAsync("用户更新成功!", "成功");
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "更新用户失败");
|
||||
StatusText = "更新失败";
|
||||
await _dialogService.ShowErrorAsync($"更新用户失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteUserAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusText = "正在删除...";
|
||||
_logger.LogInformation("软删除用户: {UserId}", userId);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.RemoveAsync(userId); // 软删除
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户软删除成功: {UserId}", userId);
|
||||
await _dialogService.ShowInfoAsync("用户已标记为删除(软删除)!", "成功");
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除用户失败");
|
||||
StatusText = "删除失败";
|
||||
await _dialogService.ShowErrorAsync($"删除用户失败: {ex.Message}", "错误");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<Window x:Class="Server.Views.UserManagementWindow"
|
||||
<hc:GlowWindow x:Class="Server.Views.UserManagementWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||
mc:Ignorable="d"
|
||||
Title="用户数据管理 - Sharp.Data 示例" Height="700" Width="1200"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
@@ -34,35 +35,43 @@
|
||||
<!-- 搜索框 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0,0,15,0">
|
||||
<TextBlock Text="搜索:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<TextBox x:Name="SearchTextBox" Width="250" Height="32" VerticalContentAlignment="Center"
|
||||
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
|
||||
Width="250" Height="32" VerticalContentAlignment="Center"
|
||||
Padding="8,0" BorderBrush="#BDBDBD" BorderThickness="1"/>
|
||||
<Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0" Click="SearchButton_Click"
|
||||
Background="#2196F3" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="清空" Width="80" Height="32" Margin="5,0,0,0" Click="ClearSearchButton_Click"
|
||||
Background="#9E9E9E" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="搜索" Width="80" Height="32" Margin="10,0,0,0"
|
||||
Command="{Binding SearchCommand}"
|
||||
Style="{StaticResource ButtonPrimary}"/>
|
||||
<Button Content="清空" Width="80" Height="32" Margin="5,0,0,0"
|
||||
Command="{Binding ClearSearchCommand}"
|
||||
Style="{StaticResource ButtonDefault}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 状态信息 -->
|
||||
<TextBlock Grid.Column="1" x:Name="StatusTextBlock" Text="就绪" VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="1" Text="{Binding StatusText}" VerticalAlignment="Center"
|
||||
Foreground="#666" Margin="15,0"/>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0" Click="AddButton_Click"
|
||||
Background="#4CAF50" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0" Click="EditButton_Click"
|
||||
Background="#FF9800" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="🗑️ 删除" Width="90" Height="32" Click="DeleteButton_Click"
|
||||
Background="#F44336" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0" Click="RefreshButton_Click"
|
||||
Background="#607D8B" Foreground="White" BorderThickness="0" FontWeight="Bold" Cursor="Hand"/>
|
||||
<Button Content="➕ 新增" Width="90" Height="32" Margin="0,0,5,0"
|
||||
Command="{Binding AddCommand}"
|
||||
Style="{StaticResource ButtonSuccess}"/>
|
||||
<Button Content="✏️ 编辑" Width="90" Height="32" Margin="0,0,5,0"
|
||||
Command="{Binding EditCommand}"
|
||||
Style="{StaticResource ButtonWarning}"/>
|
||||
<Button Content="🗑️ 删除" Width="90" Height="32"
|
||||
Command="{Binding DeleteCommand}"
|
||||
Style="{StaticResource ButtonDanger}"/>
|
||||
<Button Content="🔄 刷新" Width="90" Height="32" Margin="5,0,0,0"
|
||||
Command="{Binding RefreshCommand}"
|
||||
Style="{StaticResource ButtonInfo}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<Border Grid.Row="2" BorderBrush="#E0E0E0" BorderThickness="1" CornerRadius="5" Padding="10">
|
||||
<DataGrid x:Name="UsersDataGrid"
|
||||
<DataGrid ItemsSource="{Binding Users}"
|
||||
SelectedItem="{Binding SelectedUser}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
@@ -109,9 +118,9 @@
|
||||
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="数据库: SQLite (server.db)" FontSize="11" FontWeight="Bold"/>
|
||||
<TextBlock x:Name="RecordCountTextBlock" Text="记录数: 0" FontSize="11"/>
|
||||
<TextBlock Text="{Binding RecordCount}" FontSize="11"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
</hc:GlowWindow>
|
||||
@@ -1,202 +1,17 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Sharp.Data.Context;
|
||||
using Server.Data;
|
||||
using Common.ExpandFun;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using HandyControl.Controls;
|
||||
using Server.ViewModels;
|
||||
|
||||
namespace Server.Views;
|
||||
|
||||
public partial class UserManagementWindow : Window
|
||||
namespace Server.Views
|
||||
{
|
||||
private readonly IDataContext _dataContext;
|
||||
private readonly ILogger<UserManagementWindow> _logger;
|
||||
|
||||
public UserManagementWindow()
|
||||
/// <summary>
|
||||
/// Interaction logic for UserManagementWindow.xaml
|
||||
/// </summary>
|
||||
public partial class UserManagementWindow : GlowWindow
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var app = (App)Application.Current;
|
||||
_dataContext = app.DataContext ?? throw new InvalidOperationException("数据库未初始化");
|
||||
_logger = app.LoggerFactory?.CreateLogger<UserManagementWindow>()
|
||||
?? throw new InvalidOperationException("日志系统未初始化");
|
||||
|
||||
Loaded += UserManagementWindow_Loaded;
|
||||
}
|
||||
|
||||
private async void UserManagementWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync(string? searchText = null)
|
||||
{
|
||||
try
|
||||
public UserManagementWindow(UserManagementViewModel viewModel)
|
||||
{
|
||||
StatusTextBlock.Text = "正在加载数据...";
|
||||
_logger.LogInformation("开始加载用户数据,搜索条件: {SearchText}", searchText ?? "无");
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
|
||||
List<User> users;
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
{
|
||||
users = await repo.GetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
users = await repo.FindAsync(u =>
|
||||
u.UserName.Contains(searchText) ||
|
||||
u.Email.Contains(searchText) ||
|
||||
(u.Remarks != null && u.Remarks.Contains(searchText)));
|
||||
}
|
||||
|
||||
UsersDataGrid.ItemsSource = users.OrderByDescending(u => u.CreatedAt).ToList();
|
||||
RecordCountTextBlock.Text = $"记录数: {users.Count}";
|
||||
StatusTextBlock.Text = string.IsNullOrWhiteSpace(searchText)
|
||||
? $"加载完成,共 {users.Count} 条记录"
|
||||
: $"搜索完成,找到 {users.Count} 条记录";
|
||||
|
||||
_logger.LogInformation("加载完成,共 {Count} 条记录", users.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "加载数据失败");
|
||||
StatusTextBlock.Text = "加载失败";
|
||||
MessageBox.Show($"加载数据失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async void SearchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var searchText = SearchTextBox.Text.Trim();
|
||||
await LoadDataAsync(searchText);
|
||||
}
|
||||
|
||||
private async void ClearSearchButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
SearchTextBox.Clear();
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private void AddButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dialog = new UserEditDialog();
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = SaveUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private void EditButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (UsersDataGrid.SelectedItem is not User selectedUser)
|
||||
{
|
||||
MessageBox.Show("请选择要编辑的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new UserEditDialog(selectedUser);
|
||||
if (dialog.ShowDialog() == true)
|
||||
{
|
||||
_ = UpdateUserAsync(dialog.UserData);
|
||||
}
|
||||
}
|
||||
|
||||
private async void DeleteButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (UsersDataGrid.SelectedItem is not User selectedUser)
|
||||
{
|
||||
MessageBox.Show("请选择要删除的用户", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = MessageBox.Show(
|
||||
$"确定要删除用户 '{selectedUser.UserName}' 吗?\n\n(注意:这是软删除,数据会被标记为已删除但不会真正删除)",
|
||||
"确认删除",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Question);
|
||||
|
||||
if (result == MessageBoxResult.Yes)
|
||||
{
|
||||
await DeleteUserAsync(selectedUser.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await LoadDataAsync(SearchTextBox.Text.Trim());
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在保存...";
|
||||
_logger.LogInformation("新增用户: {UserName}", user.UserName);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.AddAsync(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户新增成功: {UserId}", user.Id);
|
||||
MessageBox.Show("用户新增成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "新增用户失败");
|
||||
StatusTextBlock.Text = "保存失败";
|
||||
MessageBox.Show($"新增用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateUserAsync(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在更新...";
|
||||
_logger.LogInformation("更新用户: {UserId}", user.Id);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
repo.Update(user);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户更新成功: {UserId}", user.Id);
|
||||
MessageBox.Show("用户更新成功!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "更新用户失败");
|
||||
StatusTextBlock.Text = "更新失败";
|
||||
MessageBox.Show($"更新用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteUserAsync(Guid userId)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatusTextBlock.Text = "正在删除...";
|
||||
_logger.LogInformation("软删除用户: {UserId}", userId);
|
||||
|
||||
var repo = _dataContext.Repository<User>();
|
||||
await repo.RemoveAsync(userId); // 软删除
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("用户软删除成功: {UserId}", userId);
|
||||
MessageBox.Show("用户已标记为删除(软删除)!", "成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
|
||||
await LoadDataAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "删除用户失败");
|
||||
StatusTextBlock.Text = "删除失败";
|
||||
MessageBox.Show($"删除用户失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
+229
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.35931.197 d17.13
|
||||
VisualStudioVersion = 17.13.35931.197
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{5979868C-C09D-4B3F-8909-C661F94EFFBE}"
|
||||
EndProject
|
||||
@@ -45,96 +45,324 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plc.Siemens", "Plc.Siemens\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ports", "Ports\Ports.csproj", "{BE65F468-991A-462B-B9E5-41FE4C3CF288}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cache", "Cache\Cache.csproj", "{AC6397DA-6011-44AF-A0E2-67E1A5E67938}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.HandyControl", "Wpf.HandyControl\Wpf.HandyControl.csproj", "{F33723EE-5163-40F6-A264-D521A9A7142F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Process", "Process\Process.csproj", "{5FF9393F-4881-4C35-A6C1-4D20F1117A60}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Python", "Python\Python.csproj", "{99FD794A-E5A7-4A55-8193-F30541B3EEF5}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x86.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x64.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x86.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x86.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x64.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x86.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x64.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.Build.0 = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5FF9393F-4881-4C35-A6C1-4D20F1117A60}.Release|x86.Build.0 = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x64.Build.0 = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{99FD794A-E5A7-4A55-8193-F30541B3EEF5}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
# Sharp.Wpf.HandyControl
|
||||
|
||||
> **✅ 可用 / Available**
|
||||
|
||||
HandyControl UI库集成模块,为 Sharp.Wpf 提供 HandyControl 的便捷集成和扩展。
|
||||
|
||||
## 📋 模块说明
|
||||
|
||||
本模块基于 [HandyControl](https://github.com/HandyOrg/HandyControl) UI库,提供:
|
||||
- HandyControl 主题集成
|
||||
- 便捷的服务注册扩展
|
||||
- 常用对话框封装
|
||||
- 自定义控件扩展
|
||||
|
||||
## 🎯 为什么要拆分?
|
||||
|
||||
### 模块分离策略
|
||||
|
||||
```
|
||||
Sharp.Wpf (Core)
|
||||
├── MVVM基础设施(ViewModelBase, Commands)
|
||||
├── 服务抽象(IDialogService, IFileDialogService)
|
||||
├── 转换器(BoolToVisibility, EnumToDescription)
|
||||
└── 行为(EventToCommand)
|
||||
|
||||
Sharp.Wpf.HandyControl(本模块)
|
||||
├── HandyControl主题配置
|
||||
├── IDialogService的HandyControl实现
|
||||
├── HandyControl控件扩展
|
||||
└── 样式和资源
|
||||
```
|
||||
|
||||
**优势**:
|
||||
1. **可选依赖** - 不用 HandyControl 就不引入这个模块
|
||||
2. **UI库无关** - Sharp.Wpf 可以与任何UI库配合(MaterialDesign, ModernWPF等)
|
||||
3. **按需加载** - 减少不必要的依赖
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装包
|
||||
```bash
|
||||
# 安装 Sharp.Wpf(必需)
|
||||
Install-Package Sharp.Wpf
|
||||
|
||||
# 安装 Sharp.Wpf.HandyControl(可选)
|
||||
Install-Package Sharp.Wpf.HandyControl
|
||||
```
|
||||
|
||||
### 2. 添加主题资源
|
||||
```xml
|
||||
<!-- App.xaml -->
|
||||
<Application x:Class="MyApp.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- HandyControl 主题 -->
|
||||
<hc:ThemeResources/>
|
||||
<hc:Theme/>
|
||||
|
||||
<!-- 可选:深色主题 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
```
|
||||
|
||||
### 3. 注册服务
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 注册基础 Wpf 服务
|
||||
services.AddWpfServices();
|
||||
|
||||
// 注册 HandyControl 集成服务
|
||||
services.AddHandyControlServices();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 使用 HandyControl 对话框
|
||||
```csharp
|
||||
public class MainViewModel : ViewModelBase {
|
||||
private readonly IHandyDialogService _dialogService;
|
||||
|
||||
public MainViewModel(IHandyDialogService dialogService) {
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
private void ShowMessage() {
|
||||
// HandyControl 样式的消息框
|
||||
_dialogService.ShowSuccess("操作成功!");
|
||||
_dialogService.ShowInfo("这是一条信息");
|
||||
_dialogService.ShowWarning("警告信息");
|
||||
_dialogService.ShowError("错误信息");
|
||||
}
|
||||
|
||||
private async Task<bool> Confirm() {
|
||||
// 确认对话框
|
||||
return await _dialogService.ConfirmAsync("确定要删除吗?", "确认删除");
|
||||
}
|
||||
|
||||
private async Task ShowInputDialog() {
|
||||
// 输入对话框
|
||||
var result = await _dialogService.ShowInputAsync("请输入名称:", "新建项目");
|
||||
if (result.IsSuccess) {
|
||||
ProjectName = result.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心 API
|
||||
|
||||
### IHandyDialogService 接口
|
||||
```csharp
|
||||
public interface IHandyDialogService : IDialogService {
|
||||
/// <summary>
|
||||
/// 显示成功消息(绿色,带勾图标)
|
||||
/// </summary>
|
||||
void ShowSuccess(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示信息消息(蓝色,带i图标)
|
||||
/// </summary>
|
||||
void ShowInfo(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告消息(橙色,带!图标)
|
||||
/// </summary>
|
||||
void ShowWarning(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息(红色,带X图标)
|
||||
/// </summary>
|
||||
void ShowError(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
Task<bool> ConfirmAsync(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
Task<Result<string>> ShowInputAsync(
|
||||
string message,
|
||||
string? title = null,
|
||||
string? defaultValue = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示等待对话框(异步操作)
|
||||
/// </summary>
|
||||
Task ShowWaitingAsync(string message, Func<Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// 显示进度对话框
|
||||
/// </summary>
|
||||
IProgress<double> ShowProgress(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知(Growl,右下角弹出)
|
||||
/// </summary>
|
||||
void ShowNotification(string message, NotificationType type = NotificationType.Info);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 典型使用场景
|
||||
|
||||
### 场景1:操作结果提示
|
||||
```csharp
|
||||
public class ProductViewModel : ViewModelBase {
|
||||
private readonly IHandyDialogService _dialog;
|
||||
|
||||
private async Task SaveAsync() {
|
||||
IsBusy = true;
|
||||
|
||||
var result = await _productService.SaveAsync(Product);
|
||||
|
||||
if (result.IsSuccess) {
|
||||
_dialog.ShowSuccess("保存成功!");
|
||||
} else {
|
||||
_dialog.ShowError($"保存失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:删除确认
|
||||
```csharp
|
||||
private async Task DeleteAsync() {
|
||||
var confirmed = await _dialog.ConfirmAsync(
|
||||
"确定要删除此项吗?\n删除后无法恢复。",
|
||||
"确认删除"
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
await _repository.RemoveAsync(SelectedItem.Id);
|
||||
_dialog.ShowSuccess("删除成功");
|
||||
await RefreshAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:输入对话框
|
||||
```csharp
|
||||
private async Task RenameAsync() {
|
||||
var result = await _dialog.ShowInputAsync(
|
||||
"请输入新名称:",
|
||||
"重命名",
|
||||
defaultValue: SelectedItem.Name
|
||||
);
|
||||
|
||||
if (result.IsSuccess && !string.IsNullOrEmpty(result.Value)) {
|
||||
SelectedItem.Name = result.Value;
|
||||
await _repository.UpdateAsync(SelectedItem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景4:等待对话框
|
||||
```csharp
|
||||
private async Task ExportDataAsync() {
|
||||
await _dialog.ShowWaitingAsync("正在导出数据,请稍候...", async () => {
|
||||
// 耗时操作
|
||||
await Task.Delay(3000);
|
||||
var data = await _repository.GetAllAsync();
|
||||
await _excelService.ExportAsync(data, "export.xlsx");
|
||||
});
|
||||
|
||||
_dialog.ShowSuccess("导出完成!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景5:进度对话框
|
||||
```csharp
|
||||
private async Task BatchProcessAsync() {
|
||||
var items = await _repository.GetAllAsync();
|
||||
var progress = _dialog.ShowProgress("正在处理...", "批量处理");
|
||||
|
||||
for (int i = 0; i < items.Count; i++) {
|
||||
await ProcessItemAsync(items[i]);
|
||||
progress.Report((i + 1) * 100.0 / items.Count); // 0-100
|
||||
}
|
||||
|
||||
_dialog.ShowSuccess($"处理完成,共 {items.Count} 项");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景6:通知(Growl)
|
||||
```csharp
|
||||
public class NotificationService {
|
||||
private readonly IHandyDialogService _dialog;
|
||||
|
||||
// 右下角弹出通知(不阻塞)
|
||||
public void NotifyNewMessage(string message) {
|
||||
_dialog.ShowNotification($"新消息:{message}", NotificationType.Info);
|
||||
}
|
||||
|
||||
public void NotifyAlarm(string alarmMessage) {
|
||||
_dialog.ShowNotification($"报警:{alarmMessage}", NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 主题切换
|
||||
```csharp
|
||||
public class ThemeService {
|
||||
public void SwitchToLightTheme() {
|
||||
SharedResourceDictionary.SharedDictionary.Source =
|
||||
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml");
|
||||
}
|
||||
|
||||
public void SwitchToDarkTheme() {
|
||||
SharedResourceDictionary.SharedDictionary.Source =
|
||||
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 自定义对话框
|
||||
```csharp
|
||||
public class CustomDialogViewModel : ViewModelBase {
|
||||
public string UserInput { get; set; } = "";
|
||||
|
||||
public ICommand ConfirmCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
}
|
||||
|
||||
// 使用
|
||||
var viewModel = new CustomDialogViewModel();
|
||||
var result = await _dialog.ShowDialogAsync(viewModel, "自定义对话框");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 窗口动画
|
||||
```csharp
|
||||
// HandyControl 提供的窗口动画
|
||||
public class AnimatedWindow : Window {
|
||||
public AnimatedWindow() {
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
// 启用淡入淡出动画
|
||||
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
|
||||
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 包含的 HandyControl 控件扩展
|
||||
|
||||
### 常用控件
|
||||
- **MessageBox** - 样式化消息框
|
||||
- **Growl** - 通知弹窗
|
||||
- **Dialog** - 对话框容器
|
||||
- **LoadingCircle** - 加载动画
|
||||
- **ProgressBar** - 进度条
|
||||
- **Badge** - 徽章
|
||||
- **Tag** - 标签
|
||||
- **Shield** - 盾牌图标
|
||||
- **TextBox** - 增强文本框(带水印、验证)
|
||||
- **DatePicker** - 日期选择器
|
||||
- **TimePicker** - 时间选择器
|
||||
- **ComboBox** - 下拉框
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
```csharp
|
||||
services.AddHandyControlServices(options => {
|
||||
// 默认对话框选项
|
||||
options.DefaultDialogWidth = 400;
|
||||
options.DefaultDialogHeight = 200;
|
||||
|
||||
// Growl 配置
|
||||
options.GrowlToken = "MainGrowl"; // Growl容器名称
|
||||
options.GrowlPosition = GrowlPosition.BottomRight;
|
||||
|
||||
// 主题配置
|
||||
options.DefaultTheme = Theme.Light;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 样式定制
|
||||
|
||||
### 自定义主题颜色
|
||||
```xml
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<hc:ThemeResources AccentColor="#FF0078D7"/>
|
||||
<hc:Theme/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **HandyControl 版本**:当前支持 HandyControl 3.5.0
|
||||
2. **依赖 Sharp.Wpf**:必须先引用 Sharp.Wpf 模块
|
||||
3. **主题资源**:需要在 App.xaml 中引入 HandyControl 资源字典
|
||||
4. **Growl容器**:需要在主窗口添加 Growl 容器才能使用通知功能
|
||||
|
||||
---
|
||||
|
||||
## 🔄 与其他UI库对比
|
||||
|
||||
| UI库 | 特点 | 适用场景 |
|
||||
|-----|------|---------|
|
||||
| **HandyControl** | 现代化设计、控件丰富 | 企业级应用、工具软件 |
|
||||
| **MaterialDesign** | Material风格 | 现代化Web风格应用 |
|
||||
| **ModernWPF** | Windows 11风格 | 跟随Windows系统风格 |
|
||||
| **原生WPF** | 轻量级 | 简单应用、追求性能 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 可能的扩展方向
|
||||
1. **Sharp.Wpf.MaterialDesign** - MaterialDesign UI库集成
|
||||
2. **Sharp.Wpf.ModernWPF** - ModernWPF UI库集成
|
||||
3. **自定义控件库** - 基于 HandyControl 的业务控件
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考资料
|
||||
|
||||
- [HandyControl GitHub](https://github.com/HandyOrg/HandyControl)
|
||||
- [HandyControl 官方文档](https://handyorg.github.io/)
|
||||
- [HandyControl Demo](https://github.com/HandyOrg/HandyControl/releases)
|
||||
|
||||
---
|
||||
|
||||
**状态**:规划阶段,未实现
|
||||
**依赖**:Sharp.Wpf, HandyControl 3.5.0
|
||||
**实现难度**:低(封装现有API)
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>Sharp.Wpf.HandyControl</AssemblyName>
|
||||
<RootNamespace>Sharp.Wpf.HandyControl</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using Sharp.Wpf.Services;
|
||||
using Sharp.Wpf.Navigation;
|
||||
using Sharp.Wpf.Theming;
|
||||
using Sharp.Wpf.Localization;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Sharp.Wpf.Exceptions;
|
||||
|
||||
namespace Sharp.Wpf.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// WPF 服务便捷注册扩展
|
||||
/// </summary>
|
||||
public static class WpfServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Wpf 基础服务(一行注册)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpWpf(this IServiceCollection services)
|
||||
{
|
||||
// 基础服务
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<IFileDialogService, FileDialogService>();
|
||||
services.AddSingleton<IDispatcher>(sp =>
|
||||
new DispatcherService(System.Windows.Application.Current.Dispatcher));
|
||||
|
||||
// 全局异常处理器
|
||||
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
|
||||
|
||||
// 导航服务
|
||||
services.AddSingleton<INavigationService, NavigationService>();
|
||||
|
||||
// 主题服务
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加国际化支持
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="resourceManager">资源管理器(通常是 Resources.ResourceManager)</param>
|
||||
/// <param name="supportedCultures">支持的语言</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddLocalization(
|
||||
this IServiceCollection services,
|
||||
ResourceManager resourceManager,
|
||||
params CultureInfo[] supportedCultures)
|
||||
{
|
||||
if (supportedCultures.Length == 0)
|
||||
{
|
||||
supportedCultures = new[] { new CultureInfo("zh-CN"), new CultureInfo("en-US") };
|
||||
}
|
||||
|
||||
var localizationService = new LocalizationService(resourceManager, supportedCultures);
|
||||
services.AddSingleton<ILocalizationService>(localizationService);
|
||||
|
||||
// 初始化 Markup Extension
|
||||
LocalizeExtension.Initialize(localizationService);
|
||||
LocalizeConverter.Initialize(localizationService);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 ViewModel
|
||||
/// </summary>
|
||||
public static IServiceCollection AddViewModel<TViewModel>(
|
||||
this IServiceCollection services)
|
||||
where TViewModel : ViewModelBase
|
||||
{
|
||||
services.AddTransient<TViewModel>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 Window 和对应的 ViewModel
|
||||
/// </summary>
|
||||
public static IServiceCollection AddWindow<TWindow, TViewModel>(
|
||||
this IServiceCollection services)
|
||||
where TWindow : System.Windows.Window
|
||||
where TViewModel : ViewModelBase
|
||||
{
|
||||
services.AddTransient<TWindow>();
|
||||
services.AddTransient<TViewModel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
|
||||
namespace Sharp.Wpf.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 国际化服务接口
|
||||
/// </summary>
|
||||
public interface ILocalizationService : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前语言
|
||||
/// </summary>
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 更改语言
|
||||
/// </summary>
|
||||
void ChangeCulture(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// 更改语言(通过语言代码)
|
||||
/// </summary>
|
||||
void ChangeCulture(string cultureName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串
|
||||
/// </summary>
|
||||
string GetString(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(带格式化)
|
||||
/// </summary>
|
||||
string GetString(string key, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的语言列表
|
||||
/// </summary>
|
||||
IEnumerable<CultureInfo> GetSupportedCultures();
|
||||
|
||||
/// <summary>
|
||||
/// 语言改变事件
|
||||
/// </summary>
|
||||
event EventHandler<CultureInfo>? CultureChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际化服务实现(基于 ResourceManager)
|
||||
/// </summary>
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly ResourceManager _resourceManager;
|
||||
private CultureInfo _currentCulture;
|
||||
private readonly List<CultureInfo> _supportedCultures;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<CultureInfo>? CultureChanged;
|
||||
|
||||
public CultureInfo CurrentCulture
|
||||
{
|
||||
get => _currentCulture;
|
||||
private set
|
||||
{
|
||||
if (_currentCulture != value)
|
||||
{
|
||||
_currentCulture = value;
|
||||
CultureInfo.CurrentCulture = value;
|
||||
CultureInfo.CurrentUICulture = value;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCulture)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); // 通知所有绑定刷新
|
||||
CultureChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="resourceManager">资源管理器(通常是 Resources.ResourceManager)</param>
|
||||
/// <param name="supportedCultures">支持的语言列表</param>
|
||||
public LocalizationService(ResourceManager resourceManager, params CultureInfo[] supportedCultures)
|
||||
{
|
||||
_resourceManager = resourceManager;
|
||||
_supportedCultures = supportedCultures.ToList();
|
||||
_currentCulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
// 如果当前语言不在支持列表中,使用第一个支持的语言
|
||||
if (!_supportedCultures.Any(c => c.Name == _currentCulture.Name) && _supportedCultures.Count > 0)
|
||||
{
|
||||
_currentCulture = _supportedCultures[0];
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeCulture(CultureInfo culture)
|
||||
{
|
||||
CurrentCulture = culture;
|
||||
}
|
||||
|
||||
public void ChangeCulture(string cultureName)
|
||||
{
|
||||
ChangeCulture(new CultureInfo(cultureName));
|
||||
}
|
||||
|
||||
public string GetString(string key)
|
||||
{
|
||||
return _resourceManager.GetString(key, CurrentCulture) ?? $"[{key}]";
|
||||
}
|
||||
|
||||
public string GetString(string key, params object[] args)
|
||||
{
|
||||
var format = GetString(key);
|
||||
return string.Format(format, args);
|
||||
}
|
||||
|
||||
public IEnumerable<CultureInfo> GetSupportedCultures()
|
||||
{
|
||||
return _supportedCultures;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 索引器,用于 XAML 绑定
|
||||
/// </summary>
|
||||
public string this[string key] => GetString(key);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Markup;
|
||||
using WpfBinding = System.Windows.Data.Binding;
|
||||
|
||||
namespace Sharp.Wpf.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化标记扩展,用于 XAML 中的国际化绑定
|
||||
/// 用法: {loc:Localize Key=WelcomeMessage}
|
||||
/// </summary>
|
||||
public class LocalizeExtension : MarkupExtension
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局本地化服务(在 App 启动时调用)
|
||||
/// </summary>
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源键
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
public LocalizeExtension() { }
|
||||
|
||||
public LocalizeExtension(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (_localizationService == null)
|
||||
{
|
||||
return $"[{Key}]";
|
||||
}
|
||||
|
||||
// 创建绑定到 LocalizationService 的 Binding
|
||||
var binding = new WpfBinding($"[{Key}]")
|
||||
{
|
||||
Source = _localizationService,
|
||||
Mode = BindingMode.OneWay
|
||||
};
|
||||
|
||||
return binding.ProvideValue(serviceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地化转换器(用于动态参数)
|
||||
/// </summary>
|
||||
public class LocalizeConverter : IValueConverter
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (_localizationService == null || parameter is not string key)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return _localizationService.GetString(key, value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.Windows;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
|
||||
namespace Sharp.Wpf.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// 导航服务接口
|
||||
/// </summary>
|
||||
public interface INavigationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 导航到指定 ViewModel
|
||||
/// </summary>
|
||||
Task NavigateAsync<TViewModel>() where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 导航到指定 ViewModel(带参数)
|
||||
/// </summary>
|
||||
Task NavigateAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 导航并等待结果
|
||||
/// </summary>
|
||||
Task<TResult?> NavigateWithResultAsync<TViewModel, TResult>() where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 后退
|
||||
/// </summary>
|
||||
Task<bool> GoBackAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 前进
|
||||
/// </summary>
|
||||
Task<bool> GoForwardAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 清除导航历史
|
||||
/// </summary>
|
||||
void ClearHistory();
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以后退
|
||||
/// </summary>
|
||||
bool CanGoBack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以前进
|
||||
/// </summary>
|
||||
bool CanGoForward { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前 ViewModel
|
||||
/// </summary>
|
||||
ViewModelBase? CurrentViewModel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 导航事件
|
||||
/// </summary>
|
||||
event EventHandler<ViewModelBase>? Navigated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航服务实现(基于 Frame 或自定义容器)
|
||||
/// </summary>
|
||||
public class NavigationService : INavigationService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Stack<ViewModelBase> _backStack = new();
|
||||
private readonly Stack<ViewModelBase> _forwardStack = new();
|
||||
private ViewModelBase? _currentViewModel;
|
||||
|
||||
public ViewModelBase? CurrentViewModel
|
||||
{
|
||||
get => _currentViewModel;
|
||||
private set
|
||||
{
|
||||
_currentViewModel = value;
|
||||
if (value != null)
|
||||
{
|
||||
Navigated?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanGoBack => _backStack.Count > 0;
|
||||
public bool CanGoForward => _forwardStack.Count > 0;
|
||||
|
||||
public event EventHandler<ViewModelBase>? Navigated;
|
||||
|
||||
public NavigationService(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task NavigateAsync<TViewModel>() where TViewModel : ViewModelBase
|
||||
{
|
||||
await NavigateAsync<TViewModel>(null);
|
||||
}
|
||||
|
||||
public async Task NavigateAsync<TViewModel>(object? parameter) where TViewModel : ViewModelBase
|
||||
{
|
||||
var viewModel = _serviceProvider.GetService(typeof(TViewModel)) as TViewModel
|
||||
?? throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未注册");
|
||||
|
||||
// 保存当前 ViewModel 到后退栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
}
|
||||
|
||||
// 清空前进栈
|
||||
_forwardStack.Clear();
|
||||
|
||||
CurrentViewModel = viewModel;
|
||||
|
||||
// 如果 ViewModel 实现了 INavigationAware,调用生命周期方法
|
||||
if (viewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResult?> NavigateWithResultAsync<TViewModel, TResult>() where TViewModel : ViewModelBase
|
||||
{
|
||||
var viewModel = _serviceProvider.GetService(typeof(TViewModel)) as TViewModel
|
||||
?? throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未注册");
|
||||
|
||||
// 如果 ViewModel 实现了结果接口
|
||||
if (viewModel is INavigationResult<TResult> resultViewModel)
|
||||
{
|
||||
// 保存当前 ViewModel
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
}
|
||||
|
||||
CurrentViewModel = viewModel;
|
||||
|
||||
if (viewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
// 等待结果
|
||||
var result = await resultViewModel.GetResultAsync();
|
||||
|
||||
// 自动后退
|
||||
await GoBackAsync();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未实现 INavigationResult<{typeof(TResult).Name}>");
|
||||
}
|
||||
|
||||
public async Task<bool> GoBackAsync()
|
||||
{
|
||||
if (!CanGoBack) return false;
|
||||
|
||||
// 保存当前 ViewModel 到前进栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_forwardStack.Push(CurrentViewModel);
|
||||
|
||||
if (CurrentViewModel is INavigationAware currentAware)
|
||||
{
|
||||
await currentAware.OnNavigatedFromAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出后退栈
|
||||
var previousViewModel = _backStack.Pop();
|
||||
CurrentViewModel = previousViewModel;
|
||||
|
||||
if (previousViewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> GoForwardAsync()
|
||||
{
|
||||
if (!CanGoForward) return false;
|
||||
|
||||
// 保存当前 ViewModel 到后退栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
|
||||
if (CurrentViewModel is INavigationAware currentAware)
|
||||
{
|
||||
await currentAware.OnNavigatedFromAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出前进栈
|
||||
var nextViewModel = _forwardStack.Pop();
|
||||
CurrentViewModel = nextViewModel;
|
||||
|
||||
if (nextViewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearHistory()
|
||||
{
|
||||
_backStack.Clear();
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航生命周期接口
|
||||
/// </summary>
|
||||
public interface INavigationAware
|
||||
{
|
||||
/// <summary>
|
||||
/// 导航到此 ViewModel 时调用
|
||||
/// </summary>
|
||||
Task OnNavigatedToAsync(object? parameter);
|
||||
|
||||
/// <summary>
|
||||
/// 离开此 ViewModel 时调用
|
||||
/// </summary>
|
||||
Task OnNavigatedFromAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航结果接口
|
||||
/// </summary>
|
||||
public interface INavigationResult<TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取导航结果
|
||||
/// </summary>
|
||||
Task<TResult?> GetResultAsync();
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
using WpfApplication = System.Windows.Application;
|
||||
|
||||
namespace Sharp.Wpf.Theming;
|
||||
|
||||
/// <summary>
|
||||
/// 主题枚举
|
||||
/// </summary>
|
||||
public enum Theme
|
||||
{
|
||||
Light,
|
||||
Dark,
|
||||
System // 跟随系统
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主题服务接口
|
||||
/// </summary>
|
||||
public interface IThemeService
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前主题
|
||||
/// </summary>
|
||||
Theme CurrentTheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题
|
||||
/// </summary>
|
||||
void ApplyTheme(Theme theme);
|
||||
|
||||
/// <summary>
|
||||
/// 获取主题颜色
|
||||
/// </summary>
|
||||
MediaColor GetColor(string colorKey);
|
||||
|
||||
/// <summary>
|
||||
/// 设置主题颜色
|
||||
/// </summary>
|
||||
void SetColor(string colorKey, MediaColor color);
|
||||
|
||||
/// <summary>
|
||||
/// 主题改变事件
|
||||
/// </summary>
|
||||
event EventHandler<Theme>? ThemeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主题服务实现
|
||||
/// </summary>
|
||||
public class ThemeService : IThemeService
|
||||
{
|
||||
private Theme _currentTheme = Theme.Light;
|
||||
private readonly ResourceDictionary _lightTheme;
|
||||
private readonly ResourceDictionary _darkTheme;
|
||||
|
||||
public Theme CurrentTheme
|
||||
{
|
||||
get => _currentTheme;
|
||||
private set
|
||||
{
|
||||
if (_currentTheme != value)
|
||||
{
|
||||
_currentTheme = value;
|
||||
ThemeChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<Theme>? ThemeChanged;
|
||||
|
||||
public ThemeService()
|
||||
{
|
||||
_lightTheme = CreateLightTheme();
|
||||
_darkTheme = CreateDarkTheme();
|
||||
}
|
||||
|
||||
public void ApplyTheme(Theme theme)
|
||||
{
|
||||
var actualTheme = theme;
|
||||
|
||||
// 如果选择跟随系统,检测系统主题
|
||||
if (theme == Theme.System)
|
||||
{
|
||||
actualTheme = DetectSystemTheme();
|
||||
}
|
||||
|
||||
var themeDict = actualTheme == Theme.Light ? _lightTheme : _darkTheme;
|
||||
|
||||
// 移除旧主题
|
||||
var oldTheme = WpfApplication.Current.Resources.MergedDictionaries
|
||||
.FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
|
||||
if (oldTheme != null)
|
||||
{
|
||||
WpfApplication.Current.Resources.MergedDictionaries.Remove(oldTheme);
|
||||
}
|
||||
|
||||
// 应用新主题
|
||||
WpfApplication.Current.Resources.MergedDictionaries.Add(themeDict);
|
||||
|
||||
CurrentTheme = theme;
|
||||
}
|
||||
|
||||
public MediaColor GetColor(string colorKey)
|
||||
{
|
||||
if (WpfApplication.Current.Resources[colorKey] is MediaColor color)
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
if (WpfApplication.Current.Resources[colorKey] is SolidColorBrush brush)
|
||||
{
|
||||
return brush.Color;
|
||||
}
|
||||
|
||||
return Colors.Transparent;
|
||||
}
|
||||
|
||||
public void SetColor(string colorKey, MediaColor color)
|
||||
{
|
||||
WpfApplication.Current.Resources[colorKey] = new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
private Theme DetectSystemTheme()
|
||||
{
|
||||
// Windows 10/11 检测系统主题
|
||||
try
|
||||
{
|
||||
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
|
||||
var value = key?.GetValue("AppsUseLightTheme");
|
||||
if (value is int intValue)
|
||||
{
|
||||
return intValue == 0 ? Theme.Dark : Theme.Light;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误,返回默认主题
|
||||
}
|
||||
|
||||
return Theme.Light;
|
||||
}
|
||||
|
||||
private ResourceDictionary CreateLightTheme()
|
||||
{
|
||||
return new ResourceDictionary
|
||||
{
|
||||
// 基础颜色
|
||||
["PrimaryColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
["BackgroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["ForegroundColor"] = new SolidColorBrush(Colors.Black),
|
||||
["BorderColor"] = new SolidColorBrush(MediaColor.FromRgb(204, 204, 204)),
|
||||
|
||||
// 状态颜色
|
||||
["SuccessColor"] = new SolidColorBrush(MediaColor.FromRgb(16, 124, 16)),
|
||||
["WarningColor"] = new SolidColorBrush(MediaColor.FromRgb(255, 185, 0)),
|
||||
["ErrorColor"] = new SolidColorBrush(MediaColor.FromRgb(232, 17, 35)),
|
||||
["InfoColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
|
||||
// 控件颜色
|
||||
["ButtonBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(240, 240, 240)),
|
||||
["ButtonHoverColor"] = new SolidColorBrush(MediaColor.FromRgb(229, 229, 229)),
|
||||
["ButtonPressedColor"] = new SolidColorBrush(MediaColor.FromRgb(204, 204, 204)),
|
||||
|
||||
["InputBackgroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["InputBorderColor"] = new SolidColorBrush(MediaColor.FromRgb(171, 173, 179)),
|
||||
};
|
||||
}
|
||||
|
||||
private ResourceDictionary CreateDarkTheme()
|
||||
{
|
||||
return new ResourceDictionary
|
||||
{
|
||||
// 基础颜色
|
||||
["PrimaryColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
["BackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(32, 32, 32)),
|
||||
["ForegroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["BorderColor"] = new SolidColorBrush(MediaColor.FromRgb(96, 96, 96)),
|
||||
|
||||
// 状态颜色
|
||||
["SuccessColor"] = new SolidColorBrush(MediaColor.FromRgb(16, 124, 16)),
|
||||
["WarningColor"] = new SolidColorBrush(MediaColor.FromRgb(255, 185, 0)),
|
||||
["ErrorColor"] = new SolidColorBrush(MediaColor.FromRgb(232, 17, 35)),
|
||||
["InfoColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
|
||||
// 控件颜色
|
||||
["ButtonBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(48, 48, 48)),
|
||||
["ButtonHoverColor"] = new SolidColorBrush(MediaColor.FromRgb(64, 64, 64)),
|
||||
["ButtonPressedColor"] = new SolidColorBrush(MediaColor.FromRgb(32, 32, 32)),
|
||||
|
||||
["InputBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(45, 45, 45)),
|
||||
["InputBorderColor"] = new SolidColorBrush(MediaColor.FromRgb(96, 96, 96)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
|
||||
namespace Sharp.Wpf.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// 支持验证的 ViewModel 基类(实现 INotifyDataErrorInfo)
|
||||
/// </summary>
|
||||
public abstract class ValidatableViewModelBase : ViewModelBase, INotifyDataErrorInfo
|
||||
{
|
||||
private readonly Dictionary<string, List<string>> _errors = new();
|
||||
|
||||
public bool HasErrors => _errors.Count > 0;
|
||||
|
||||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
public IEnumerable GetErrors(string? propertyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
return _errors.Values.SelectMany(e => e);
|
||||
}
|
||||
|
||||
return _errors.TryGetValue(propertyName!, out var errors) ? errors : Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置属性值并验证
|
||||
/// </summary>
|
||||
protected bool SetProperty<T>(ref T storage, T value, bool validate = true, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(storage, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
if (validate && propertyName != null)
|
||||
{
|
||||
ValidateProperty(value, propertyName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证单个属性
|
||||
/// </summary>
|
||||
protected void ValidateProperty(object? value, string propertyName)
|
||||
{
|
||||
ClearErrors(propertyName);
|
||||
|
||||
var context = new ValidationContext(this) { MemberName = propertyName };
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateProperty(value, context, results))
|
||||
{
|
||||
var errors = results.Select(r => r.ErrorMessage ?? "验证失败").ToList();
|
||||
_errors[propertyName] = errors;
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证所有属性
|
||||
/// </summary>
|
||||
public bool ValidateAll()
|
||||
{
|
||||
var context = new ValidationContext(this);
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateObject(this, context, results, validateAllProperties: true))
|
||||
{
|
||||
// 清除旧错误
|
||||
var oldErrorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
|
||||
// 添加新错误
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var memberName in result.MemberNames)
|
||||
{
|
||||
if (!_errors.ContainsKey(memberName))
|
||||
{
|
||||
_errors[memberName] = new List<string>();
|
||||
}
|
||||
_errors[memberName].Add(result.ErrorMessage ?? "验证失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有属性错误变化
|
||||
var allProperties = oldErrorProperties.Union(_errors.Keys).Distinct();
|
||||
foreach (var property in allProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清除所有错误
|
||||
var errorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
foreach (var property in errorProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定属性的错误
|
||||
/// </summary>
|
||||
protected void ClearErrors(string propertyName)
|
||||
{
|
||||
if (_errors.Remove(propertyName))
|
||||
{
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有错误
|
||||
/// </summary>
|
||||
protected void ClearAllErrors()
|
||||
{
|
||||
var errorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
foreach (var property in errorProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加自定义错误
|
||||
/// </summary>
|
||||
protected void AddError(string propertyName, string error)
|
||||
{
|
||||
if (!_errors.ContainsKey(propertyName))
|
||||
{
|
||||
_errors[propertyName] = new List<string>();
|
||||
}
|
||||
|
||||
if (!_errors[propertyName].Contains(error))
|
||||
{
|
||||
_errors[propertyName].Add(error);
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnErrorsChanged(string propertyName)
|
||||
{
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
OnPropertyChanged(nameof(HasErrors));
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,8 @@
|
||||
<RootNamespace>Sharp.Wpf</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+bc09280d4ea03ae8d0db7cd8c25ddd02d63abd84")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+433e206d456d0ef3b2fc57c458ff3fc030643be6")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
d55a55a135e80fbfbfad99db94d1d2c02d22ff2d590de8697e3ccba5e8c5d43a
|
||||
cd23ae979f4410b7a90bcdab8832e81e422253dc2d502d88544f9372c3b0bd4d
|
||||
Binary file not shown.
@@ -49,6 +49,12 @@
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"target": "Package",
|
||||
"version": "[9.0.0, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
|
||||
@@ -1,11 +1,61 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
"net8.0-windows7.0": {}
|
||||
"net8.0-windows7.0": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": {
|
||||
"type": "package",
|
||||
"compile": {
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"buildTransitive/net8.0/_._": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": {
|
||||
"sha512": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==",
|
||||
"type": "package",
|
||||
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.0",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"Icon.png",
|
||||
"LICENSE.TXT",
|
||||
"PACKAGE.md",
|
||||
"THIRD-PARTY-NOTICES.TXT",
|
||||
"buildTransitive/net461/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
|
||||
"buildTransitive/net462/_._",
|
||||
"buildTransitive/net8.0/_._",
|
||||
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
|
||||
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"microsoft.extensions.dependencyinjection.abstractions.9.0.0.nupkg.sha512",
|
||||
"microsoft.extensions.dependencyinjection.abstractions.nuspec",
|
||||
"useSharedDesignerContext.txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net8.0-windows7.0": []
|
||||
"net8.0-windows7.0": [
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions >= 9.0.0"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\Administrator\\.nuget\\packages\\": {},
|
||||
@@ -56,6 +106,12 @@
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"target": "Package",
|
||||
"version": "[9.0.0, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "NHqLv58UxH0=",
|
||||
"dgSpecHash": "zqaFYIDtlpU=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\Administrator\\.nuget\\packages\\microsoft.extensions.dependencyinjection.abstractions\\9.0.0\\microsoft.extensions.dependencyinjection.abstractions.9.0.0.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
"restore":{"projectUniqueName":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","projectName":"Sharp.Wpf","projectPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","outputPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"],"originalTargetFrameworks":["net8.0-windows"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.100"}"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"},"Microsoft.WindowsDesktop.App":{"privateAssets":"none"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\10.0.100/PortableRuntimeIdentifierGraph.json"}}
|
||||
"restore":{"projectUniqueName":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","projectName":"Sharp.Wpf","projectPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","outputPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"],"originalTargetFrameworks":["net8.0-windows"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.100"}"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","dependencies":{"Microsoft.Extensions.DependencyInjection.Abstractions":{"target":"Package","version":"[9.0.0, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"},"Microsoft.WindowsDesktop.App":{"privateAssets":"none"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\10.0.100/PortableRuntimeIdentifierGraph.json"}}
|
||||
@@ -1 +1 @@
|
||||
17652582922004239
|
||||
17654362814006268
|
||||
@@ -1 +1 @@
|
||||
17652582922004239
|
||||
17657764918263100
|
||||
@@ -22,7 +22,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in new issue
Block a user