U:调整调用方式和兼容性
This commit is contained in:
1 parent
433e206d45
commit
4ec76b7855
50 files changed
+5592
-47
No files matched your search
@@ -0,0 +1,194 @@
|
||||
# 01 - 最小 WPF 应用
|
||||
|
||||
这是一个最简单的 Sharp WPF 应用示例,演示了框架的基础用法。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 了解 Sharp 框架的基本结构
|
||||
- 掌握 MVVM 模式(ViewModelBase)
|
||||
- 使用 Commands(RelayCommand, AsyncCommand)
|
||||
- 使用对话框服务(IDialogService)
|
||||
- 理解 Quick Start 配置方式
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Common** - Result<T>, 基础工具
|
||||
- **Sharp.Config** - 配置管理
|
||||
- **Sharp.Wpf** - MVVM 基础设施
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
01-MinimalWpfApp/
|
||||
├── App.xaml # 应用程序入口
|
||||
├── App.xaml.cs # Quick Start 配置
|
||||
├── MainWindow.xaml # 主窗口界面
|
||||
├── MainWindow.xaml.cs # 主窗口代码隐藏
|
||||
├── ViewModels/
|
||||
│ └── MainViewModel.cs # 主窗口 ViewModel
|
||||
├── appsettings.json # 配置文件
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/01-MinimalWpfApp
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 应用启动
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 一行添加基础服务(配置、日志、缓存)
|
||||
services.AddSharpEssentials();
|
||||
|
||||
// 一行添加 WPF 服务(对话框、调度器、导航、主题)
|
||||
services.AddSharpWpf();
|
||||
|
||||
// 注册 ViewModel 和 Window
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
// 构建服务提供者
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// 显示主窗口
|
||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- `AddSharpEssentials()` - 自动配置 Config + Log + Cache
|
||||
- `AddSharpWpf()` - 自动注册 WPF 服务
|
||||
- 无需手动配置,使用约定优于配置
|
||||
|
||||
### 2. MainViewModel.cs - ViewModel 示例
|
||||
|
||||
```csharp
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private string _message = "Hello, Sharp!";
|
||||
private int _counter = 0;
|
||||
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
public int Counter
|
||||
{
|
||||
get => _counter;
|
||||
set => SetProperty(ref _counter, value);
|
||||
}
|
||||
|
||||
// 同步命令
|
||||
public ICommand IncrementCommand { get; }
|
||||
|
||||
// 异步命令
|
||||
public ICommand ShowDialogCommand { get; }
|
||||
|
||||
public MainViewModel(IDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
|
||||
// RelayCommand - 同步操作
|
||||
IncrementCommand = new RelayCommand(() => {
|
||||
Counter++;
|
||||
Message = $"你点击了 {Counter} 次";
|
||||
});
|
||||
|
||||
// AsyncCommand - 异步操作
|
||||
ShowDialogCommand = new AsyncCommand(async (ct) => {
|
||||
IsBusy = true; // 显示加载状态
|
||||
|
||||
// 模拟耗时操作
|
||||
await Task.Delay(1000, ct);
|
||||
|
||||
_dialogService.ShowMessage($"当前计数: {Counter}", "提示");
|
||||
|
||||
IsBusy = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 继承 `ViewModelBase` 获得 INotifyPropertyChanged
|
||||
- 使用 `SetProperty()` 自动触发属性变化通知
|
||||
- `IsBusy` 属性用于显示加载状态
|
||||
- `RelayCommand` 用于同步操作
|
||||
- `AsyncCommand` 用于异步操作(支持取消)
|
||||
|
||||
### 3. MainWindow.xaml - 界面绑定
|
||||
|
||||
```xml
|
||||
<Window ...>
|
||||
<Window.DataContext>
|
||||
<!-- ViewModel 会通过 DI 自动注入 -->
|
||||
</Window.DataContext>
|
||||
|
||||
<StackPanel Margin="20">
|
||||
<!-- 绑定属性 -->
|
||||
<TextBlock Text="{Binding Message}" FontSize="24"/>
|
||||
|
||||
<!-- 绑定命令 -->
|
||||
<Button Content="点击我" Command="{Binding IncrementCommand}"/>
|
||||
|
||||
<!-- 异步命令 -->
|
||||
<Button Content="显示对话框" Command="{Binding ShowDialogCommand}"/>
|
||||
|
||||
<!-- 显示加载状态 -->
|
||||
<TextBlock Text="加载中..." Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 使用 `{Binding}` 绑定 ViewModel 属性
|
||||
- Command 绑定到 ICommand 属性
|
||||
- 使用内置转换器(BoolToVisibilityConverter)
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加新属性
|
||||
在 MainViewModel 中添加一个 `Name` 属性,并在界面上显示。
|
||||
|
||||
### 练习2:添加清零按钮
|
||||
添加一个 `ResetCommand`,点击后将 `Counter` 重置为 0。
|
||||
|
||||
### 练习3:使用对话框确认
|
||||
在 ResetCommand 中,先弹出确认对话框,用户确认后再清零。
|
||||
|
||||
### 练习4:尝试 Result<T>
|
||||
修改 ShowDialogCommand,使用 `Result.Try()` 包装可能出错的代码。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [Sharp.Wpf README](../../src/Sharp/Wpf/README.md) - WPF 模块完整文档
|
||||
- [QUICKSTART.md](../../src/Sharp/QUICKSTART.md) - 快速启动指南
|
||||
- 下一个示例:[02-WpfWithDatabase](../02-WpfWithDatabase/README.md)
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 为什么 MainWindow 不需要手动设置 DataContext?**
|
||||
A: 通过 DI 容器获取 Window 时,会自动注入对应的 ViewModel。
|
||||
|
||||
**Q: IsBusy 是在哪里定义的?**
|
||||
A: `IsBusy` 是 `ViewModelBase` 的内置属性,用于显示加载状态。
|
||||
|
||||
**Q: 可以不用 DI 吗?**
|
||||
A: 可以,但不推荐。使用 DI 可以更好地管理依赖关系,便于测试。
|
||||
|
||||
---
|
||||
|
||||
**开始编码吧!** 🚀
|
||||
@@ -0,0 +1,233 @@
|
||||
# 03 - 表单验证示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的表单验证功能,包括 DataAnnotations 验证、实时错误提示和自定义验证。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `ValidatableViewModelBase` 实现表单验证
|
||||
- 掌握 DataAnnotations 验证特性
|
||||
- 实现实时验证反馈
|
||||
- 自定义验证规则
|
||||
- 显示验证错误消息
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - ValidatableViewModelBase, 验证支持
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/03-FormValidation
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. UserViewModel.cs - 带验证的 ViewModel
|
||||
|
||||
```csharp
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Wpf.Validation;
|
||||
|
||||
public class UserViewModel : ValidatableViewModelBase
|
||||
{
|
||||
private string _username = "";
|
||||
private string _email = "";
|
||||
private string _password = "";
|
||||
private string _confirmPassword = "";
|
||||
private int _age;
|
||||
|
||||
[Required(ErrorMessage = "用户名不能为空")]
|
||||
[StringLength(20, MinimumLength = 3, ErrorMessage = "用户名长度必须在3-20个字符之间")]
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => SetProperty(ref _username, value, validate: true); // 启用验证
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "邮箱不能为空")]
|
||||
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
|
||||
public string Email
|
||||
{
|
||||
get => _email;
|
||||
set => SetProperty(ref _email, value, validate: true);
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "密码不能为空")]
|
||||
[StringLength(50, MinimumLength = 6, ErrorMessage = "密码长度必须在6-50个字符之间")]
|
||||
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
|
||||
ErrorMessage = "密码必须包含大小写字母和数字")]
|
||||
public string Password
|
||||
{
|
||||
get => _password;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _password, value, validate: true);
|
||||
// 密码变化时,重新验证确认密码
|
||||
ValidateProperty(ConfirmPassword, nameof(ConfirmPassword));
|
||||
}
|
||||
}
|
||||
|
||||
[Required(ErrorMessage = "请确认密码")]
|
||||
public string ConfirmPassword
|
||||
{
|
||||
get => _confirmPassword;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _confirmPassword, value, validate: false);
|
||||
// 自定义验证:确认密码必须与密码一致
|
||||
if (value != Password)
|
||||
{
|
||||
AddError(nameof(ConfirmPassword), "两次密码输入不一致");
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearErrors(nameof(ConfirmPassword));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Range(18, 120, ErrorMessage = "年龄必须在18-120之间")]
|
||||
public int Age
|
||||
{
|
||||
get => _age;
|
||||
set => SetProperty(ref _age, value, validate: true);
|
||||
}
|
||||
|
||||
// 提交命令
|
||||
public ICommand SubmitCommand { get; }
|
||||
|
||||
public UserViewModel(IDialogService dialogService)
|
||||
{
|
||||
SubmitCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 验证所有属性
|
||||
if (ValidateAll())
|
||||
{
|
||||
dialogService.ShowMessage(
|
||||
$"注册成功!\n用户名: {Username}\n邮箱: {Email}\n年龄: {Age}",
|
||||
"成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
dialogService.ShowMessage("请修正表单错误后再提交", "验证失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. MainWindow.xaml - 验证错误显示
|
||||
|
||||
```xml
|
||||
<Window x:Class="FormValidationExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="表单验证示例" Height="500" Width="400">
|
||||
<Window.Resources>
|
||||
<!-- 错误模板 -->
|
||||
<ControlTemplate x:Key="ValidationErrorTemplate">
|
||||
<DockPanel>
|
||||
<Border BorderBrush="Red" BorderThickness="1" CornerRadius="2">
|
||||
<AdornedElementPlaceholder/>
|
||||
</Border>
|
||||
<TextBlock Foreground="Red" FontSize="12" Margin="5,0,0,0"
|
||||
Text="{Binding [0].ErrorContent}"
|
||||
DockPanel.Dock="Right"/>
|
||||
</DockPanel>
|
||||
</ControlTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<StackPanel Margin="20">
|
||||
<TextBlock Text="用户注册表单" FontSize="24" FontWeight="Bold" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<TextBlock Text="用户名:"/>
|
||||
<TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<TextBlock Text="邮箱:"/>
|
||||
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 密码 -->
|
||||
<TextBlock Text="密码:"/>
|
||||
<PasswordBox x:Name="PasswordBox" Margin="0,5,0,5"/>
|
||||
<TextBlock Text="* 密码必须包含大小写字母和数字" FontSize="10" Foreground="Gray" Margin="0,0,0,15"/>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<TextBlock Text="确认密码:"/>
|
||||
<PasswordBox x:Name="ConfirmPasswordBox" Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 年龄 -->
|
||||
<TextBlock Text="年龄:"/>
|
||||
<TextBox Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}"
|
||||
Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}"
|
||||
Margin="0,5,0,20"/>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<Button Content="提交注册" Command="{Binding SubmitCommand}"
|
||||
Height="40" FontSize="16"/>
|
||||
|
||||
<!-- 验证状态 -->
|
||||
<TextBlock Margin="0,10,0,0" Foreground="Red"
|
||||
Text="表单包含错误,请修正后提交"
|
||||
Visibility="{Binding HasErrors, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 3. 关键点说明
|
||||
|
||||
**ValidatableViewModelBase 的关键特性**:
|
||||
1. **实时验证** - `SetProperty(ref field, value, validate: true)`
|
||||
2. **验证所有属性** - `ValidateAll()` 返回 bool
|
||||
3. **自定义错误** - `AddError(propertyName, errorMessage)`
|
||||
4. **清除错误** - `ClearErrors(propertyName)`
|
||||
5. **错误状态** - `HasErrors` 属性
|
||||
|
||||
**XAML 绑定要点**:
|
||||
1. **UpdateSourceTrigger=PropertyChanged** - 实时触发验证
|
||||
2. **ValidatesOnNotifyDataErrors=True** - 启用错误显示
|
||||
3. **Validation.ErrorTemplate** - 自定义错误样式
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多验证规则
|
||||
添加电话号码字段,使用正则表达式验证手机号格式。
|
||||
|
||||
### 练习2:异步验证
|
||||
实现用户名唯一性检查(模拟调用 API)。
|
||||
|
||||
### 练习3:条件验证
|
||||
当用户勾选"我是学生"时,年龄必须小于25岁。
|
||||
|
||||
### 练习4:自定义验证特性
|
||||
创建 `UniqueUsernameAttribute` 自定义验证特性。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: PasswordBox 无法绑定,怎么办?**
|
||||
A: PasswordBox 的 Password 属性不是依赖属性,不能直接绑定。可以使用附加属性或在代码隐藏中处理。
|
||||
|
||||
**Q: 为什么有时验证不触发?**
|
||||
A: 确保:
|
||||
1. 绑定使用了 `UpdateSourceTrigger=PropertyChanged`
|
||||
2. SetProperty 的 validate 参数设为 true
|
||||
3. XAML 中设置了 `ValidatesOnNotifyDataErrors=True`
|
||||
|
||||
**Q: 如何自定义错误消息样式?**
|
||||
A: 修改 `Validation.ErrorTemplate`,可以自定义边框颜色、图标等。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [DataAnnotations 验证特性](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations)
|
||||
- [INotifyDataErrorInfo 接口](https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.inotifydataerrorinfo)
|
||||
- 下一个示例:[04-Navigation](../04-Navigation/README.md)
|
||||
|
||||
---
|
||||
|
||||
**掌握表单验证,构建高质量的用户输入体验!** ✅
|
||||
@@ -0,0 +1,315 @@
|
||||
# 04 - 导航系统示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的导航服务实现多页面 WPF 应用,包括页面跳转、参数传递、前进/后退等功能。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `INavigationService` 实现页面导航
|
||||
- 理解导航历史栈(前进/后退)
|
||||
- 页面间参数传递
|
||||
- 导航生命周期钩子(INavigationAware)
|
||||
- 弹出式导航并获取结果
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - INavigationService, NavigationService
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/04-Navigation
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. MainViewModel.cs - 主导航容器
|
||||
|
||||
```csharp
|
||||
using Wpf.Navigation;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly INavigationService _navigationService;
|
||||
private ViewModelBase? _currentPage;
|
||||
|
||||
public ViewModelBase? CurrentPage
|
||||
{
|
||||
get => _currentPage;
|
||||
set => SetProperty(ref _currentPage, value);
|
||||
}
|
||||
|
||||
public bool CanGoBack => _navigationService.CanGoBack;
|
||||
public bool CanGoForward => _navigationService.CanGoForward;
|
||||
|
||||
public ICommand NavigateToHomeCommand { get; }
|
||||
public ICommand NavigateToProductsCommand { get; }
|
||||
public ICommand NavigateToSettingsCommand { get; }
|
||||
public ICommand GoBackCommand { get; }
|
||||
public ICommand GoForwardCommand { get; }
|
||||
|
||||
public MainViewModel(INavigationService navigationService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
|
||||
// 监听导航事件
|
||||
_navigationService.Navigated += (s, vm) =>
|
||||
{
|
||||
CurrentPage = vm;
|
||||
OnPropertyChanged(nameof(CanGoBack));
|
||||
OnPropertyChanged(nameof(CanGoForward));
|
||||
};
|
||||
|
||||
// 导航命令
|
||||
NavigateToHomeCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<HomeViewModel>();
|
||||
});
|
||||
|
||||
NavigateToProductsCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<ProductsViewModel>();
|
||||
});
|
||||
|
||||
NavigateToSettingsCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.NavigateAsync<SettingsViewModel>();
|
||||
});
|
||||
|
||||
GoBackCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.GoBackAsync();
|
||||
}, () => CanGoBack);
|
||||
|
||||
GoForwardCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _navigationService.GoForwardAsync();
|
||||
}, () => CanGoForward);
|
||||
|
||||
// 初始导航到首页
|
||||
Task.Run(async () => await _navigationService.NavigateAsync<HomeViewModel>());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ProductsViewModel.cs - 带参数的页面
|
||||
|
||||
```csharp
|
||||
public class ProductsViewModel : ViewModelBase, INavigationAware
|
||||
{
|
||||
private readonly INavigationService _navigationService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private string _category = "全部";
|
||||
|
||||
public string Category
|
||||
{
|
||||
get => _category;
|
||||
set => SetProperty(ref _category, value);
|
||||
}
|
||||
|
||||
public ICommand ViewProductDetailsCommand { get; }
|
||||
|
||||
public ProductsViewModel(
|
||||
INavigationService navigationService,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_navigationService = navigationService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
ViewProductDetailsCommand = new AsyncCommand<int>(async (productId, ct) =>
|
||||
{
|
||||
// 导航到产品详情页,传递产品ID
|
||||
await _navigationService.NavigateAsync<ProductDetailViewModel>(productId);
|
||||
});
|
||||
}
|
||||
|
||||
// 导航生命周期:进入页面时
|
||||
public async Task OnNavigatedToAsync(object? parameter)
|
||||
{
|
||||
if (parameter is string category)
|
||||
{
|
||||
Category = category;
|
||||
_dialogService.ShowMessage($"显示分类: {category}", "提示");
|
||||
}
|
||||
|
||||
// 可以在这里加载数据
|
||||
await LoadProductsAsync();
|
||||
}
|
||||
|
||||
// 导航生命周期:离开页面时
|
||||
public async Task OnNavigatedFromAsync()
|
||||
{
|
||||
// 清理资源、保存状态等
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadProductsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
await Task.Delay(500); // 模拟加载
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ProductDetailViewModel.cs - 详情页(带结果返回)
|
||||
|
||||
```csharp
|
||||
public class ProductDetailViewModel : ViewModelBase,
|
||||
INavigationAware,
|
||||
INavigationResult<bool> // 返回 bool(是否购买)
|
||||
{
|
||||
private readonly TaskCompletionSource<bool> _resultSource = new();
|
||||
private int _productId;
|
||||
private string _productName = "";
|
||||
|
||||
public int ProductId
|
||||
{
|
||||
get => _productId;
|
||||
set => SetProperty(ref _productId, value);
|
||||
}
|
||||
|
||||
public string ProductName
|
||||
{
|
||||
get => _productName;
|
||||
set => SetProperty(ref _productName, value);
|
||||
}
|
||||
|
||||
public ICommand BuyCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
|
||||
public ProductDetailViewModel()
|
||||
{
|
||||
BuyCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 返回结果:已购买
|
||||
_resultSource.SetResult(true);
|
||||
});
|
||||
|
||||
CancelCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 返回结果:取消
|
||||
_resultSource.SetResult(false);
|
||||
});
|
||||
}
|
||||
|
||||
public async Task OnNavigatedToAsync(object? parameter)
|
||||
{
|
||||
if (parameter is int productId)
|
||||
{
|
||||
ProductId = productId;
|
||||
// 加载产品详情
|
||||
ProductName = $"产品 #{productId}";
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task OnNavigatedFromAsync() => Task.CompletedTask;
|
||||
|
||||
// 实现 INavigationResult<bool>
|
||||
public Task<bool> GetResultAsync()
|
||||
{
|
||||
return _resultSource.Task;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. App.xaml.cs - 注册导航服务
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf(); // 自动注册 INavigationService
|
||||
|
||||
// 注册所有页面 ViewModel
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddViewModel<HomeViewModel>();
|
||||
services.AddViewModel<ProductsViewModel>();
|
||||
services.AddViewModel<ProductDetailViewModel>();
|
||||
services.AddViewModel<SettingsViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. MainWindow.xaml - 导航容器
|
||||
|
||||
```xml
|
||||
<Window x:Class="NavigationExample.MainWindow"
|
||||
Title="导航系统示例" Height="600" Width="800">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 导航栏 -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Background="#f0f0f0" Padding="10">
|
||||
<Button Content="首页" Command="{Binding NavigateToHomeCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="产品列表" Command="{Binding NavigateToProductsCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="设置" Command="{Binding NavigateToSettingsCommand}" Margin="0,0,20,0"/>
|
||||
|
||||
<Button Content="← 后退" Command="{Binding GoBackCommand}" Margin="0,0,10,0"/>
|
||||
<Button Content="前进 →" Command="{Binding GoForwardCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 内容区域 - 显示当前页面 -->
|
||||
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}">
|
||||
<ContentControl.Resources>
|
||||
<!-- 为每个 ViewModel 指定对应的 View -->
|
||||
<DataTemplate DataType="{x:Type local:HomeViewModel}">
|
||||
<local:HomeView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:ProductsViewModel}">
|
||||
<local:ProductsView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type local:SettingsViewModel}">
|
||||
<local:SettingsView/>
|
||||
</DataTemplate>
|
||||
</ContentControl.Resources>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:面包屑导航
|
||||
添加面包屑显示当前导航路径(如:首页 > 产品列表 > 产品详情)。
|
||||
|
||||
### 练习2:导航守卫
|
||||
实现 `INavigationGuard` 接口,在离开页面前检查是否有未保存的更改。
|
||||
|
||||
### 练习3:嵌套导航
|
||||
在产品列表页内部实现子导航(如:全部/热销/新品标签页切换)。
|
||||
|
||||
### 练习4:导航缓存
|
||||
实现页面缓存,避免重复创建 ViewModel(保持页面状态)。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: 导航后如何保持页面状态?**
|
||||
A: 不要后退时,CurrentViewModel 会保留在前进栈中,前进时会恢复状态。如果需要完全保持状态,可以使用 Singleton ViewModel。
|
||||
|
||||
**Q: 如何传递复杂对象作为参数?**
|
||||
A: 使用 `NavigateAsync(object parameter)`,parameter 可以是任何对象。
|
||||
|
||||
**Q: 导航历史栈何时清空?**
|
||||
A: 调用 `_navigationService.ClearHistory()` 手动清空。
|
||||
|
||||
**Q: 能否使用动画过渡?**
|
||||
A: 可以,在 ContentControl 上添加 Storyboard 实现页面切换动画。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [WPF 数据模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/data/data-templating-overview)
|
||||
- [MVVM 导航模式](https://prismlibrary.com/docs/navigation.html)
|
||||
- 下一个示例:[05-Localization](../05-Localization/README.md)
|
||||
|
||||
---
|
||||
|
||||
**掌握导航系统,构建流畅的多页面应用!** 🧭
|
||||
@@ -0,0 +1,278 @@
|
||||
# 05 - 国际化(i18n)示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的国际化功能实现多语言支持,包括运行时切换语言、资源文件管理等。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `ILocalizationService` 实现多语言
|
||||
- 在 XAML 中使用 `{loc:Localize}` 绑定本地化字符串
|
||||
- 运行时动态切换语言
|
||||
- 管理资源文件(.resx)
|
||||
- 格式化带参数的本地化字符串
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - ILocalizationService, LocalizeExtension
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/05-Localization
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. 创建资源文件
|
||||
|
||||
```
|
||||
Resources/
|
||||
├── Strings.resx # 默认语言(中文)
|
||||
├── Strings.en-US.resx # 英文
|
||||
└── Strings.ja-JP.resx # 日文(可选)
|
||||
```
|
||||
|
||||
**Strings.resx** (中文):
|
||||
```xml
|
||||
<data name="Welcome" xml:space="preserve">
|
||||
<value>欢迎使用 Sharp 框架</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>用户名</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>密码</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>登录</value>
|
||||
</data>
|
||||
<data name="WelcomeUser" xml:space="preserve">
|
||||
<value>欢迎, {0}!</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
**Strings.en-US.resx** (英文):
|
||||
```xml
|
||||
<data name="Welcome" xml:space="preserve">
|
||||
<value>Welcome to Sharp Framework</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>Username</value>
|
||||
</data>
|
||||
<data name="Password" xml:space="preserve">
|
||||
<value>Password</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="WelcomeUser" xml:space="preserve">
|
||||
<value>Welcome, {0}!</value>
|
||||
</data>
|
||||
```
|
||||
|
||||
### 2. App.xaml.cs - 注册国际化服务
|
||||
|
||||
```csharp
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using Wpf.Localization;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf();
|
||||
|
||||
// 添加国际化支持
|
||||
var resourceManager = new ResourceManager(
|
||||
"LocalizationExample.Resources.Strings", // 资源文件的完整命名空间
|
||||
Assembly.GetExecutingAssembly());
|
||||
|
||||
services.AddLocalization(
|
||||
resourceManager,
|
||||
new CultureInfo("zh-CN"), // 中文
|
||||
new CultureInfo("en-US"), // 英文
|
||||
new CultureInfo("ja-JP") // 日文
|
||||
);
|
||||
|
||||
services.AddViewModel<MainViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. MainViewModel.cs - 语言切换
|
||||
|
||||
```csharp
|
||||
using Wpf.Localization;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ILocalizationService _localization;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
public ILocalizationService Localization => _localization;
|
||||
|
||||
public List<CultureInfo> AvailableLanguages { get; }
|
||||
|
||||
public CultureInfo CurrentLanguage
|
||||
{
|
||||
get => _localization.CurrentCulture;
|
||||
set
|
||||
{
|
||||
_localization.ChangeCulture(value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand ChangeLanguageCommand { get; }
|
||||
public ICommand ShowGreetingCommand { get; }
|
||||
|
||||
public MainViewModel(
|
||||
ILocalizationService localization,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_localization = localization;
|
||||
_dialogService = dialogService;
|
||||
|
||||
AvailableLanguages = localization.GetSupportedCultures().ToList();
|
||||
|
||||
// 监听语言变化
|
||||
_localization.CultureChanged += (s, culture) =>
|
||||
{
|
||||
_dialogService.ShowMessage(
|
||||
$"语言已切换到: {culture.NativeName}",
|
||||
"提示");
|
||||
};
|
||||
|
||||
ChangeLanguageCommand = new RelayCommand<CultureInfo>(culture =>
|
||||
{
|
||||
if (culture != null)
|
||||
{
|
||||
CurrentLanguage = culture;
|
||||
}
|
||||
});
|
||||
|
||||
ShowGreetingCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 使用格式化字符串
|
||||
var message = _localization.GetString("WelcomeUser", "张三");
|
||||
_dialogService.ShowMessage(message, "问候");
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. MainWindow.xaml - 使用本地化绑定
|
||||
|
||||
```xml
|
||||
<Window x:Class="LocalizationExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:Wpf.Localization;assembly=Sharp.Wpf"
|
||||
Title="国际化示例" Height="400" Width="500">
|
||||
<StackPanel Margin="20">
|
||||
<!-- 方式1: 使用 LocalizeExtension(推荐) -->
|
||||
<TextBlock Text="{loc:Localize Welcome}" FontSize="24" FontWeight="Bold"/>
|
||||
|
||||
<!-- 语言选择 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,20,0,0">
|
||||
<TextBlock Text="选择语言:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<ComboBox ItemsSource="{Binding AvailableLanguages}"
|
||||
SelectedItem="{Binding CurrentLanguage}"
|
||||
DisplayMemberPath="NativeName"
|
||||
Width="150"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<StackPanel Margin="0,30,0,0">
|
||||
<!-- 用户名 -->
|
||||
<TextBlock Text="{loc:Localize Username}"/>
|
||||
<TextBox Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 密码 -->
|
||||
<TextBlock Text="{loc:Localize Password}"/>
|
||||
<PasswordBox Margin="0,5,0,15"/>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<Button Content="{loc:Localize Login}"
|
||||
Height="35" FontSize="14"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 方式2: 直接绑定 Localization 服务 -->
|
||||
<StackPanel Margin="0,30,0,0">
|
||||
<TextBlock Text="{Binding Localization[Welcome]}" FontStyle="Italic"/>
|
||||
<TextBlock Text="{Binding Localization[Username]}" Margin="0,5,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 测试格式化字符串 -->
|
||||
<Button Content="显示问候" Command="{Binding ShowGreetingCommand}" Margin="0,20,0,0"/>
|
||||
</StackPanel>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 5. 在代码中使用本地化
|
||||
|
||||
```csharp
|
||||
// 方式1: 通过服务获取
|
||||
var welcomeText = _localization.GetString("Welcome");
|
||||
|
||||
// 方式2: 带格式化参数
|
||||
var greeting = _localization.GetString("WelcomeUser", username);
|
||||
|
||||
// 方式3: 索引器(用于绑定)
|
||||
var password = _localization["Password"];
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多语言
|
||||
添加法语(fr-FR)或德语(de-DE)资源文件。
|
||||
|
||||
### 练习2:保存语言偏好
|
||||
将用户选择的语言保存到配置文件,下次启动时自动加载。
|
||||
|
||||
### 练习3:动态加载资源
|
||||
实现从外部 JSON 文件加载翻译资源(而非编译进程序)。
|
||||
|
||||
### 练习4:翻译进度显示
|
||||
显示每种语言的翻译完成度(如:英文 95%,日文 60%)。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: 资源文件修改后不生效?**
|
||||
A: 确保:
|
||||
1. 资源文件的"生成操作"设为"嵌入的资源"
|
||||
2. 重新生成项目(Clean + Rebuild)
|
||||
|
||||
**Q: 如何支持从右到左(RTL)语言?**
|
||||
A: 监听 CultureChanged 事件,设置 `FlowDirection`:
|
||||
```csharp
|
||||
_localization.CultureChanged += (s, culture) => {
|
||||
if (culture.TextInfo.IsRightToLeft) {
|
||||
Application.Current.MainWindow.FlowDirection = FlowDirection.RightToLeft;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Q: 能否热重载资源文件?**
|
||||
A: Sharp.Wpf 当前使用编译时嵌入资源,不支持运行时重载。可以使用外部 JSON 文件实现热重载。
|
||||
|
||||
**Q: 如何处理复数形式?**
|
||||
A: 使用不同的资源键或使用占位符:
|
||||
```
|
||||
"{Count} items" → "{0} 个项目"
|
||||
```
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [.NET 资源文件](https://learn.microsoft.com/zh-cn/dotnet/core/extensions/resources)
|
||||
- [CultureInfo 类](https://learn.microsoft.com/zh-cn/dotnet/api/system.globalization.cultureinfo)
|
||||
- [WPF 本地化](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/globalization-for-wpf)
|
||||
- 下一个示例:[06-Theming](../06-Theming/README.md)
|
||||
|
||||
---
|
||||
|
||||
**构建面向全球用户的应用!** 🌍
|
||||
@@ -0,0 +1,312 @@
|
||||
# 06 - 主题系统示例
|
||||
|
||||
演示如何使用 Sharp.Wpf 的主题系统实现浅色/深色主题切换,包括自定义颜色、跟随系统主题等功能。
|
||||
|
||||
## 🎯 学习目标
|
||||
|
||||
- 使用 `IThemeService` 管理应用主题
|
||||
- 实现浅色/深色主题切换
|
||||
- 自定义主题颜色
|
||||
- 跟随系统主题设置
|
||||
- 动态主题切换(无需重启)
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- **Sharp.Wpf** - IThemeService, ThemeService
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/06-Theming
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 注册主题服务
|
||||
|
||||
```csharp
|
||||
using Wpf.Theming;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
services.AddSharpWpf(); // 自动注册 IThemeService
|
||||
|
||||
services.AddViewModel<MainViewModel>();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// 应用初始主题
|
||||
var themeService = provider.GetRequiredService<IThemeService>();
|
||||
themeService.ApplyTheme(Theme.Light); // 或 Theme.System
|
||||
|
||||
var mainWindow = provider.GetRequiredService<MainWindow>();
|
||||
mainWindow.Show();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. MainViewModel.cs - 主题切换逻辑
|
||||
|
||||
```csharp
|
||||
using Wpf.Theming;
|
||||
|
||||
public class MainViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private Theme _currentTheme;
|
||||
|
||||
public Theme CurrentTheme
|
||||
{
|
||||
get => _currentTheme;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _currentTheme, value);
|
||||
_themeService.ApplyTheme(value);
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand SwitchToLightCommand { get; }
|
||||
public ICommand SwitchToDarkCommand { get; }
|
||||
public ICommand SwitchToSystemCommand { get; }
|
||||
|
||||
public MainViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_currentTheme = themeService.CurrentTheme;
|
||||
|
||||
// 监听主题变化
|
||||
_themeService.ThemeChanged += (s, theme) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(CurrentTheme));
|
||||
};
|
||||
|
||||
SwitchToLightCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.Light;
|
||||
});
|
||||
|
||||
SwitchToDarkCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.Dark;
|
||||
});
|
||||
|
||||
SwitchToSystemCommand = new RelayCommand(() =>
|
||||
{
|
||||
CurrentTheme = Theme.System;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. MainWindow.xaml - 使用主题颜色
|
||||
|
||||
```xml
|
||||
<Window x:Class="ThemingExample.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="主题系统示例" Height="500" Width="600"
|
||||
Background="{DynamicResource BackgroundColor}"> <!-- 使用主题颜色 -->
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 主题切换栏 -->
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal"
|
||||
Background="{DynamicResource PrimaryColor}" Padding="15">
|
||||
<TextBlock Text="选择主题:"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<Button Content="☀️ 浅色" Command="{Binding SwitchToLightCommand}"
|
||||
Margin="0,0,10,0" Padding="15,5"/>
|
||||
<Button Content="🌙 深色" Command="{Binding SwitchToDarkCommand}"
|
||||
Margin="0,0,10,0" Padding="15,5"/>
|
||||
<Button Content="🖥️ 跟随系统" Command="{Binding SwitchToSystemCommand}"
|
||||
Padding="15,5"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<ScrollViewer Grid.Row="1" Padding="20">
|
||||
<StackPanel>
|
||||
<TextBlock Text="主题颜色预览" FontSize="24" FontWeight="Bold"
|
||||
Foreground="{DynamicResource ForegroundColor}" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 基础颜色 -->
|
||||
<GroupBox Header="基础颜色" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<Border Background="{DynamicResource PrimaryColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="PrimaryColor" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource BackgroundColor}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="BackgroundColor"
|
||||
Foreground="{DynamicResource ForegroundColor}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 状态颜色 -->
|
||||
<GroupBox Header="状态颜色" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<Border Background="{DynamicResource SuccessColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="✓ Success" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource WarningColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="⚠ Warning" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource ErrorColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="✗ Error" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource InfoColor}"
|
||||
Height="40" Margin="0,5">
|
||||
<TextBlock Text="ℹ Info" Foreground="White"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 控件预览 -->
|
||||
<GroupBox Header="控件预览" Margin="0,0,0,15">
|
||||
<StackPanel>
|
||||
<!-- 按钮 -->
|
||||
<Button Content="普通按钮" Margin="0,5" Padding="10,5"
|
||||
Background="{DynamicResource ButtonBackgroundColor}"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<TextBox Text="输入框示例" Margin="0,5" Padding="5"
|
||||
Background="{DynamicResource InputBackgroundColor}"
|
||||
BorderBrush="{DynamicResource InputBorderColor}"/>
|
||||
|
||||
<!-- 复选框 -->
|
||||
<CheckBox Content="复选框示例" Margin="0,5"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
|
||||
<!-- 单选按钮 -->
|
||||
<RadioButton Content="单选按钮示例" Margin="0,5"
|
||||
Foreground="{DynamicResource ForegroundColor}"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 文本示例 -->
|
||||
<GroupBox Header="文本示例">
|
||||
<StackPanel>
|
||||
<TextBlock Text="这是标题文本" FontSize="20" FontWeight="Bold"
|
||||
Foreground="{DynamicResource ForegroundColor}" Margin="0,5"/>
|
||||
<TextBlock Text="这是正文文本。主题切换后,所有使用 DynamicResource 的颜色都会自动更新,无需重启应用。"
|
||||
TextWrapping="Wrap" Foreground="{DynamicResource ForegroundColor}"
|
||||
Margin="0,5"/>
|
||||
<TextBlock Text="这是次要文本" FontSize="12" FontStyle="Italic"
|
||||
Foreground="Gray" Margin="0,5"/>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
### 4. 自定义主题颜色
|
||||
|
||||
```csharp
|
||||
public class CustomThemeViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
|
||||
public ICommand ChangePrimaryColorCommand { get; }
|
||||
|
||||
public CustomThemeViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
|
||||
ChangePrimaryColorCommand = new RelayCommand(() =>
|
||||
{
|
||||
// 打开颜色选择器(可以使用第三方库或自定义)
|
||||
var color = Colors.DeepSkyBlue; // 示例:选择的颜色
|
||||
|
||||
// 更新主题颜色
|
||||
_themeService.SetColor("PrimaryColor", color);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 主题资源字典(可选 - 高级用法)
|
||||
|
||||
可以创建独立的资源字典文件:
|
||||
|
||||
**Themes/LightTheme.xaml**:
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="White"/>
|
||||
<SolidColorBrush x:Key="ForegroundColor" Color="Black"/>
|
||||
<!-- ... 其他颜色 -->
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
**Themes/DarkTheme.xaml**:
|
||||
```xml
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="PrimaryColor" Color="#0078D7"/>
|
||||
<SolidColorBrush x:Key="BackgroundColor" Color="#202020"/>
|
||||
<SolidColorBrush x:Key="ForegroundColor" Color="White"/>
|
||||
<!-- ... 其他颜色 -->
|
||||
</ResourceDictionary>
|
||||
```
|
||||
|
||||
## 🎓 练习建议
|
||||
|
||||
### 练习1:添加更多预设主题
|
||||
添加"高对比度"主题,适合视力不佳的用户。
|
||||
|
||||
### 练习2:主题动画过渡
|
||||
在切换主题时添加颜色渐变动画,而非瞬间切换。
|
||||
|
||||
### 练习3:保存主题偏好
|
||||
将用户选择的主题保存到配置文件,下次启动时自动应用。
|
||||
|
||||
### 练习4:自定义主题编辑器
|
||||
创建一个界面,允许用户自定义每种颜色,并导出/导入主题配置。
|
||||
|
||||
## 💡 常见问题
|
||||
|
||||
**Q: StaticResource 和 DynamicResource 的区别?**
|
||||
A:
|
||||
- `StaticResource`: 编译时绑定,主题切换后不会更新
|
||||
- `DynamicResource`: 运行时绑定,主题切换后自动更新(推荐用于主题)
|
||||
|
||||
**Q: 主题切换后部分控件没变化?**
|
||||
A: 确保使用了 `{DynamicResource}` 而非 `{StaticResource}`。
|
||||
|
||||
**Q: 如何跟随 Windows 系统主题?**
|
||||
A: 使用 `Theme.System`,Sharp.Wpf 会自动检测系统主题设置。
|
||||
|
||||
**Q: 能否为不同的窗口设置不同主题?**
|
||||
A: 可以,但不推荐。可以为每个 Window 设置独立的 ResourceDictionary。
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- [WPF 资源](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/systems/xaml-resources-overview)
|
||||
- [WPF 样式和模板](https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/controls/styles-templates-overview)
|
||||
- [Windows 主题检测](https://learn.microsoft.com/zh-cn/windows/apps/desktop/modernize/apply-windows-themes)
|
||||
- 综合示例:[11-FullStackDemo](../11-FullStackDemo/README.md)
|
||||
|
||||
---
|
||||
|
||||
**打造美观现代的用户界面!** 🎨
|
||||
@@ -0,0 +1,569 @@
|
||||
# 11 - Full Stack Demo - 完整功能演示
|
||||
|
||||
这是一个综合示例,集成了 Sharp 框架的几乎所有功能,演示如何构建一个完整的工业监控系统。
|
||||
|
||||
## 🎯 功能列表
|
||||
|
||||
### 核心功能
|
||||
- ✅ 用户登录(数据库验证)
|
||||
- ✅ 多页面导航
|
||||
- ✅ 主题切换(浅色/深色)
|
||||
- ✅ 多语言支持(中英文)
|
||||
- ✅ 表单验证
|
||||
|
||||
### 数据管理
|
||||
- ✅ 产品 CRUD 操作
|
||||
- ✅ 数据分页显示
|
||||
- ✅ 缓存优化
|
||||
|
||||
### 工业IoT
|
||||
- ✅ **PLC 通信测试** - 可配置 IP 和参数进行测试
|
||||
- ✅ **串口通信测试** - 可选择串口和配置参数
|
||||
|
||||
### 其他
|
||||
- ✅ 配置热重载
|
||||
- ✅ 日志记录
|
||||
- ✅ HTTP 客户端调用
|
||||
|
||||
## 📦 使用的模块
|
||||
|
||||
- Sharp.Common
|
||||
- Sharp.Config
|
||||
- Sharp.Wpf
|
||||
- Sharp.Data
|
||||
- Sharp.Cache
|
||||
- Sharp.Log
|
||||
- Sharp.Plc.Siemens
|
||||
- Sharp.Ports
|
||||
- Sharp.NetClient
|
||||
|
||||
## 🚀 快速运行
|
||||
|
||||
```bash
|
||||
cd examples/11-FullStackDemo
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 📝 项目结构
|
||||
|
||||
```
|
||||
11-FullStackDemo/
|
||||
├── App.xaml.cs # 应用启动配置
|
||||
├── appsettings.json # 配置文件
|
||||
├── Models/
|
||||
│ ├── User.cs # 用户实体
|
||||
│ └── Product.cs # 产品实体
|
||||
├── ViewModels/
|
||||
│ ├── LoginViewModel.cs # 登录页
|
||||
│ ├── MainViewModel.cs # 主窗口
|
||||
│ ├── HomeViewModel.cs # 首页(仪表板)
|
||||
│ ├── ProductsViewModel.cs # 产品管理
|
||||
│ ├── PlcTestViewModel.cs # PLC 测试页 ⭐
|
||||
│ ├── PortsTestViewModel.cs# 串口测试页 ⭐
|
||||
│ └── SettingsViewModel.cs # 设置页
|
||||
├── Views/
|
||||
│ ├── LoginWindow.xaml
|
||||
│ ├── MainWindow.xaml
|
||||
│ ├── HomeView.xaml
|
||||
│ ├── ProductsView.xaml
|
||||
│ ├── PlcTestView.xaml # PLC 测试界面 ⭐
|
||||
│ ├── PortsTestView.xaml # 串口测试界面 ⭐
|
||||
│ └── SettingsView.xaml
|
||||
└── Data/
|
||||
└── AppDbContext.cs
|
||||
```
|
||||
|
||||
## 🔧 核心代码讲解
|
||||
|
||||
### 1. App.xaml.cs - 完整启动配置
|
||||
|
||||
```csharp
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 1. 配置管理
|
||||
var config = ConfigBuilder.CreateDefault().Configuration;
|
||||
services.AddSingleton(config);
|
||||
|
||||
// 2. 数据库
|
||||
services.AddSharpDatabase<AppDbContext>(config);
|
||||
|
||||
// 3. WPF 服务(包括导航、主题、国际化)
|
||||
services.AddSharpWpf();
|
||||
services.AddLocalization(Resources.Strings.ResourceManager,
|
||||
new CultureInfo("zh-CN"),
|
||||
new CultureInfo("en-US"));
|
||||
|
||||
// 4. 缓存
|
||||
services.AddSharpCache();
|
||||
|
||||
// 5. 日志
|
||||
services.AddLogging(builder => {
|
||||
builder.AddConsole();
|
||||
builder.AddDebug();
|
||||
});
|
||||
|
||||
// 6. PLC 服务(可选 - 如果配置中启用)
|
||||
var plcEnabled = config.GetValue<bool>("Plc:Enabled", false);
|
||||
if (plcEnabled)
|
||||
{
|
||||
services.AddSiemensPlc(config.GetSection("Plc:Siemens"));
|
||||
}
|
||||
|
||||
// 7. 串口服务
|
||||
services.AddSingleton<ISerialPortService, SerialPortService>();
|
||||
|
||||
// 8. HTTP 客户端
|
||||
services.AddHttpClient();
|
||||
|
||||
// 9. 注册 ViewModels
|
||||
services.AddViewModel<LoginViewModel>();
|
||||
services.AddViewModel<MainViewModel>();
|
||||
services.AddViewModel<HomeViewModel>();
|
||||
services.AddViewModel<ProductsViewModel>();
|
||||
services.AddViewModel<PlcTestViewModel>();
|
||||
services.AddViewModel<PortsTestViewModel>();
|
||||
services.AddViewModel<SettingsViewModel>();
|
||||
|
||||
// 10. 注册 Windows
|
||||
services.AddTransient<LoginWindow>();
|
||||
services.AddTransient<MainWindow>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// 11. 初始化数据库
|
||||
using (var scope = _serviceProvider.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
db.Database.EnsureCreated();
|
||||
SeedData(db); // 初始化测试数据
|
||||
}
|
||||
|
||||
// 12. 应用主题
|
||||
var themeService = _serviceProvider.GetRequiredService<IThemeService>();
|
||||
var savedTheme = config.GetValue<string>("App:Theme", "Light");
|
||||
themeService.ApplyTheme(Enum.Parse<Theme>(savedTheme));
|
||||
|
||||
// 13. 显示登录窗口
|
||||
ShowLoginWindow();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. PlcTestViewModel.cs - PLC 测试页 ⭐
|
||||
|
||||
```csharp
|
||||
public class PlcTestViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private IPlcService? _plcService;
|
||||
|
||||
// 连接参数
|
||||
private string _ipAddress = "192.168.1.100";
|
||||
private int _rack = 0;
|
||||
private int _slot = 1;
|
||||
private bool _isConnected;
|
||||
|
||||
public string IpAddress
|
||||
{
|
||||
get => _ipAddress;
|
||||
set => SetProperty(ref _ipAddress, value);
|
||||
}
|
||||
|
||||
public int Rack
|
||||
{
|
||||
get => _rack;
|
||||
set => SetProperty(ref _rack, value);
|
||||
}
|
||||
|
||||
public int Slot
|
||||
{
|
||||
get => _slot;
|
||||
set => SetProperty(ref _slot, value);
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
// 读写测试
|
||||
private string _address = "DB1.DBD0";
|
||||
private string _value = "0";
|
||||
private string _readResult = "";
|
||||
|
||||
public string Address
|
||||
{
|
||||
get => _address;
|
||||
set => SetProperty(ref _address, value);
|
||||
}
|
||||
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
public string ReadResult
|
||||
{
|
||||
get => _readResult;
|
||||
set => SetProperty(ref _readResult, value);
|
||||
}
|
||||
|
||||
public ICommand ConnectCommand { get; }
|
||||
public ICommand DisconnectCommand { get; }
|
||||
public ICommand ReadCommand { get; }
|
||||
public ICommand WriteCommand { get; }
|
||||
|
||||
public PlcTestViewModel(IDialogService dialogService)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
|
||||
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
// 动态创建 PLC 服务
|
||||
var options = new SiemensPlcOptions
|
||||
{
|
||||
IpAddress = IpAddress,
|
||||
Rack = Rack,
|
||||
Slot = Slot,
|
||||
CpuType = CpuType.S71500
|
||||
};
|
||||
|
||||
_plcService = new PlcMonitorService(
|
||||
new SiemensDriver(options),
|
||||
new SiemensOptimizer(),
|
||||
new BigEndianConverter(),
|
||||
Options.Create(new PlcMonitorOptions()),
|
||||
null);
|
||||
|
||||
await _plcService.StartAsync(ct);
|
||||
|
||||
IsConnected = true;
|
||||
_dialogService.ShowMessage("PLC 连接成功!", "成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"连接失败: {ex.Message}", "错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
|
||||
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService != null)
|
||||
{
|
||||
await _plcService.StopAsync();
|
||||
IsConnected = false;
|
||||
_dialogService.ShowMessage("已断开连接", "提示");
|
||||
}
|
||||
});
|
||||
|
||||
ReadCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService == null)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
var result = await _plcService.ReadAsync<int>(Address);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ReadResult = $"读取成功: {result.Value}";
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadResult = $"读取失败: {result.ErrorMessage}";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ReadResult = $"读取异常: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
|
||||
WriteCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (_plcService == null)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接 PLC", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IsBusy = true;
|
||||
var intValue = int.Parse(Value);
|
||||
var result = await _plcService.WriteAsync(Address, intValue);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_dialogService.ShowMessage("写入成功!", "成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"写入失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"写入异常: {ex.Message}", "错误");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. PortsTestViewModel.cs - 串口测试页 ⭐
|
||||
|
||||
```csharp
|
||||
public class PortsTestViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ISerialPortService _serialPortService;
|
||||
private readonly IDialogService _dialogService;
|
||||
|
||||
// 可用串口列表
|
||||
public ObservableCollection<string> AvailablePorts { get; } = new();
|
||||
|
||||
private string? _selectedPort;
|
||||
private int _baudRate = 9600;
|
||||
private bool _isConnected;
|
||||
|
||||
public string? SelectedPort
|
||||
{
|
||||
get => _selectedPort;
|
||||
set => SetProperty(ref _selectedPort, value);
|
||||
}
|
||||
|
||||
public int BaudRate
|
||||
{
|
||||
get => _baudRate;
|
||||
set => SetProperty(ref _baudRate, value);
|
||||
}
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
get => _isConnected;
|
||||
set => SetProperty(ref _isConnected, value);
|
||||
}
|
||||
|
||||
// 发送/接收
|
||||
private string _sendData = "";
|
||||
private string _receiveData = "";
|
||||
|
||||
public string SendData
|
||||
{
|
||||
get => _sendData;
|
||||
set => SetProperty(ref _sendData, value);
|
||||
}
|
||||
|
||||
public string ReceiveData
|
||||
{
|
||||
get => _receiveData;
|
||||
set => SetProperty(ref _receiveData, value);
|
||||
}
|
||||
|
||||
public ICommand RefreshPortsCommand { get; }
|
||||
public ICommand ConnectCommand { get; }
|
||||
public ICommand DisconnectCommand { get; }
|
||||
public ICommand SendCommand { get; }
|
||||
public ICommand ClearCommand { get; }
|
||||
|
||||
public PortsTestViewModel(
|
||||
ISerialPortService serialPortService,
|
||||
IDialogService dialogService)
|
||||
{
|
||||
_serialPortService = serialPortService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
// 监听数据接收
|
||||
_serialPortService.DataReceived += (s, e) =>
|
||||
{
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 接收: {BitConverter.ToString(e.Data)}\n";
|
||||
});
|
||||
};
|
||||
|
||||
RefreshPortsCommand = new RelayCommand(() =>
|
||||
{
|
||||
AvailablePorts.Clear();
|
||||
var ports = SerialPortDiscovery.GetAvailablePorts();
|
||||
foreach (var port in ports)
|
||||
{
|
||||
AvailablePorts.Add(port.PortName);
|
||||
}
|
||||
|
||||
if (AvailablePorts.Count > 0)
|
||||
{
|
||||
SelectedPort = AvailablePorts[0];
|
||||
}
|
||||
});
|
||||
|
||||
ConnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(SelectedPort))
|
||||
{
|
||||
_dialogService.ShowMessage("请选择串口", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var options = new SerialPortOptions
|
||||
{
|
||||
PortName = SelectedPort,
|
||||
BaudRate = BaudRate
|
||||
};
|
||||
|
||||
var result = await _serialPortService.StartListeningAsync(options, autoReconnect: false);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
IsConnected = true;
|
||||
_dialogService.ShowMessage("串口连接成功!", "成功");
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"连接失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"连接异常: {ex.Message}", "错误");
|
||||
}
|
||||
});
|
||||
|
||||
DisconnectCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
await _serialPortService.StopListeningAsync();
|
||||
IsConnected = false;
|
||||
_dialogService.ShowMessage("已断开连接", "提示");
|
||||
});
|
||||
|
||||
SendCommand = new AsyncCommand(async (ct) =>
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
_dialogService.ShowMessage("请先连接串口", "提示");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 将字符串转换为字节数组
|
||||
var data = Encoding.UTF8.GetBytes(SendData);
|
||||
var result = await _serialPortService.SendAsync(data);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
ReceiveData += $"[{DateTime.Now:HH:mm:ss}] 发送: {SendData}\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
_dialogService.ShowMessage($"发送失败: {result.ErrorMessage}", "错误");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_dialogService.ShowMessage($"发送异常: {ex.Message}", "错误");
|
||||
}
|
||||
});
|
||||
|
||||
ClearCommand = new RelayCommand(() =>
|
||||
{
|
||||
ReceiveData = "";
|
||||
});
|
||||
|
||||
// 初始化:刷新串口列表
|
||||
RefreshPortsCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. appsettings.json - 配置文件
|
||||
|
||||
```json
|
||||
{
|
||||
"App": {
|
||||
"Theme": "Light",
|
||||
"Language": "zh-CN"
|
||||
},
|
||||
"Database": {
|
||||
"Provider": "Sqlite",
|
||||
"ConnectionString": "Data Source=demo.db"
|
||||
},
|
||||
"Plc": {
|
||||
"Enabled": false,
|
||||
"Siemens": {
|
||||
"IpAddress": "192.168.1.100",
|
||||
"Port": 102,
|
||||
"CpuType": "S71500",
|
||||
"Rack": 0,
|
||||
"Slot": 1
|
||||
}
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎓 使用说明
|
||||
|
||||
### 测试 PLC 连接
|
||||
1. 进入"PLC 测试"页面
|
||||
2. 输入 PLC 的 IP 地址、Rack、Slot
|
||||
3. 点击"连接"按钮
|
||||
4. 连接成功后,输入地址(如 `DB1.DBD0`)
|
||||
5. 点击"读取"或"写入"测试通信
|
||||
|
||||
### 测试串口通信
|
||||
1. 进入"串口测试"页面
|
||||
2. 点击"刷新端口"查看可用串口
|
||||
3. 选择串口并设置波特率
|
||||
4. 点击"连接"
|
||||
5. 在发送框输入数据,点击"发送"
|
||||
6. 接收框会显示收到的数据
|
||||
|
||||
## 💡 提示
|
||||
|
||||
- PLC 和串口功能可以独立测试,无需实际设备(会显示连接失败,但不影响其他功能)
|
||||
- 数据库使用 SQLite,无需安装
|
||||
- 配置文件支持热重载,修改后自动生效
|
||||
- 所有功能都有错误处理,不会导致程序崩溃
|
||||
|
||||
## 📚 延伸阅读
|
||||
|
||||
- 所有示例项目的 README
|
||||
- Sharp 框架文档
|
||||
|
||||
---
|
||||
|
||||
**这是一个完整的工业监控系统模板,可以直接用于实际项目开发!** 🏭
|
||||
@@ -0,0 +1,273 @@
|
||||
# Sharp Framework Examples
|
||||
|
||||
本目录包含 Sharp 框架各个功能的示例项目,帮助你快速上手。
|
||||
|
||||
## 📚 示例项目列表
|
||||
|
||||
### 基础示例
|
||||
|
||||
#### 1. **01-MinimalWpfApp** - 最小 WPF 应用 ⭐
|
||||
**演示内容**:
|
||||
- 最简单的 Sharp WPF 应用
|
||||
- 仅使用 Common + Wpf 模块
|
||||
- MVVM 基础(ViewModelBase, Commands)
|
||||
- 对话框服务
|
||||
|
||||
**适用场景**:刚接触 Sharp 框架,想快速了解基本用法
|
||||
|
||||
**运行**:
|
||||
```bash
|
||||
cd examples/01-MinimalWpfApp
|
||||
dotnet run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. **02-WpfWithDatabase** - WPF + 数据库 ⭐⭐
|
||||
**演示内容**:
|
||||
- WPF + SQLite 数据库
|
||||
- Repository 模式(CRUD操作)
|
||||
- 数据绑定与列表展示
|
||||
- 添加/编辑/删除产品
|
||||
|
||||
**适用场景**:需要本地数据存储的桌面应用
|
||||
|
||||
**技术栈**:Common, Config, Wpf, Data (SQLite)
|
||||
|
||||
---
|
||||
|
||||
#### 3. **03-FormValidation** - 表单验证 ⭐⭐
|
||||
**演示内容**:
|
||||
- ValidatableViewModelBase 使用
|
||||
- DataAnnotations 验证
|
||||
- 实时验证反馈
|
||||
- 错误消息显示
|
||||
|
||||
**适用场景**:需要用户输入验证的应用
|
||||
|
||||
---
|
||||
|
||||
#### 4. **04-Navigation** - 导航系统 ⭐⭐
|
||||
**演示内容**:
|
||||
- INavigationService 使用
|
||||
- 多页面导航
|
||||
- 前进/后退功能
|
||||
- 页面间参数传递
|
||||
- 导航生命周期(INavigationAware)
|
||||
|
||||
**适用场景**:多页面 WPF 应用
|
||||
|
||||
---
|
||||
|
||||
#### 5. **05-Localization** - 国际化 ⭐⭐
|
||||
**演示内容**:
|
||||
- 多语言支持(中文、英文)
|
||||
- LocalizeExtension XAML 绑定
|
||||
- 运行时切换语言
|
||||
- 资源文件管理
|
||||
|
||||
**适用场景**:需要多语言支持的应用
|
||||
|
||||
---
|
||||
|
||||
#### 6. **06-Theming** - 主题系统 ⭐⭐
|
||||
**演示内容**:
|
||||
- 浅色/深色主题切换
|
||||
- 自定义主题颜色
|
||||
- 跟随系统主题
|
||||
- 动态主题切换
|
||||
|
||||
**适用场景**:需要主题切换的现代应用
|
||||
|
||||
---
|
||||
|
||||
### 工业IoT示例
|
||||
|
||||
#### 7. **07-PlcMonitoring** - PLC 监控 ⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 连接西门子 PLC
|
||||
- 实时数据监控
|
||||
- 数据变化订阅
|
||||
- 读写PLC标签
|
||||
- 数据可视化(图表)
|
||||
|
||||
**适用场景**:工业自动化、设备监控
|
||||
|
||||
**技术栈**:Wpf, Plc.Siemens, Cache(缓存历史数据)
|
||||
|
||||
---
|
||||
|
||||
#### 8. **08-SerialPortComm** - 串口通信 ⭐⭐
|
||||
**演示内容**:
|
||||
- 串口自动发现
|
||||
- 数据收发
|
||||
- HEX/ASCII 显示
|
||||
- 数据记录
|
||||
|
||||
**适用场景**:串口设备通信、调试工具
|
||||
|
||||
**技术栈**:Wpf, Ports
|
||||
|
||||
---
|
||||
|
||||
### 网络示例
|
||||
|
||||
#### 9. **09-HttpServer** - 嵌入式 Web 服务器 ⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 在 WPF 应用中嵌入 HTTP 服务器
|
||||
- RESTful API 实现
|
||||
- 中间件使用(CORS, 认证, 日志)
|
||||
- WPF 界面管理服务器
|
||||
|
||||
**适用场景**:设备管理后台、本地Web服务
|
||||
|
||||
**技术栈**:Wpf, NetServer, Log
|
||||
|
||||
---
|
||||
|
||||
#### 10. **10-HttpClient** - HTTP 客户端 ⭐⭐
|
||||
**演示内容**:
|
||||
- 调用外部 API
|
||||
- 重试策略(Polly)
|
||||
- Result<T> 错误处理
|
||||
- 缓存 API 响应
|
||||
|
||||
**适用场景**:需要调用外部服务的应用
|
||||
|
||||
**技术栈**:Wpf, NetClient, Cache
|
||||
|
||||
---
|
||||
|
||||
### 综合示例
|
||||
|
||||
#### 11. **11-FullStackDemo** - 完整功能演示 ⭐⭐⭐⭐
|
||||
**演示内容**:
|
||||
- 用户登录(数据库验证)
|
||||
- 产品管理(CRUD)
|
||||
- PLC 数据监控
|
||||
- 串口通信
|
||||
- 数据报表导出
|
||||
- 多语言
|
||||
- 主题切换
|
||||
- 配置热重载
|
||||
|
||||
**适用场景**:工业监控系统、MES系统、数采系统
|
||||
|
||||
**技术栈**:全部模块
|
||||
|
||||
---
|
||||
|
||||
#### 12. **12-CachingDemo** - 缓存使用 ⭐
|
||||
**演示内容**:
|
||||
- 字典数据缓存
|
||||
- API 响应缓存
|
||||
- 计算结果缓存
|
||||
- 缓存失效策略
|
||||
|
||||
**技术栈**:Wpf, Cache, NetClient
|
||||
|
||||
---
|
||||
|
||||
#### 13. **13-EventBusDemo** - 事件总线 ⭐
|
||||
**演示内容**:
|
||||
- 跨模块通信
|
||||
- 事件发布/订阅
|
||||
- 解耦架构
|
||||
|
||||
**技术栈**:Wpf, EventBus
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 运行示例
|
||||
|
||||
```bash
|
||||
# 1. 克隆仓库
|
||||
git clone https://github.com/your-repo/sharp-basic.git
|
||||
cd sharp-basic/examples
|
||||
|
||||
# 2. 选择一个示例
|
||||
cd 01-MinimalWpfApp
|
||||
|
||||
# 3. 还原依赖并运行
|
||||
dotnet restore
|
||||
dotnet run
|
||||
```
|
||||
|
||||
### 从示例创建新项目
|
||||
|
||||
```bash
|
||||
# 复制示例作为起点
|
||||
cp -r examples/02-WpfWithDatabase MyNewProject
|
||||
cd MyNewProject
|
||||
|
||||
# 修改项目名称
|
||||
# 编辑 .csproj 文件和命名空间
|
||||
# 开始开发!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 学习路径
|
||||
|
||||
### 新手推荐路径
|
||||
1. **01-MinimalWpfApp** - 了解基础
|
||||
2. **02-WpfWithDatabase** - 学习数据库操作
|
||||
3. **03-FormValidation** - 掌握表单验证
|
||||
4. **04-Navigation** - 理解导航系统
|
||||
|
||||
### 工业自动化开发者
|
||||
1. **07-PlcMonitoring** - PLC 通信
|
||||
2. **08-SerialPortComm** - 串口通信
|
||||
3. **11-FullStackDemo** - 完整系统
|
||||
|
||||
### Web 开发者转 WPF
|
||||
1. **09-HttpServer** - HTTP 服务器
|
||||
2. **10-HttpClient** - HTTP 客户端
|
||||
3. **06-Theming** - 主题系统
|
||||
|
||||
---
|
||||
|
||||
## 💡 示例代码说明
|
||||
|
||||
所有示例都遵循:
|
||||
- ✅ **最佳实践**:展示推荐的使用方式
|
||||
- ✅ **完整代码**:可直接运行,无需额外配置
|
||||
- ✅ **详细注释**:中文注释,解释关键代码
|
||||
- ✅ **README**:每个示例都有独立的 README
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 技术要求
|
||||
|
||||
- .NET 8.0 SDK
|
||||
- Visual Studio 2022 或 VS Code (推荐安装 C# Dev Kit)
|
||||
- Windows 10/11 (WPF 应用需要 Windows)
|
||||
|
||||
---
|
||||
|
||||
## 📝 贡献示例
|
||||
|
||||
欢迎贡献新的示例!请确保:
|
||||
1. 代码简洁明了
|
||||
2. 包含 README 说明
|
||||
3. 注释使用中文
|
||||
4. 可独立运行
|
||||
|
||||
---
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 示例可以直接用于生产吗?**
|
||||
A: 示例代码主要用于学习,生产使用需要根据实际情况调整(如安全性、错误处理、日志等)。
|
||||
|
||||
**Q: 示例使用什么数据库?**
|
||||
A: 大部分示例使用 SQLite(无需安装,开箱即用),也有 SQL Server 示例。
|
||||
|
||||
**Q: 可以在 .NET Framework 上运行吗?**
|
||||
A: 不可以,Sharp 框架基于 .NET 8,需要 .NET 8 运行时。
|
||||
|
||||
---
|
||||
|
||||
**选择一个示例开始你的 Sharp 之旅吧!** 🎉
|
||||
Reference in new issue
Block a user