U:调整调用方式和兼容性
This commit is contained in:
1 parent
433e206d45
commit
4ec76b7855
50 files changed
+5592
-47
No files matched your search
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 _);
|
||||
}
|
||||
}
|
||||
@@ -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内置)
|
||||
@@ -9,8 +9,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+de1a29c522c231e011e0b795cb1494b1dd7282bf")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+433e206d456d0ef3b2fc57c458ff3fc030643be6")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Client")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
b775c782e822e9da489917907ac5dc67ababf8e2b94747f6176fcf3d21ac9454
|
||||
de45fc2ddda59d691d6bfd3f2673675ae92a6f2f53cc261c9aeefe56c546545d
|
||||
@@ -6,4 +6,11 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Common.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 快速启动扩展,提供"零配置"的便捷注册方法
|
||||
/// </summary>
|
||||
public static class QuickStartExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp 框架的所有基础服务(一行搞定)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置(可选,不传则自动创建默认配置)</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpEssentials(
|
||||
this IServiceCollection services,
|
||||
IConfiguration? configuration = null)
|
||||
{
|
||||
// 1. 配置管理(如果没传就用默认)
|
||||
// Note: ConfigBuilder.CreateDefault() 在 Config 模块中
|
||||
// 这里假设用户已经传入 configuration 或在外部创建
|
||||
if (configuration != null)
|
||||
{
|
||||
services.AddSingleton(configuration);
|
||||
}
|
||||
|
||||
// 2. 缓存服务(使用 Cache 模块的扩展方法)
|
||||
// Note: AddSharpCache() 在 Cache.DependencyInjection 中定义
|
||||
services.AddMemoryCache();
|
||||
|
||||
// 3. 全局服务定位器(兼容性)
|
||||
if (configuration != null)
|
||||
{
|
||||
RegisterGlobalServices(services, configuration);
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册到 Global 容器(向后兼容)
|
||||
/// </summary>
|
||||
private static void RegisterGlobalServices(IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
// 构建临时 ServiceProvider
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// 注册到 Global
|
||||
Global.RegisterSingleton(configuration);
|
||||
Global.RegisterSingleton(provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Config;
|
||||
|
||||
public static class ConfigBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建默认配置(约定优于配置)
|
||||
/// 自动查找 appsettings.json,启用热重载和环境变量
|
||||
/// </summary>
|
||||
/// <param name="basePath">基础路径,默认为应用程序目录</param>
|
||||
/// <param name="environmentPrefix">环境变量前缀,默认为 "APP_"</param>
|
||||
/// <returns>配置管理器</returns>
|
||||
public static ConfigManager CreateDefault(
|
||||
string? basePath = null,
|
||||
string? environmentPrefix = "APP_")
|
||||
{
|
||||
basePath ??= AppContext.BaseDirectory;
|
||||
|
||||
// 查找配置文件(appsettings.json, appsettings.Development.json 等)
|
||||
var configFiles = new List<string> { "appsettings.json" };
|
||||
|
||||
// 根据环境添加特定配置
|
||||
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
|
||||
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
|
||||
?? "Production";
|
||||
|
||||
var envConfigFile = $"appsettings.{environment}.json";
|
||||
var envConfigPath = Path.Combine(basePath, envConfigFile);
|
||||
if (File.Exists(envConfigPath))
|
||||
{
|
||||
configFiles.Add(envConfigFile);
|
||||
}
|
||||
|
||||
// 构建配置
|
||||
return ConfigBuilder.BuildFromFiles(
|
||||
configFiles.ToArray(),
|
||||
reloadOnChange: true,
|
||||
includeEnvironment: true,
|
||||
environmentPrefix: environmentPrefix,
|
||||
basePath: basePath
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建最小化配置(只有 appsettings.json)
|
||||
/// </summary>
|
||||
public static ConfigManager CreateMinimal(string? basePath = null)
|
||||
{
|
||||
basePath ??= AppContext.BaseDirectory;
|
||||
|
||||
return ConfigBuilder.BuildFromFiles(
|
||||
new[] { "appsettings.json" },
|
||||
reloadOnChange: false,
|
||||
includeEnvironment: false,
|
||||
basePath: basePath
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,18 @@
|
||||
|
||||
<ItemGroup>
|
||||
<!-- EF Core -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
|
||||
|
||||
<!-- 数据库提供者 -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" />
|
||||
|
||||
<!-- 依赖注入 -->
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Sharp.Data.Repository;
|
||||
|
||||
namespace Data.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// 数据访问层便捷注册扩展
|
||||
/// </summary>
|
||||
public static class DataServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp 数据库支持(约定优于配置)
|
||||
/// 自动从配置读取数据库连接信息
|
||||
/// </summary>
|
||||
/// <typeparam name="TContext">DbContext 类型</typeparam>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="configuration">配置</param>
|
||||
/// <param name="configSection">配置节名称,默认为 "Database"</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration,
|
||||
string configSection = "Database")
|
||||
where TContext : DbContext
|
||||
{
|
||||
var provider = configuration[$"{configSection}:Provider"] ?? "SqlServer";
|
||||
var connectionString = configuration[$"{configSection}:ConnectionString"]
|
||||
?? throw new InvalidOperationException(
|
||||
$"未找到数据库连接字符串配置: {configSection}:ConnectionString");
|
||||
|
||||
return AddSharpDatabase<TContext>(services, provider, connectionString);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加 Sharp 数据库支持(显式指定提供程序和连接字符串)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddSharpDatabase<TContext>(
|
||||
this IServiceCollection services,
|
||||
string provider,
|
||||
string connectionString)
|
||||
where TContext : DbContext
|
||||
{
|
||||
services.AddDbContext<TContext>(options =>
|
||||
{
|
||||
switch (provider.ToLower())
|
||||
{
|
||||
case "sqlserver":
|
||||
case "mssql":
|
||||
options.UseSqlServer(connectionString, sqlOptions =>
|
||||
{
|
||||
sqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 3,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(5),
|
||||
errorNumbersToAdd: null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "sqlite":
|
||||
options.UseSqlite(connectionString);
|
||||
break;
|
||||
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "mysql":
|
||||
var serverVersion = ServerVersion.AutoDetect(connectionString);
|
||||
options.UseMySql(connectionString, serverVersion, mysqlOptions =>
|
||||
{
|
||||
mysqlOptions.EnableRetryOnFailure(3, TimeSpan.FromSeconds(5), null);
|
||||
});
|
||||
break;
|
||||
|
||||
case "inmemory":
|
||||
options.UseInMemoryDatabase(connectionString);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new NotSupportedException($"不支持的数据库提供程序: {provider}");
|
||||
}
|
||||
|
||||
// 开发环境启用详细错误
|
||||
if (Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Development")
|
||||
{
|
||||
options.EnableDetailedErrors();
|
||||
options.EnableSensitiveDataLogging();
|
||||
}
|
||||
});
|
||||
|
||||
// 注册仓储
|
||||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,456 @@
|
||||
|
||||
> **🚧 开发中 / Under Construction**
|
||||
|
||||
这是一个预留的事件总线模块,计划用于实现进程内(In-Process)或分布式(Distributed)的消息发布/订阅模式。
|
||||
轻量级进程内事件总线,用于解耦应用程序中的模块通信。
|
||||
|
||||
## 计划功能
|
||||
- 定义统一的 `IEventBus` 接口。
|
||||
- 提供基于内存(Channel/MemoryCache)的默认实现。
|
||||
- 提供集成 RabbitMQ / Redis 的扩展实现。
|
||||
## 📋 为什么需要 EventBus?
|
||||
|
||||
目前该目录仅包含占位符。
|
||||
### 场景对比
|
||||
|
||||
#### ❌ 不使用 EventBus - 紧耦合
|
||||
```csharp
|
||||
public class PlcMonitorService {
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRepository<PlcData> _repository;
|
||||
|
||||
// 需要注入所有订阅者,耦合严重
|
||||
public PlcMonitorService(MainViewModel vm, ILogger logger, IRepository<PlcData> repo) { }
|
||||
|
||||
private void OnDataChanged(PlcTag tag, object value) {
|
||||
_mainViewModel.UpdateValue(tag.Id, value); // UI更新
|
||||
_logger.LogInformation($"Tag {tag.Id} changed"); // 日志
|
||||
_repository.AddAsync(new PlcData { ... }); // 存储
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ 使用 EventBus - 解耦
|
||||
```csharp
|
||||
// 发布者:只管发布,不关心谁处理
|
||||
public class PlcMonitorService {
|
||||
private readonly IEventBus _eventBus;
|
||||
|
||||
private void OnDataChanged(PlcTag tag, object value) {
|
||||
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅者1:UI
|
||||
public class MainViewModel {
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(e => UpdateValue(e.TagId, e.Value));
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅者2:日志(后续添加,无需修改发布者)
|
||||
public class PlcDataLogger {
|
||||
public PlcDataLogger(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(e =>
|
||||
_logger.LogInformation($"Tag {e.TagId} changed"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 核心优势
|
||||
|
||||
1. **解耦** - 发布者不需要知道订阅者
|
||||
2. **可扩展** - 运行时动态添加/移除订阅者
|
||||
3. **类型安全** - 强类型事件,编译期检查
|
||||
4. **跨层通信** - ViewModels、Services、Repositories 之间自由通信
|
||||
|
||||
---
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
1. **简单易用** - 一行注册,一行订阅
|
||||
2. **类型安全** - 强类型事件,避免字符串魔法值
|
||||
3. **性能优先** - 基于 `System.Threading.Channels`,高性能异步
|
||||
4. **线程安全** - 支持多线程发布/订阅
|
||||
5. **生命周期管理** - 自动清理订阅,防止内存泄漏
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始(未来)
|
||||
|
||||
### 1. 注册服务
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 一行注册
|
||||
services.AddEventBus();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 定义事件
|
||||
```csharp
|
||||
// 事件:简单的 POCO 类
|
||||
public record UserLoginEvent(string Username, DateTime LoginTime);
|
||||
|
||||
public record PlcDataChangedEvent(string TagId, object Value);
|
||||
|
||||
public record OrderCreatedEvent(int OrderId, decimal TotalAmount);
|
||||
```
|
||||
|
||||
### 3. 发布事件
|
||||
```csharp
|
||||
public class LoginService {
|
||||
private readonly IEventBus _eventBus;
|
||||
|
||||
public LoginService(IEventBus eventBus) => _eventBus = eventBus;
|
||||
|
||||
public async Task LoginAsync(string username, string password) {
|
||||
// ... 登录逻辑
|
||||
|
||||
// 发布事件
|
||||
_eventBus.Publish(new UserLoginEvent(username, DateTime.Now));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 订阅事件
|
||||
```csharp
|
||||
// 方式1:在构造函数中订阅
|
||||
public class MainViewModel : ViewModelBase {
|
||||
private IDisposable? _subscription;
|
||||
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
_subscription = eventBus.Subscribe<UserLoginEvent>(OnUserLogin);
|
||||
}
|
||||
|
||||
private void OnUserLogin(UserLoginEvent e) {
|
||||
WelcomeMessage = $"欢迎, {e.Username}!";
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
_subscription?.Dispose(); // 取消订阅
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
// 方式2:异步订阅
|
||||
public class DataSyncService {
|
||||
public DataSyncService(IEventBus eventBus) {
|
||||
eventBus.Subscribe<OrderCreatedEvent>(async e => {
|
||||
await SyncToRemoteAsync(e.OrderId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 方式3:条件过滤
|
||||
public class HighValueOrderMonitor {
|
||||
public HighValueOrderMonitor(IEventBus eventBus) {
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => e.TotalAmount > 10000, e => {
|
||||
// 只处理大额订单
|
||||
NotifyManager(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心 API 设计
|
||||
|
||||
### IEventBus 接口
|
||||
```csharp
|
||||
public interface IEventBus {
|
||||
/// <summary>
|
||||
/// 发布事件(同步)
|
||||
/// </summary>
|
||||
void Publish<TEvent>(TEvent @event) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 发布事件(异步)
|
||||
/// </summary>
|
||||
Task PublishAsync<TEvent>(TEvent @event, CancellationToken ct = default) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(同步处理器)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(异步处理器)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(带过滤条件)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(
|
||||
Func<TEvent, bool> filter,
|
||||
Action<TEvent> handler) where TEvent : class;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅事件(带优先级,数字越小优先级越高)
|
||||
/// </summary>
|
||||
IDisposable Subscribe<TEvent>(
|
||||
Action<TEvent> handler,
|
||||
int priority = 0) where TEvent : class;
|
||||
}
|
||||
```
|
||||
|
||||
### EventSubscription 类(流式API)
|
||||
```csharp
|
||||
public class EventSubscription<TEvent> : IDisposable where TEvent : class {
|
||||
/// <summary>
|
||||
/// 添加过滤条件
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> Where(Func<TEvent, bool> predicate);
|
||||
|
||||
/// <summary>
|
||||
/// 设置优先级
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> WithPriority(int priority);
|
||||
|
||||
/// <summary>
|
||||
/// 在指定线程上执行(如UI线程)
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> OnThread(TaskScheduler scheduler);
|
||||
|
||||
/// <summary>
|
||||
/// 延迟执行
|
||||
/// </summary>
|
||||
public EventSubscription<TEvent> Delay(TimeSpan delay);
|
||||
|
||||
/// <summary>
|
||||
/// 最终处理器
|
||||
/// </summary>
|
||||
public IDisposable Handle(Action<TEvent> handler);
|
||||
|
||||
public IDisposable HandleAsync(Func<TEvent, Task> handler);
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
eventBus.CreateSubscription<PlcDataChangedEvent>()
|
||||
.Where(e => e.TagId.StartsWith("Temperature"))
|
||||
.Delay(TimeSpan.FromSeconds(1)) // 防抖
|
||||
.OnThread(TaskScheduler.FromCurrentSynchronizationContext()) // UI线程
|
||||
.Handle(e => UpdateChart(e.Value));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 事件优先级
|
||||
```csharp
|
||||
// 优先级:数字越小越先执行
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => ValidateOrder(e), priority: 1);
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => SendEmail(e), priority: 10);
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => UpdateStatistics(e), priority: 20);
|
||||
```
|
||||
|
||||
### 2. 弱引用订阅(防止内存泄漏)
|
||||
```csharp
|
||||
// 自动模式:订阅者被GC回收时自动取消订阅
|
||||
eventBus.SubscribeWeak<UserLoginEvent>(viewModel, (vm, e) => vm.OnLogin(e));
|
||||
```
|
||||
|
||||
### 3. 延迟发布
|
||||
```csharp
|
||||
// 延迟1秒发布(防抖场景)
|
||||
eventBus.PublishDelayed(new SearchTextChangedEvent(text), TimeSpan.FromSeconds(1));
|
||||
```
|
||||
|
||||
### 4. 错误处理
|
||||
```csharp
|
||||
// 全局错误处理器
|
||||
services.AddEventBus(options => {
|
||||
options.OnError = (exception, @event) => {
|
||||
logger.LogError(exception, "事件处理失败: {@Event}", @event);
|
||||
};
|
||||
});
|
||||
|
||||
// 单个订阅的错误处理
|
||||
eventBus.Subscribe<OrderCreatedEvent>(e => {
|
||||
try {
|
||||
ProcessOrder(e);
|
||||
} catch (Exception ex) {
|
||||
logger.LogError(ex, "订单处理失败");
|
||||
// 不影响其他订阅者
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 事件日志(可选)
|
||||
```csharp
|
||||
services.AddEventBus(options => {
|
||||
options.EnableEventLogging = true; // 记录所有事件发布
|
||||
options.LogEventData = true; // 记录事件数据(调试用)
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 典型使用场景
|
||||
|
||||
### 场景1:跨 ViewModel 通信
|
||||
```csharp
|
||||
// LoginViewModel 发布登录成功事件
|
||||
public class LoginViewModel {
|
||||
private async Task LoginAsync() {
|
||||
// ... 登录逻辑
|
||||
_eventBus.Publish(new UserLoginSuccessEvent(user));
|
||||
}
|
||||
}
|
||||
|
||||
// MainViewModel 订阅并更新UI
|
||||
public class MainViewModel {
|
||||
public MainViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<UserLoginSuccessEvent>(e => {
|
||||
CurrentUser = e.User;
|
||||
LoadUserData();
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2:PLC数据监控
|
||||
```csharp
|
||||
// PLC监控服务发布数据变化
|
||||
public class PlcMonitorService {
|
||||
private void OnTagChanged(PlcTag tag, object value) {
|
||||
_eventBus.Publish(new PlcDataChangedEvent(tag.Id, value));
|
||||
}
|
||||
}
|
||||
|
||||
// UI订阅更新界面
|
||||
public class DashboardViewModel {
|
||||
public DashboardViewModel(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(
|
||||
e => e.TagId == "Temperature1",
|
||||
e => Temperature = (double)e.Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 数据记录服务订阅存储数据
|
||||
public class DataLoggerService {
|
||||
public DataLoggerService(IEventBus eventBus) {
|
||||
eventBus.Subscribe<PlcDataChangedEvent>(async e => {
|
||||
await _repository.AddAsync(new DataLog {
|
||||
TagId = e.TagId,
|
||||
Value = e.Value,
|
||||
Timestamp = DateTime.Now
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景3:报警系统
|
||||
```csharp
|
||||
// 报警事件
|
||||
public record AlarmTriggeredEvent(string AlarmId, AlarmLevel Level, string Message);
|
||||
|
||||
// 多个订阅者处理报警
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(e => e.Level >= AlarmLevel.High, e => {
|
||||
PlayAlarmSound(); // UI线程
|
||||
});
|
||||
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(async e => {
|
||||
await SendEmailToManager(e); // 异步发送邮件
|
||||
});
|
||||
|
||||
eventBus.Subscribe<AlarmTriggeredEvent>(e => {
|
||||
LogAlarm(e); // 记录日志
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
```csharp
|
||||
services.AddEventBus(options => {
|
||||
// 通道容量(默认1000)
|
||||
options.ChannelCapacity = 10000;
|
||||
|
||||
// 错误处理
|
||||
options.OnError = (ex, evt) => logger.LogError(ex, "事件错误");
|
||||
|
||||
// 事件日志
|
||||
options.EnableEventLogging = true;
|
||||
|
||||
// 超时配置(异步处理器超时时间)
|
||||
options.HandlerTimeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 实现方案
|
||||
|
||||
### 基于 System.Threading.Channels
|
||||
- **高性能**:异步无锁队列
|
||||
- **线程安全**:内置并发支持
|
||||
- **背压支持**:通道容量控制
|
||||
|
||||
### 架构设计
|
||||
```
|
||||
IEventBus(接口)
|
||||
↓
|
||||
InMemoryEventBus(默认实现)
|
||||
↓ 内部使用
|
||||
System.Threading.Channels.Channel<EventEnvelope>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 性能目标
|
||||
|
||||
- **发布延迟**:< 1ms(同步),< 10ms(异步)
|
||||
- **吞吐量**:> 100,000 事件/秒
|
||||
- **内存占用**:最小化,弱引用订阅防止泄漏
|
||||
- **线程安全**:完全线程安全
|
||||
|
||||
---
|
||||
|
||||
## 🔄 与其他模式对比
|
||||
|
||||
| 模式 | 优点 | 缺点 | 适用场景 |
|
||||
|-----|------|------|---------|
|
||||
| **EventBus** | 完全解耦、多订阅者 | 略微增加复杂度 | 跨模块通信、插件架构 |
|
||||
| **直接回调** | 简单直接 | 紧耦合、单订阅者 | 简单父子组件通信 |
|
||||
| **事件聚合器** | 解耦 | 需要手动管理 | 中等复杂度应用 |
|
||||
| **消息队列** | 跨进程、持久化 | 重量级、需要中间件 | 分布式系统 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **不要滥用**:简单的父子组件通信用直接回调即可
|
||||
2. **事件命名**:使用过去式(如 `UserLoggedInEvent` 而非 `UserLoginEvent`)
|
||||
3. **事件不可变**:推荐使用 `record` 定义事件
|
||||
4. **及时取消订阅**:避免内存泄漏
|
||||
5. **异常处理**:订阅者内部处理异常,不要抛出
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 可能的扩展方向
|
||||
1. **分布式 EventBus**(基于 Redis Pub/Sub)
|
||||
2. **事件溯源**(Event Sourcing)支持
|
||||
3. **事件重放**(Replay)功能
|
||||
4. **事件转换**(Transform)中间件
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考资料
|
||||
|
||||
- [MediatR](https://github.com/jbogard/MediatR) - .NET 消息中介库
|
||||
- [Prism EventAggregator](https://prismlibrary.com/docs/event-aggregator.html) - WPF MVVM 事件聚合器
|
||||
- [System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) - .NET 高性能通道
|
||||
|
||||
---
|
||||
|
||||
**状态**:设计阶段,未实现
|
||||
**预计实现时间**:根据实际需求决定
|
||||
**当前替代方案**:使用 Common 模块的 Global 容器 + 委托事件
|
||||
@@ -0,0 +1,82 @@
|
||||
# Sharp Project Context
|
||||
|
||||
## Project Overview
|
||||
**Sharp** is a modular C# .NET 8 solution designed for industrial automation and network services. It features a "Spring Boot-like" HTTP server framework, a robust PLC communication library, and WPF-based client/server applications. The architecture emphasizes dependency injection, functional error handling (Result pattern), and clean separation of concerns.
|
||||
|
||||
## Key Modules
|
||||
|
||||
### Core Infrastructure
|
||||
- **Common**: A utility library providing a lightweight DI container (`Global`), functional `Result<T>` pattern, and extensions for Strings, DateTime, Encryption, and Reflection.
|
||||
- **Config**: Configuration management.
|
||||
- **Log**: Integration with `Microsoft.Extensions.Logging`.
|
||||
- **EventBus**: Event-driven architecture components.
|
||||
|
||||
### Networking & Communications
|
||||
- **NetServer**: A lightweight, attribute-based HTTP server framework built on `HttpListener`. Features include:
|
||||
- Routing (`[Route]`, `[HttpGet]`), Authentication (`[Authorize]`), Rate Limiting, and Middleware support.
|
||||
- **NetClient**: Typed HTTP client abstractions.
|
||||
- **Plc**: Industrial communication abstraction layer.
|
||||
- **Plc.Siemens**: Implementation for Siemens S7 CPUs using optimized batch reading strategies (merging adjacent tags to reduce network overhead).
|
||||
- **Ports**: Serial/COM port abstractions.
|
||||
|
||||
### Applications (UI)
|
||||
- **Client**: Main WPF Client application.
|
||||
- **Server**: WPF Server application (likely a dashboard or local control interface).
|
||||
- **Wpf**: Shared WPF UI logic, behaviors, and converters.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Prerequisites
|
||||
- .NET 8 SDK
|
||||
- Visual Studio 2022 or VS Code (with C# Dev Kit)
|
||||
|
||||
### Building & Running
|
||||
- **Restore Dependencies**:
|
||||
```bash
|
||||
dotnet restore
|
||||
```
|
||||
- **Build Solution**:
|
||||
```bash
|
||||
dotnet build Sharp.sln -c Debug
|
||||
```
|
||||
- **Run WPF Client**:
|
||||
```bash
|
||||
dotnet run --project Client/Client.csproj
|
||||
```
|
||||
- **Run NetServer Example**:
|
||||
```bash
|
||||
dotnet run --project NetServer/Examples
|
||||
# Note: Check specific project path if Examples is a folder or project
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Tests are located in the `../../test/` directory (relative to `src/Sharp`).
|
||||
- **Run All Tests**:
|
||||
```bash
|
||||
dotnet test Sharp.sln
|
||||
```
|
||||
- **Run Specific Module Tests**:
|
||||
```bash
|
||||
dotnet test ../../test/Common.Test/Common.Test.csproj
|
||||
```
|
||||
|
||||
## Code Conventions & Standards
|
||||
|
||||
- **Language**: C# 12 / .NET 8 (`net8.0-windows` for UI).
|
||||
- **Naming**:
|
||||
- `PascalCase` for public members and types.
|
||||
- `camelCase` for private fields and local variables.
|
||||
- Async methods must end with `Async`.
|
||||
- Interfaces prefixed with `I`.
|
||||
- **Architecture**:
|
||||
- Prefer **Dependency Injection** (DI) via constructor injection.
|
||||
- Use the **Result Pattern** (`Result<T>`) from `Common` for flow control instead of exceptions for business logic.
|
||||
- **Plc Communication**: Use `IPlcService` and `PlcTag` for managed, optimized data access.
|
||||
- **Git Commit Messages**:
|
||||
- Concise and imperative (e.g., `feat: add plc reconnection logic` or `修复: 优化批量读取算法`).
|
||||
- Chinese commit messages are acceptable and common in history.
|
||||
|
||||
## Important Implementation Details
|
||||
- **Plc Optimization**: The `SiemensOptimizer` automatically batches read requests based on `MaxGapBytes` (default 20 bytes) and PDU size to maximize throughput.
|
||||
- **NetServer Middleware**: Middleware order matters. Default pipeline: Exception -> Logging -> Cors -> Auth -> RateLimit -> Cache -> Route.
|
||||
- **Common Utilities**: extensively use `Global` for service location in legacy/static contexts, but prefer DI in modern components.
|
||||
@@ -7,9 +7,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.*" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.*" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -11,9 +11,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
<PackageReference Include="S7netplus" Version="0.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -12,9 +12,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageReference Include="System.IO.Ports" Version="9.0.0" />
|
||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,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
|
||||
@@ -25,9 +25,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+201
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.35931.197 d17.13
|
||||
VisualStudioVersion = 17.13.35931.197
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{5979868C-C09D-4B3F-8909-C661F94EFFBE}"
|
||||
EndProject
|
||||
@@ -45,96 +45,296 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Plc.Siemens", "Plc.Siemens\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ports", "Ports\Ports.csproj", "{BE65F468-991A-462B-B9E5-41FE4C3CF288}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cache", "Cache\Cache.csproj", "{AC6397DA-6011-44AF-A0E2-67E1A5E67938}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wpf.HandyControl", "Wpf.HandyControl\Wpf.HandyControl.csproj", "{F33723EE-5163-40F6-A264-D521A9A7142F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{5979868C-C09D-4B3F-8909-C661F94EFFBE}.Release|x86.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x64.Build.0 = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{EF054B5F-5D65-4917-B2D7-A85FA2719128}.Release|x86.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x64.Build.0 = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{37FBE4C9-1658-4953-8D12-71068EE28804}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E1DEDCA3-85E7-4965-B05A-2965A18E9249}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BF56E23F-4B11-4C3F-9654-2D23D92314C7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{15F92FFF-521A-4F76-9886-1C0B1969D96D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C6BD72CA-701F-4983-A62E-2BD834DA10EC}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8223A4A8-DE17-4E26-A3EB-674E58B9C446}.Release|x86.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x64.Build.0 = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{74D45E9E-A513-453F-B51D-5541D2857AB8}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F48331AE-8662-411E-B789-A0158A4A1F21}.Release|x86.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x64.Build.0 = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{10DDCB28-9DF5-4BC7-8A3E-AA92275BBB40}.Release|x86.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{FA8C6432-F42B-4A47-B0C1-60E1773E9CF7}.Release|x86.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x64.Build.0 = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{DE83D16E-72D4-4E54-B4D2-17D9CF4E09DD}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B7E3FCE9-204B-47E9-B1F7-D0D08CFDBD44}.Release|x86.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x64.Build.0 = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{020997C0-FB5F-4222-8620-818840EF4447}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C2505AD1-D775-4881-A56F-42BDB7CDE58D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D4FAD87A-D436-4F66-8A7F-B57DD130E966}.Release|x86.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{8EEC8BF4-CF9A-473D-B7A0-005E151C645D}.Release|x86.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{12FA0DCD-A69B-4862-901C-A8CF8694B9E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x64.Build.0 = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{7C877AB8-1938-4CC7-8692-0D2253C728DF}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BE65F468-991A-462B-B9E5-41FE4C3CF288}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x64.Build.0 = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{AC6397DA-6011-44AF-A0E2-67E1A5E67938}.Release|x86.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{F33723EE-5163-40F6-A264-D521A9A7142F}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
# Sharp.Wpf.HandyControl
|
||||
|
||||
> **✅ 可用 / Available**
|
||||
|
||||
HandyControl UI库集成模块,为 Sharp.Wpf 提供 HandyControl 的便捷集成和扩展。
|
||||
|
||||
## 📋 模块说明
|
||||
|
||||
本模块基于 [HandyControl](https://github.com/HandyOrg/HandyControl) UI库,提供:
|
||||
- HandyControl 主题集成
|
||||
- 便捷的服务注册扩展
|
||||
- 常用对话框封装
|
||||
- 自定义控件扩展
|
||||
|
||||
## 🎯 为什么要拆分?
|
||||
|
||||
### 模块分离策略
|
||||
|
||||
```
|
||||
Sharp.Wpf (Core)
|
||||
├── MVVM基础设施(ViewModelBase, Commands)
|
||||
├── 服务抽象(IDialogService, IFileDialogService)
|
||||
├── 转换器(BoolToVisibility, EnumToDescription)
|
||||
└── 行为(EventToCommand)
|
||||
|
||||
Sharp.Wpf.HandyControl(本模块)
|
||||
├── HandyControl主题配置
|
||||
├── IDialogService的HandyControl实现
|
||||
├── HandyControl控件扩展
|
||||
└── 样式和资源
|
||||
```
|
||||
|
||||
**优势**:
|
||||
1. **可选依赖** - 不用 HandyControl 就不引入这个模块
|
||||
2. **UI库无关** - Sharp.Wpf 可以与任何UI库配合(MaterialDesign, ModernWPF等)
|
||||
3. **按需加载** - 减少不必要的依赖
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装包
|
||||
```bash
|
||||
# 安装 Sharp.Wpf(必需)
|
||||
Install-Package Sharp.Wpf
|
||||
|
||||
# 安装 Sharp.Wpf.HandyControl(可选)
|
||||
Install-Package Sharp.Wpf.HandyControl
|
||||
```
|
||||
|
||||
### 2. 添加主题资源
|
||||
```xml
|
||||
<!-- App.xaml -->
|
||||
<Application x:Class="MyApp.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:hc="https://handyorg.github.io/handycontrol">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- HandyControl 主题 -->
|
||||
<hc:ThemeResources/>
|
||||
<hc:Theme/>
|
||||
|
||||
<!-- 可选:深色主题 -->
|
||||
<ResourceDictionary Source="pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
```
|
||||
|
||||
### 3. 注册服务
|
||||
```csharp
|
||||
// App.xaml.cs
|
||||
protected override void OnStartup(StartupEventArgs e) {
|
||||
var services = new ServiceCollection();
|
||||
|
||||
// 注册基础 Wpf 服务
|
||||
services.AddWpfServices();
|
||||
|
||||
// 注册 HandyControl 集成服务
|
||||
services.AddHandyControlServices();
|
||||
|
||||
var provider = services.BuildServiceProvider();
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 使用 HandyControl 对话框
|
||||
```csharp
|
||||
public class MainViewModel : ViewModelBase {
|
||||
private readonly IHandyDialogService _dialogService;
|
||||
|
||||
public MainViewModel(IHandyDialogService dialogService) {
|
||||
_dialogService = dialogService;
|
||||
}
|
||||
|
||||
private void ShowMessage() {
|
||||
// HandyControl 样式的消息框
|
||||
_dialogService.ShowSuccess("操作成功!");
|
||||
_dialogService.ShowInfo("这是一条信息");
|
||||
_dialogService.ShowWarning("警告信息");
|
||||
_dialogService.ShowError("错误信息");
|
||||
}
|
||||
|
||||
private async Task<bool> Confirm() {
|
||||
// 确认对话框
|
||||
return await _dialogService.ConfirmAsync("确定要删除吗?", "确认删除");
|
||||
}
|
||||
|
||||
private async Task ShowInputDialog() {
|
||||
// 输入对话框
|
||||
var result = await _dialogService.ShowInputAsync("请输入名称:", "新建项目");
|
||||
if (result.IsSuccess) {
|
||||
ProjectName = result.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心 API
|
||||
|
||||
### IHandyDialogService 接口
|
||||
```csharp
|
||||
public interface IHandyDialogService : IDialogService {
|
||||
/// <summary>
|
||||
/// 显示成功消息(绿色,带勾图标)
|
||||
/// </summary>
|
||||
void ShowSuccess(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示信息消息(蓝色,带i图标)
|
||||
/// </summary>
|
||||
void ShowInfo(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示警告消息(橙色,带!图标)
|
||||
/// </summary>
|
||||
void ShowWarning(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示错误消息(红色,带X图标)
|
||||
/// </summary>
|
||||
void ShowError(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示确认对话框
|
||||
/// </summary>
|
||||
Task<bool> ConfirmAsync(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示输入对话框
|
||||
/// </summary>
|
||||
Task<Result<string>> ShowInputAsync(
|
||||
string message,
|
||||
string? title = null,
|
||||
string? defaultValue = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示等待对话框(异步操作)
|
||||
/// </summary>
|
||||
Task ShowWaitingAsync(string message, Func<Task> action);
|
||||
|
||||
/// <summary>
|
||||
/// 显示进度对话框
|
||||
/// </summary>
|
||||
IProgress<double> ShowProgress(string message, string? title = null);
|
||||
|
||||
/// <summary>
|
||||
/// 显示通知(Growl,右下角弹出)
|
||||
/// </summary>
|
||||
void ShowNotification(string message, NotificationType type = NotificationType.Info);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 典型使用场景
|
||||
|
||||
### 场景1:操作结果提示
|
||||
```csharp
|
||||
public class ProductViewModel : ViewModelBase {
|
||||
private readonly IHandyDialogService _dialog;
|
||||
|
||||
private async Task SaveAsync() {
|
||||
IsBusy = true;
|
||||
|
||||
var result = await _productService.SaveAsync(Product);
|
||||
|
||||
if (result.IsSuccess) {
|
||||
_dialog.ShowSuccess("保存成功!");
|
||||
} else {
|
||||
_dialog.ShowError($"保存失败:{result.ErrorMessage}");
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:删除确认
|
||||
```csharp
|
||||
private async Task DeleteAsync() {
|
||||
var confirmed = await _dialog.ConfirmAsync(
|
||||
"确定要删除此项吗?\n删除后无法恢复。",
|
||||
"确认删除"
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
await _repository.RemoveAsync(SelectedItem.Id);
|
||||
_dialog.ShowSuccess("删除成功");
|
||||
await RefreshAsync();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:输入对话框
|
||||
```csharp
|
||||
private async Task RenameAsync() {
|
||||
var result = await _dialog.ShowInputAsync(
|
||||
"请输入新名称:",
|
||||
"重命名",
|
||||
defaultValue: SelectedItem.Name
|
||||
);
|
||||
|
||||
if (result.IsSuccess && !string.IsNullOrEmpty(result.Value)) {
|
||||
SelectedItem.Name = result.Value;
|
||||
await _repository.UpdateAsync(SelectedItem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景4:等待对话框
|
||||
```csharp
|
||||
private async Task ExportDataAsync() {
|
||||
await _dialog.ShowWaitingAsync("正在导出数据,请稍候...", async () => {
|
||||
// 耗时操作
|
||||
await Task.Delay(3000);
|
||||
var data = await _repository.GetAllAsync();
|
||||
await _excelService.ExportAsync(data, "export.xlsx");
|
||||
});
|
||||
|
||||
_dialog.ShowSuccess("导出完成!");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景5:进度对话框
|
||||
```csharp
|
||||
private async Task BatchProcessAsync() {
|
||||
var items = await _repository.GetAllAsync();
|
||||
var progress = _dialog.ShowProgress("正在处理...", "批量处理");
|
||||
|
||||
for (int i = 0; i < items.Count; i++) {
|
||||
await ProcessItemAsync(items[i]);
|
||||
progress.Report((i + 1) * 100.0 / items.Count); // 0-100
|
||||
}
|
||||
|
||||
_dialog.ShowSuccess($"处理完成,共 {items.Count} 项");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景6:通知(Growl)
|
||||
```csharp
|
||||
public class NotificationService {
|
||||
private readonly IHandyDialogService _dialog;
|
||||
|
||||
// 右下角弹出通知(不阻塞)
|
||||
public void NotifyNewMessage(string message) {
|
||||
_dialog.ShowNotification($"新消息:{message}", NotificationType.Info);
|
||||
}
|
||||
|
||||
public void NotifyAlarm(string alarmMessage) {
|
||||
_dialog.ShowNotification($"报警:{alarmMessage}", NotificationType.Warning);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 高级功能
|
||||
|
||||
### 1. 主题切换
|
||||
```csharp
|
||||
public class ThemeService {
|
||||
public void SwitchToLightTheme() {
|
||||
SharedResourceDictionary.SharedDictionary.Source =
|
||||
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml");
|
||||
}
|
||||
|
||||
public void SwitchToDarkTheme() {
|
||||
SharedResourceDictionary.SharedDictionary.Source =
|
||||
new Uri("pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 自定义对话框
|
||||
```csharp
|
||||
public class CustomDialogViewModel : ViewModelBase {
|
||||
public string UserInput { get; set; } = "";
|
||||
|
||||
public ICommand ConfirmCommand { get; }
|
||||
public ICommand CancelCommand { get; }
|
||||
}
|
||||
|
||||
// 使用
|
||||
var viewModel = new CustomDialogViewModel();
|
||||
var result = await _dialog.ShowDialogAsync(viewModel, "自定义对话框");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 窗口动画
|
||||
```csharp
|
||||
// HandyControl 提供的窗口动画
|
||||
public class AnimatedWindow : Window {
|
||||
public AnimatedWindow() {
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
|
||||
// 启用淡入淡出动画
|
||||
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
|
||||
this.SetValue(ControlBehavior.ShowInTaskManagerProperty, false);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 包含的 HandyControl 控件扩展
|
||||
|
||||
### 常用控件
|
||||
- **MessageBox** - 样式化消息框
|
||||
- **Growl** - 通知弹窗
|
||||
- **Dialog** - 对话框容器
|
||||
- **LoadingCircle** - 加载动画
|
||||
- **ProgressBar** - 进度条
|
||||
- **Badge** - 徽章
|
||||
- **Tag** - 标签
|
||||
- **Shield** - 盾牌图标
|
||||
- **TextBox** - 增强文本框(带水印、验证)
|
||||
- **DatePicker** - 日期选择器
|
||||
- **TimePicker** - 时间选择器
|
||||
- **ComboBox** - 下拉框
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 配置选项
|
||||
|
||||
```csharp
|
||||
services.AddHandyControlServices(options => {
|
||||
// 默认对话框选项
|
||||
options.DefaultDialogWidth = 400;
|
||||
options.DefaultDialogHeight = 200;
|
||||
|
||||
// Growl 配置
|
||||
options.GrowlToken = "MainGrowl"; // Growl容器名称
|
||||
options.GrowlPosition = GrowlPosition.BottomRight;
|
||||
|
||||
// 主题配置
|
||||
options.DefaultTheme = Theme.Light;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 样式定制
|
||||
|
||||
### 自定义主题颜色
|
||||
```xml
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<hc:ThemeResources AccentColor="#FF0078D7"/>
|
||||
<hc:Theme/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **HandyControl 版本**:当前支持 HandyControl 3.5.0
|
||||
2. **依赖 Sharp.Wpf**:必须先引用 Sharp.Wpf 模块
|
||||
3. **主题资源**:需要在 App.xaml 中引入 HandyControl 资源字典
|
||||
4. **Growl容器**:需要在主窗口添加 Growl 容器才能使用通知功能
|
||||
|
||||
---
|
||||
|
||||
## 🔄 与其他UI库对比
|
||||
|
||||
| UI库 | 特点 | 适用场景 |
|
||||
|-----|------|---------|
|
||||
| **HandyControl** | 现代化设计、控件丰富 | 企业级应用、工具软件 |
|
||||
| **MaterialDesign** | Material风格 | 现代化Web风格应用 |
|
||||
| **ModernWPF** | Windows 11风格 | 跟随Windows系统风格 |
|
||||
| **原生WPF** | 轻量级 | 简单应用、追求性能 |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 未来扩展
|
||||
|
||||
### 可能的扩展方向
|
||||
1. **Sharp.Wpf.MaterialDesign** - MaterialDesign UI库集成
|
||||
2. **Sharp.Wpf.ModernWPF** - ModernWPF UI库集成
|
||||
3. **自定义控件库** - 基于 HandyControl 的业务控件
|
||||
|
||||
---
|
||||
|
||||
## 📖 参考资料
|
||||
|
||||
- [HandyControl GitHub](https://github.com/HandyOrg/HandyControl)
|
||||
- [HandyControl 官方文档](https://handyorg.github.io/)
|
||||
- [HandyControl Demo](https://github.com/HandyOrg/HandyControl/releases)
|
||||
|
||||
---
|
||||
|
||||
**状态**:规划阶段,未实现
|
||||
**依赖**:Sharp.Wpf, HandyControl 3.5.0
|
||||
**实现难度**:低(封装现有API)
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<AssemblyName>Sharp.Wpf.HandyControl</AssemblyName>
|
||||
<RootNamespace>Sharp.Wpf.HandyControl</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HandyControl" Version="3.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wpf\Wpf.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using Sharp.Wpf.Services;
|
||||
using Sharp.Wpf.Navigation;
|
||||
using Sharp.Wpf.Theming;
|
||||
using Sharp.Wpf.Localization;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
using Sharp.Wpf.Abstractions;
|
||||
using Sharp.Wpf.Exceptions;
|
||||
|
||||
namespace Sharp.Wpf.DependencyInjection;
|
||||
|
||||
/// <summary>
|
||||
/// WPF 服务便捷注册扩展
|
||||
/// </summary>
|
||||
public static class WpfServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加 Sharp.Wpf 基础服务(一行注册)
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddSharpWpf(this IServiceCollection services)
|
||||
{
|
||||
// 基础服务
|
||||
services.AddSingleton<IDialogService, DialogService>();
|
||||
services.AddSingleton<IFileDialogService, FileDialogService>();
|
||||
services.AddSingleton<IDispatcher>(sp =>
|
||||
new DispatcherService(System.Windows.Application.Current.Dispatcher));
|
||||
|
||||
// 全局异常处理器
|
||||
services.AddSingleton<IUiExceptionHandler, UiExceptionHandler>();
|
||||
|
||||
// 导航服务
|
||||
services.AddSingleton<INavigationService, NavigationService>();
|
||||
|
||||
// 主题服务
|
||||
services.AddSingleton<IThemeService, ThemeService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加国际化支持
|
||||
/// </summary>
|
||||
/// <param name="services">服务集合</param>
|
||||
/// <param name="resourceManager">资源管理器(通常是 Resources.ResourceManager)</param>
|
||||
/// <param name="supportedCultures">支持的语言</param>
|
||||
/// <returns>服务集合</returns>
|
||||
public static IServiceCollection AddLocalization(
|
||||
this IServiceCollection services,
|
||||
ResourceManager resourceManager,
|
||||
params CultureInfo[] supportedCultures)
|
||||
{
|
||||
if (supportedCultures.Length == 0)
|
||||
{
|
||||
supportedCultures = new[] { new CultureInfo("zh-CN"), new CultureInfo("en-US") };
|
||||
}
|
||||
|
||||
var localizationService = new LocalizationService(resourceManager, supportedCultures);
|
||||
services.AddSingleton<ILocalizationService>(localizationService);
|
||||
|
||||
// 初始化 Markup Extension
|
||||
LocalizeExtension.Initialize(localizationService);
|
||||
LocalizeConverter.Initialize(localizationService);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 ViewModel
|
||||
/// </summary>
|
||||
public static IServiceCollection AddViewModel<TViewModel>(
|
||||
this IServiceCollection services)
|
||||
where TViewModel : ViewModelBase
|
||||
{
|
||||
services.AddTransient<TViewModel>();
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册 Window 和对应的 ViewModel
|
||||
/// </summary>
|
||||
public static IServiceCollection AddWindow<TWindow, TViewModel>(
|
||||
this IServiceCollection services)
|
||||
where TWindow : System.Windows.Window
|
||||
where TViewModel : ViewModelBase
|
||||
{
|
||||
services.AddTransient<TWindow>();
|
||||
services.AddTransient<TViewModel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
|
||||
namespace Sharp.Wpf.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 国际化服务接口
|
||||
/// </summary>
|
||||
public interface ILocalizationService : INotifyPropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前语言
|
||||
/// </summary>
|
||||
CultureInfo CurrentCulture { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 更改语言
|
||||
/// </summary>
|
||||
void ChangeCulture(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// 更改语言(通过语言代码)
|
||||
/// </summary>
|
||||
void ChangeCulture(string cultureName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串
|
||||
/// </summary>
|
||||
string GetString(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(带格式化)
|
||||
/// </summary>
|
||||
string GetString(string key, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 获取支持的语言列表
|
||||
/// </summary>
|
||||
IEnumerable<CultureInfo> GetSupportedCultures();
|
||||
|
||||
/// <summary>
|
||||
/// 语言改变事件
|
||||
/// </summary>
|
||||
event EventHandler<CultureInfo>? CultureChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际化服务实现(基于 ResourceManager)
|
||||
/// </summary>
|
||||
public class LocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly ResourceManager _resourceManager;
|
||||
private CultureInfo _currentCulture;
|
||||
private readonly List<CultureInfo> _supportedCultures;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<CultureInfo>? CultureChanged;
|
||||
|
||||
public CultureInfo CurrentCulture
|
||||
{
|
||||
get => _currentCulture;
|
||||
private set
|
||||
{
|
||||
if (_currentCulture != value)
|
||||
{
|
||||
_currentCulture = value;
|
||||
CultureInfo.CurrentCulture = value;
|
||||
CultureInfo.CurrentUICulture = value;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCulture)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Item[]")); // 通知所有绑定刷新
|
||||
CultureChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="resourceManager">资源管理器(通常是 Resources.ResourceManager)</param>
|
||||
/// <param name="supportedCultures">支持的语言列表</param>
|
||||
public LocalizationService(ResourceManager resourceManager, params CultureInfo[] supportedCultures)
|
||||
{
|
||||
_resourceManager = resourceManager;
|
||||
_supportedCultures = supportedCultures.ToList();
|
||||
_currentCulture = CultureInfo.CurrentUICulture;
|
||||
|
||||
// 如果当前语言不在支持列表中,使用第一个支持的语言
|
||||
if (!_supportedCultures.Any(c => c.Name == _currentCulture.Name) && _supportedCultures.Count > 0)
|
||||
{
|
||||
_currentCulture = _supportedCultures[0];
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeCulture(CultureInfo culture)
|
||||
{
|
||||
CurrentCulture = culture;
|
||||
}
|
||||
|
||||
public void ChangeCulture(string cultureName)
|
||||
{
|
||||
ChangeCulture(new CultureInfo(cultureName));
|
||||
}
|
||||
|
||||
public string GetString(string key)
|
||||
{
|
||||
return _resourceManager.GetString(key, CurrentCulture) ?? $"[{key}]";
|
||||
}
|
||||
|
||||
public string GetString(string key, params object[] args)
|
||||
{
|
||||
var format = GetString(key);
|
||||
return string.Format(format, args);
|
||||
}
|
||||
|
||||
public IEnumerable<CultureInfo> GetSupportedCultures()
|
||||
{
|
||||
return _supportedCultures;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 索引器,用于 XAML 绑定
|
||||
/// </summary>
|
||||
public string this[string key] => GetString(key);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Markup;
|
||||
using WpfBinding = System.Windows.Data.Binding;
|
||||
|
||||
namespace Sharp.Wpf.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// 本地化标记扩展,用于 XAML 中的国际化绑定
|
||||
/// 用法: {loc:Localize Key=WelcomeMessage}
|
||||
/// </summary>
|
||||
public class LocalizeExtension : MarkupExtension
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
|
||||
/// <summary>
|
||||
/// 设置全局本地化服务(在 App 启动时调用)
|
||||
/// </summary>
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 资源键
|
||||
/// </summary>
|
||||
public string Key { get; set; } = string.Empty;
|
||||
|
||||
public LocalizeExtension() { }
|
||||
|
||||
public LocalizeExtension(string key)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (_localizationService == null)
|
||||
{
|
||||
return $"[{Key}]";
|
||||
}
|
||||
|
||||
// 创建绑定到 LocalizationService 的 Binding
|
||||
var binding = new WpfBinding($"[{Key}]")
|
||||
{
|
||||
Source = _localizationService,
|
||||
Mode = BindingMode.OneWay
|
||||
};
|
||||
|
||||
return binding.ProvideValue(serviceProvider);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本地化转换器(用于动态参数)
|
||||
/// </summary>
|
||||
public class LocalizeConverter : IValueConverter
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (_localizationService == null || parameter is not string key)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return _localizationService.GetString(key, value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
using System.Windows;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
|
||||
namespace Sharp.Wpf.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// 导航服务接口
|
||||
/// </summary>
|
||||
public interface INavigationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 导航到指定 ViewModel
|
||||
/// </summary>
|
||||
Task NavigateAsync<TViewModel>() where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 导航到指定 ViewModel(带参数)
|
||||
/// </summary>
|
||||
Task NavigateAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 导航并等待结果
|
||||
/// </summary>
|
||||
Task<TResult?> NavigateWithResultAsync<TViewModel, TResult>() where TViewModel : ViewModelBase;
|
||||
|
||||
/// <summary>
|
||||
/// 后退
|
||||
/// </summary>
|
||||
Task<bool> GoBackAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 前进
|
||||
/// </summary>
|
||||
Task<bool> GoForwardAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 清除导航历史
|
||||
/// </summary>
|
||||
void ClearHistory();
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以后退
|
||||
/// </summary>
|
||||
bool CanGoBack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以前进
|
||||
/// </summary>
|
||||
bool CanGoForward { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前 ViewModel
|
||||
/// </summary>
|
||||
ViewModelBase? CurrentViewModel { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 导航事件
|
||||
/// </summary>
|
||||
event EventHandler<ViewModelBase>? Navigated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航服务实现(基于 Frame 或自定义容器)
|
||||
/// </summary>
|
||||
public class NavigationService : INavigationService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Stack<ViewModelBase> _backStack = new();
|
||||
private readonly Stack<ViewModelBase> _forwardStack = new();
|
||||
private ViewModelBase? _currentViewModel;
|
||||
|
||||
public ViewModelBase? CurrentViewModel
|
||||
{
|
||||
get => _currentViewModel;
|
||||
private set
|
||||
{
|
||||
_currentViewModel = value;
|
||||
if (value != null)
|
||||
{
|
||||
Navigated?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanGoBack => _backStack.Count > 0;
|
||||
public bool CanGoForward => _forwardStack.Count > 0;
|
||||
|
||||
public event EventHandler<ViewModelBase>? Navigated;
|
||||
|
||||
public NavigationService(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task NavigateAsync<TViewModel>() where TViewModel : ViewModelBase
|
||||
{
|
||||
await NavigateAsync<TViewModel>(null);
|
||||
}
|
||||
|
||||
public async Task NavigateAsync<TViewModel>(object? parameter) where TViewModel : ViewModelBase
|
||||
{
|
||||
var viewModel = _serviceProvider.GetService(typeof(TViewModel)) as TViewModel
|
||||
?? throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未注册");
|
||||
|
||||
// 保存当前 ViewModel 到后退栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
}
|
||||
|
||||
// 清空前进栈
|
||||
_forwardStack.Clear();
|
||||
|
||||
CurrentViewModel = viewModel;
|
||||
|
||||
// 如果 ViewModel 实现了 INavigationAware,调用生命周期方法
|
||||
if (viewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TResult?> NavigateWithResultAsync<TViewModel, TResult>() where TViewModel : ViewModelBase
|
||||
{
|
||||
var viewModel = _serviceProvider.GetService(typeof(TViewModel)) as TViewModel
|
||||
?? throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未注册");
|
||||
|
||||
// 如果 ViewModel 实现了结果接口
|
||||
if (viewModel is INavigationResult<TResult> resultViewModel)
|
||||
{
|
||||
// 保存当前 ViewModel
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
}
|
||||
|
||||
CurrentViewModel = viewModel;
|
||||
|
||||
if (viewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
// 等待结果
|
||||
var result = await resultViewModel.GetResultAsync();
|
||||
|
||||
// 自动后退
|
||||
await GoBackAsync();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"ViewModel {typeof(TViewModel).Name} 未实现 INavigationResult<{typeof(TResult).Name}>");
|
||||
}
|
||||
|
||||
public async Task<bool> GoBackAsync()
|
||||
{
|
||||
if (!CanGoBack) return false;
|
||||
|
||||
// 保存当前 ViewModel 到前进栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_forwardStack.Push(CurrentViewModel);
|
||||
|
||||
if (CurrentViewModel is INavigationAware currentAware)
|
||||
{
|
||||
await currentAware.OnNavigatedFromAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出后退栈
|
||||
var previousViewModel = _backStack.Pop();
|
||||
CurrentViewModel = previousViewModel;
|
||||
|
||||
if (previousViewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> GoForwardAsync()
|
||||
{
|
||||
if (!CanGoForward) return false;
|
||||
|
||||
// 保存当前 ViewModel 到后退栈
|
||||
if (CurrentViewModel != null)
|
||||
{
|
||||
_backStack.Push(CurrentViewModel);
|
||||
|
||||
if (CurrentViewModel is INavigationAware currentAware)
|
||||
{
|
||||
await currentAware.OnNavigatedFromAsync();
|
||||
}
|
||||
}
|
||||
|
||||
// 弹出前进栈
|
||||
var nextViewModel = _forwardStack.Pop();
|
||||
CurrentViewModel = nextViewModel;
|
||||
|
||||
if (nextViewModel is INavigationAware aware)
|
||||
{
|
||||
await aware.OnNavigatedToAsync(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearHistory()
|
||||
{
|
||||
_backStack.Clear();
|
||||
_forwardStack.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航生命周期接口
|
||||
/// </summary>
|
||||
public interface INavigationAware
|
||||
{
|
||||
/// <summary>
|
||||
/// 导航到此 ViewModel 时调用
|
||||
/// </summary>
|
||||
Task OnNavigatedToAsync(object? parameter);
|
||||
|
||||
/// <summary>
|
||||
/// 离开此 ViewModel 时调用
|
||||
/// </summary>
|
||||
Task OnNavigatedFromAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导航结果接口
|
||||
/// </summary>
|
||||
public interface INavigationResult<TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取导航结果
|
||||
/// </summary>
|
||||
Task<TResult?> GetResultAsync();
|
||||
}
|
||||
@@ -111,3 +111,15 @@ var folder = await new FileDialogService().SelectFolderAsync();
|
||||
- 若要禁用全局异常弹窗,可在 `ExceptionHandlingOptions` 中设置 `ShowDialog = false` 并仅使用 `Log` 钩子记录。
|
||||
- `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
|
||||
@@ -0,0 +1,198 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using MediaColor = System.Windows.Media.Color;
|
||||
using WpfApplication = System.Windows.Application;
|
||||
|
||||
namespace Sharp.Wpf.Theming;
|
||||
|
||||
/// <summary>
|
||||
/// 主题枚举
|
||||
/// </summary>
|
||||
public enum Theme
|
||||
{
|
||||
Light,
|
||||
Dark,
|
||||
System // 跟随系统
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主题服务接口
|
||||
/// </summary>
|
||||
public interface IThemeService
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前主题
|
||||
/// </summary>
|
||||
Theme CurrentTheme { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用主题
|
||||
/// </summary>
|
||||
void ApplyTheme(Theme theme);
|
||||
|
||||
/// <summary>
|
||||
/// 获取主题颜色
|
||||
/// </summary>
|
||||
MediaColor GetColor(string colorKey);
|
||||
|
||||
/// <summary>
|
||||
/// 设置主题颜色
|
||||
/// </summary>
|
||||
void SetColor(string colorKey, MediaColor color);
|
||||
|
||||
/// <summary>
|
||||
/// 主题改变事件
|
||||
/// </summary>
|
||||
event EventHandler<Theme>? ThemeChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主题服务实现
|
||||
/// </summary>
|
||||
public class ThemeService : IThemeService
|
||||
{
|
||||
private Theme _currentTheme = Theme.Light;
|
||||
private readonly ResourceDictionary _lightTheme;
|
||||
private readonly ResourceDictionary _darkTheme;
|
||||
|
||||
public Theme CurrentTheme
|
||||
{
|
||||
get => _currentTheme;
|
||||
private set
|
||||
{
|
||||
if (_currentTheme != value)
|
||||
{
|
||||
_currentTheme = value;
|
||||
ThemeChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<Theme>? ThemeChanged;
|
||||
|
||||
public ThemeService()
|
||||
{
|
||||
_lightTheme = CreateLightTheme();
|
||||
_darkTheme = CreateDarkTheme();
|
||||
}
|
||||
|
||||
public void ApplyTheme(Theme theme)
|
||||
{
|
||||
var actualTheme = theme;
|
||||
|
||||
// 如果选择跟随系统,检测系统主题
|
||||
if (theme == Theme.System)
|
||||
{
|
||||
actualTheme = DetectSystemTheme();
|
||||
}
|
||||
|
||||
var themeDict = actualTheme == Theme.Light ? _lightTheme : _darkTheme;
|
||||
|
||||
// 移除旧主题
|
||||
var oldTheme = WpfApplication.Current.Resources.MergedDictionaries
|
||||
.FirstOrDefault(d => d.Source?.ToString().Contains("Theme") == true);
|
||||
if (oldTheme != null)
|
||||
{
|
||||
WpfApplication.Current.Resources.MergedDictionaries.Remove(oldTheme);
|
||||
}
|
||||
|
||||
// 应用新主题
|
||||
WpfApplication.Current.Resources.MergedDictionaries.Add(themeDict);
|
||||
|
||||
CurrentTheme = theme;
|
||||
}
|
||||
|
||||
public MediaColor GetColor(string colorKey)
|
||||
{
|
||||
if (WpfApplication.Current.Resources[colorKey] is MediaColor color)
|
||||
{
|
||||
return color;
|
||||
}
|
||||
|
||||
if (WpfApplication.Current.Resources[colorKey] is SolidColorBrush brush)
|
||||
{
|
||||
return brush.Color;
|
||||
}
|
||||
|
||||
return Colors.Transparent;
|
||||
}
|
||||
|
||||
public void SetColor(string colorKey, MediaColor color)
|
||||
{
|
||||
WpfApplication.Current.Resources[colorKey] = new SolidColorBrush(color);
|
||||
}
|
||||
|
||||
private Theme DetectSystemTheme()
|
||||
{
|
||||
// Windows 10/11 检测系统主题
|
||||
try
|
||||
{
|
||||
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
|
||||
|
||||
var value = key?.GetValue("AppsUseLightTheme");
|
||||
if (value is int intValue)
|
||||
{
|
||||
return intValue == 0 ? Theme.Dark : Theme.Light;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误,返回默认主题
|
||||
}
|
||||
|
||||
return Theme.Light;
|
||||
}
|
||||
|
||||
private ResourceDictionary CreateLightTheme()
|
||||
{
|
||||
return new ResourceDictionary
|
||||
{
|
||||
// 基础颜色
|
||||
["PrimaryColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
["BackgroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["ForegroundColor"] = new SolidColorBrush(Colors.Black),
|
||||
["BorderColor"] = new SolidColorBrush(MediaColor.FromRgb(204, 204, 204)),
|
||||
|
||||
// 状态颜色
|
||||
["SuccessColor"] = new SolidColorBrush(MediaColor.FromRgb(16, 124, 16)),
|
||||
["WarningColor"] = new SolidColorBrush(MediaColor.FromRgb(255, 185, 0)),
|
||||
["ErrorColor"] = new SolidColorBrush(MediaColor.FromRgb(232, 17, 35)),
|
||||
["InfoColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
|
||||
// 控件颜色
|
||||
["ButtonBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(240, 240, 240)),
|
||||
["ButtonHoverColor"] = new SolidColorBrush(MediaColor.FromRgb(229, 229, 229)),
|
||||
["ButtonPressedColor"] = new SolidColorBrush(MediaColor.FromRgb(204, 204, 204)),
|
||||
|
||||
["InputBackgroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["InputBorderColor"] = new SolidColorBrush(MediaColor.FromRgb(171, 173, 179)),
|
||||
};
|
||||
}
|
||||
|
||||
private ResourceDictionary CreateDarkTheme()
|
||||
{
|
||||
return new ResourceDictionary
|
||||
{
|
||||
// 基础颜色
|
||||
["PrimaryColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
["BackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(32, 32, 32)),
|
||||
["ForegroundColor"] = new SolidColorBrush(Colors.White),
|
||||
["BorderColor"] = new SolidColorBrush(MediaColor.FromRgb(96, 96, 96)),
|
||||
|
||||
// 状态颜色
|
||||
["SuccessColor"] = new SolidColorBrush(MediaColor.FromRgb(16, 124, 16)),
|
||||
["WarningColor"] = new SolidColorBrush(MediaColor.FromRgb(255, 185, 0)),
|
||||
["ErrorColor"] = new SolidColorBrush(MediaColor.FromRgb(232, 17, 35)),
|
||||
["InfoColor"] = new SolidColorBrush(MediaColor.FromRgb(0, 120, 215)),
|
||||
|
||||
// 控件颜色
|
||||
["ButtonBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(48, 48, 48)),
|
||||
["ButtonHoverColor"] = new SolidColorBrush(MediaColor.FromRgb(64, 64, 64)),
|
||||
["ButtonPressedColor"] = new SolidColorBrush(MediaColor.FromRgb(32, 32, 32)),
|
||||
|
||||
["InputBackgroundColor"] = new SolidColorBrush(MediaColor.FromRgb(45, 45, 45)),
|
||||
["InputBorderColor"] = new SolidColorBrush(MediaColor.FromRgb(96, 96, 96)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Sharp.Wpf.Mvvm;
|
||||
|
||||
namespace Sharp.Wpf.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// 支持验证的 ViewModel 基类(实现 INotifyDataErrorInfo)
|
||||
/// </summary>
|
||||
public abstract class ValidatableViewModelBase : ViewModelBase, INotifyDataErrorInfo
|
||||
{
|
||||
private readonly Dictionary<string, List<string>> _errors = new();
|
||||
|
||||
public bool HasErrors => _errors.Count > 0;
|
||||
|
||||
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
|
||||
|
||||
public IEnumerable GetErrors(string? propertyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(propertyName))
|
||||
{
|
||||
return _errors.Values.SelectMany(e => e);
|
||||
}
|
||||
|
||||
return _errors.TryGetValue(propertyName!, out var errors) ? errors : Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置属性值并验证
|
||||
/// </summary>
|
||||
protected bool SetProperty<T>(ref T storage, T value, bool validate = true, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(storage, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
|
||||
if (validate && propertyName != null)
|
||||
{
|
||||
ValidateProperty(value, propertyName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证单个属性
|
||||
/// </summary>
|
||||
protected void ValidateProperty(object? value, string propertyName)
|
||||
{
|
||||
ClearErrors(propertyName);
|
||||
|
||||
var context = new ValidationContext(this) { MemberName = propertyName };
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateProperty(value, context, results))
|
||||
{
|
||||
var errors = results.Select(r => r.ErrorMessage ?? "验证失败").ToList();
|
||||
_errors[propertyName] = errors;
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证所有属性
|
||||
/// </summary>
|
||||
public bool ValidateAll()
|
||||
{
|
||||
var context = new ValidationContext(this);
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
if (!Validator.TryValidateObject(this, context, results, validateAllProperties: true))
|
||||
{
|
||||
// 清除旧错误
|
||||
var oldErrorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
|
||||
// 添加新错误
|
||||
foreach (var result in results)
|
||||
{
|
||||
foreach (var memberName in result.MemberNames)
|
||||
{
|
||||
if (!_errors.ContainsKey(memberName))
|
||||
{
|
||||
_errors[memberName] = new List<string>();
|
||||
}
|
||||
_errors[memberName].Add(result.ErrorMessage ?? "验证失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 通知所有属性错误变化
|
||||
var allProperties = oldErrorProperties.Union(_errors.Keys).Distinct();
|
||||
foreach (var property in allProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 清除所有错误
|
||||
var errorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
foreach (var property in errorProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除指定属性的错误
|
||||
/// </summary>
|
||||
protected void ClearErrors(string propertyName)
|
||||
{
|
||||
if (_errors.Remove(propertyName))
|
||||
{
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有错误
|
||||
/// </summary>
|
||||
protected void ClearAllErrors()
|
||||
{
|
||||
var errorProperties = _errors.Keys.ToList();
|
||||
_errors.Clear();
|
||||
foreach (var property in errorProperties)
|
||||
{
|
||||
OnErrorsChanged(property);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加自定义错误
|
||||
/// </summary>
|
||||
protected void AddError(string propertyName, string error)
|
||||
{
|
||||
if (!_errors.ContainsKey(propertyName))
|
||||
{
|
||||
_errors[propertyName] = new List<string>();
|
||||
}
|
||||
|
||||
if (!_errors[propertyName].Contains(error))
|
||||
{
|
||||
_errors[propertyName].Add(error);
|
||||
OnErrorsChanged(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnErrorsChanged(string propertyName)
|
||||
{
|
||||
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
|
||||
OnPropertyChanged(nameof(HasErrors));
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,8 @@
|
||||
<RootNamespace>Sharp.Wpf</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+bc09280d4ea03ae8d0db7cd8c25ddd02d63abd84")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+433e206d456d0ef3b2fc57c458ff3fc030643be6")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Sharp.Wpf")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
d55a55a135e80fbfbfad99db94d1d2c02d22ff2d590de8697e3ccba5e8c5d43a
|
||||
cd23ae979f4410b7a90bcdab8832e81e422253dc2d502d88544f9372c3b0bd4d
|
||||
Binary file not shown.
@@ -49,6 +49,12 @@
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"target": "Package",
|
||||
"version": "[9.0.0, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
|
||||
@@ -1,11 +1,61 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
"net8.0-windows7.0": {}
|
||||
"net8.0-windows7.0": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": {
|
||||
"type": "package",
|
||||
"compile": {
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
|
||||
"related": ".xml"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"buildTransitive/net8.0/_._": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.0": {
|
||||
"sha512": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==",
|
||||
"type": "package",
|
||||
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.0",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"Icon.png",
|
||||
"LICENSE.TXT",
|
||||
"PACKAGE.md",
|
||||
"THIRD-PARTY-NOTICES.TXT",
|
||||
"buildTransitive/net461/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
|
||||
"buildTransitive/net462/_._",
|
||||
"buildTransitive/net8.0/_._",
|
||||
"buildTransitive/netcoreapp2.0/Microsoft.Extensions.DependencyInjection.Abstractions.targets",
|
||||
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net462/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/netstandard2.0/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.dll",
|
||||
"lib/netstandard2.1/Microsoft.Extensions.DependencyInjection.Abstractions.xml",
|
||||
"microsoft.extensions.dependencyinjection.abstractions.9.0.0.nupkg.sha512",
|
||||
"microsoft.extensions.dependencyinjection.abstractions.nuspec",
|
||||
"useSharedDesignerContext.txt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net8.0-windows7.0": []
|
||||
"net8.0-windows7.0": [
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions >= 9.0.0"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\Administrator\\.nuget\\packages\\": {},
|
||||
@@ -56,6 +106,12 @@
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"dependencies": {
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"target": "Package",
|
||||
"version": "[9.0.0, )"
|
||||
}
|
||||
},
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "NHqLv58UxH0=",
|
||||
"dgSpecHash": "zqaFYIDtlpU=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\Administrator\\.nuget\\packages\\microsoft.extensions.dependencyinjection.abstractions\\9.0.0\\microsoft.extensions.dependencyinjection.abstractions.9.0.0.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
"restore":{"projectUniqueName":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","projectName":"Sharp.Wpf","projectPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","outputPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"],"originalTargetFrameworks":["net8.0-windows"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.100"}"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"},"Microsoft.WindowsDesktop.App":{"privateAssets":"none"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\10.0.100/PortableRuntimeIdentifierGraph.json"}}
|
||||
"restore":{"projectUniqueName":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","projectName":"Sharp.Wpf","projectPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\Wpf.csproj","outputPath":"D:\\code\\sharp-basic\\src\\Sharp\\Wpf\\obj\\","projectStyle":"PackageReference","fallbackFolders":["D:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"],"originalTargetFrameworks":["net8.0-windows"],"sources":{"C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\":{},"https://api.nuget.org/v3/index.json":{}},"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","projectReferences":{}}},"warningProperties":{"warnAsError":["NU1605"]},"restoreAuditProperties":{"enableAudit":"true","auditLevel":"low","auditMode":"direct"},"SdkAnalysisLevel":"10.0.100"}"frameworks":{"net8.0-windows7.0":{"targetAlias":"net8.0-windows","dependencies":{"Microsoft.Extensions.DependencyInjection.Abstractions":{"target":"Package","version":"[9.0.0, )"}},"imports":["net461","net462","net47","net471","net472","net48","net481"],"assetTargetFallback":true,"warn":true,"frameworkReferences":{"Microsoft.NETCore.App":{"privateAssets":"all"},"Microsoft.WindowsDesktop.App":{"privateAssets":"none"}},"runtimeIdentifierGraphPath":"C:\\Program Files\\dotnet\\sdk\\10.0.100/PortableRuntimeIdentifierGraph.json"}}
|
||||
@@ -1 +1 @@
|
||||
17652582922004239
|
||||
17654362814006268
|
||||
@@ -1 +1 @@
|
||||
17652582922004239
|
||||
17657764918263100
|
||||
Reference in new issue
Block a user