U:调整调用方式和兼容性

This commit is contained in:
jimmy committed 2025-12-15 13:37:11 +08:00
1 parent 433e206d45
commit 4ec76b7855
50 files changed
+5592 -47

No files matched your search

+194
View File
@@ -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 可以更好地管理依赖关系,便于测试。
---
**开始编码吧!** 🚀
+233
View File
@@ -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)
---
**掌握表单验证,构建高质量的用户输入体验!** ✅
+315
View File
@@ -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)
---
**掌握导航系统,构建流畅的多页面应用!** 🧭
+278
View File
@@ -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)
---
**构建面向全球用户的应用!** 🌍
+312
View File
@@ -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)
---
**打造美观现代的用户界面!** 🎨
+569
View File
@@ -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 框架文档
---
**这是一个完整的工业监控系统模板,可以直接用于实际项目开发!** 🏭
+273
View File
@@ -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 之旅吧!** 🎉
+16
View File
@@ -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>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>
+83
View File
@@ -0,0 +1,83 @@
namespace 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)
{
// 简化实现:只支持前缀匹配(如 "user:*")
if (!pattern.EndsWith("*"))
{
cache.Remove(pattern);
return;
}
var prefix = pattern.TrimEnd('*');
// 注意:此实现需要 MemoryCacheService 暴露所有键
// 简化版本:只能移除单个键
// 完整实现需要使用反射或其他方式访问内部键集合
}
}
@@ -0,0 +1,39 @@
using Microsoft.Extensions.DependencyInjection;
namespace 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;
}
}
+142
View File
@@ -0,0 +1,142 @@
using Microsoft.Extensions.Caching.Memory;
namespace 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>
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 bool Exists(string key)
{
return _cache.TryGetValue(key, out _);
}
}
+623
View File
@@ -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内置)
+2 -2
View File
@@ -9,8 +9,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="HandyControl" Version="3.5.0" /> <PackageReference Include="HandyControl" Version="3.5.0" />
</ItemGroup> </ItemGroup>
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")] [assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [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.AssemblyProductAttribute("Client")]
[assembly: System.Reflection.AssemblyTitleAttribute("Client")] [assembly: System.Reflection.AssemblyTitleAttribute("Client")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
b775c782e822e9da489917907ac5dc67ababf8e2b94747f6176fcf3d21ac9454 de45fc2ddda59d691d6bfd3f2673675ae92a6f2f53cc261c9aeefe56c546545d
+7
View File
@@ -6,4 +6,11 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </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> </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
);
}
}
+8 -8
View File
@@ -9,18 +9,18 @@
<ItemGroup> <ItemGroup>
<!-- EF Core --> <!-- EF Core -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
<!-- 数据库提供者 --> <!-- 数据库提供者 -->
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.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>
<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;
}
}
+451 -6
View File
@@ -2,11 +2,456 @@
> **🚧 开发中 / Under Construction** > **🚧 开发中 / Under Construction**
这是一个预留的事件总线模块,计划用于实现进程内(In-Process)或分布式(Distributed)的消息发布/订阅模式。 轻量级进程内事件总线,用于解耦应用程序中的模块通信。
## 计划功能 ## 📋 为什么需要 EventBus?
- 定义统一的 `IEventBus` 接口。
- 提供基于内存(Channel/MemoryCache)的默认实现。
- 提供集成 RabbitMQ / Redis 的扩展实现。
目前该目录仅包含占位符。 ### 场景对比
#### ❌ 不使用 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 容器 + 委托事件
+82
View File
@@ -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.
+3 -3
View File
@@ -7,9 +7,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Http" Version="8.*" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.*" /> <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" /> <PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" />
</ItemGroup> </ItemGroup>
+3 -3
View File
@@ -14,9 +14,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -3
View File
@@ -11,9 +11,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
<PackageReference Include="S7netplus" Version="0.20.0" /> <PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup> </ItemGroup>
+2 -2
View File
@@ -11,8 +11,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+3 -3
View File
@@ -12,9 +12,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.IO.Ports" Version="8.0.0" /> <PackageReference Include="System.IO.Ports" Version="9.0.0" />
<PackageReference Include="System.Management" Version="8.0.0" /> <PackageReference Include="System.Management" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+81
View File
@@ -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
+3 -3
View File
@@ -25,9 +25,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+201 -1
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13 VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{5979868C-C09D-4B3F-8909-C661F94EFFBE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{5979868C-C09D-4B3F-8909-C661F94EFFBE}"
EndProject EndProject
@@ -45,96 +45,296 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plc.Siemens", "Plc.Siemens\
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ports", "Ports\Ports.csproj", "{BE65F468-991A-462B-B9E5-41FE4C3CF288}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ports", "Ports\Ports.csproj", "{BE65F468-991A-462B-B9E5-41FE4C3CF288}"
EndProject 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
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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|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.ActiveCfg = Release|Any CPU
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|Any CPU.Build.0 = 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
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+435
View File
@@ -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();
}
+12
View File
@@ -111,3 +111,15 @@ var folder = await new FileDialogService().SelectFolderAsync();
- 若要禁用全局异常弹窗,可在 `ExceptionHandlingOptions` 中设置 `ShowDialog = false` 并仅使用 `Log` 钩子记录。 - 若要禁用全局异常弹窗,可在 `ExceptionHandlingOptions` 中设置 `ShowDialog = false` 并仅使用 `Log` 钩子记录。
- `EventToCommand` 通过反射订阅事件,适用于大多数 Routed 事件;如需传递事件参数,可设置 `PassEventArgsToCommand="True"`。 - `EventToCommand` 通过反射订阅事件,适用于大多数 Routed 事件;如需传递事件参数,可设置 `PassEventArgsToCommand="True"`。
我们主要使用Data模块,Repository模块不是非常常用。增加你提到的功能,并在Client模块提供对应功能测试、使用案例,使用Sqlite数据库作为测试数据库。
对于NetServer增加JWT验证选项(可选使用),增加对于WebSocket的支持、API版本支持、健康端点检查、模型验证等,并且Server模块增加对应功能的测试、使用案例,在Client进行连接通信
对于PLC模块增加Modbus的支持,并在Server中提供使用示例。
Ports 模块增加ModbusRTU功能的支持、帧同步器、虚拟串口,并且在Client中增加对应功能的演示
这么多模块和功能,他们分别是客户端和服务端的功能,能不能让Client和Server集成他们需要集成的内容,尽可能的内容复用,各个模块之间也进行引用减少重复代码。从这个角度审视目前的所有的代码,并重新设计并在Client和Server中进行集成。并且
Client和Server页面也使用HandyControl
+198
View File
@@ -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));
}
}
+4
View File
@@ -11,4 +11,8 @@
<RootNamespace>Sharp.Wpf</RootNamespace> <RootNamespace>Sharp.Wpf</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project> </Project>
@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")] [assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [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.AssemblyProductAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")] [assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
@@ -1 +1 @@
d55a55a135e80fbfbfad99db94d1d2c02d22ff2d590de8697e3ccba5e8c5d43a cd23ae979f4410b7a90bcdab8832e81e422253dc2d502d88544f9372c3b0bd4d
@@ -49,6 +49,12 @@
"frameworks": { "frameworks": {
"net8.0-windows7.0": { "net8.0-windows7.0": {
"targetAlias": "net8.0-windows", "targetAlias": "net8.0-windows",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"target": "Package",
"version": "[9.0.0, )"
}
},
"imports": [ "imports": [
"net461", "net461",
"net462", "net462",
+59 -3
View File
@@ -1,11 +1,61 @@
{ {
"version": 3, "version": 3,
"targets": { "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": { "projectFileDependencyGroups": {
"net8.0-windows7.0": [] "net8.0-windows7.0": [
"Microsoft.Extensions.DependencyInjection.Abstractions >= 9.0.0"
]
}, },
"packageFolders": { "packageFolders": {
"C:\\Users\\Administrator\\.nuget\\packages\\": {}, "C:\\Users\\Administrator\\.nuget\\packages\\": {},
@@ -56,6 +106,12 @@
"frameworks": { "frameworks": {
"net8.0-windows7.0": { "net8.0-windows7.0": {
"targetAlias": "net8.0-windows", "targetAlias": "net8.0-windows",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"target": "Package",
"version": "[9.0.0, )"
}
},
"imports": [ "imports": [
"net461", "net461",
"net462", "net462",
+4 -2
View File
@@ -1,8 +1,10 @@
{ {
"version": 2, "version": 2,
"dgSpecHash": "NHqLv58UxH0=", "dgSpecHash": "zqaFYIDtlpU=",
"success": true, "success": true,
"projectFilePath": "D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj", "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": [] "logs": []
} }
+1 -1
View File
@@ -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
View File
@@ -1 +1 @@
17652582922004239 17657764918263100
+1 -1
View File
@@ -22,7 +22,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>