更新框架
This commit is contained in:
1 parent
ef50fa0620
commit
84b28f9672
72 files changed
+2222
-282
No files matched your search
@@ -0,0 +1,61 @@
|
||||
using Store.Management.Framework.Configs;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Security;
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace Store.Management.Framework;
|
||||
|
||||
/// <summary>当前进程的应用信息;目录与持久化设置分别由 FolderConfig 和设置服务管理。</summary>
|
||||
public static class Config
|
||||
{
|
||||
public static Guid SessionID { get; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>进程启动时间(本地时间),不受首次访问 Config 的时机影响。</summary>
|
||||
public static DateTime AppStartTime { get; } = GetAppStartTime();
|
||||
|
||||
public static string AppName => FolderConfig.AppName;
|
||||
public static string AppFullName => AppName;
|
||||
public static Version WindowsVersion => Environment.OSVersion.Version;
|
||||
|
||||
/// <summary>优先使用入口程序集文件版本;单文件发布等场景回退到程序集版本。</summary>
|
||||
public static Version Version { get; } = GetVersion();
|
||||
|
||||
/// <summary>入口程序集的编译配置,不使用是否附加调试器作为判断依据。</summary>
|
||||
public static bool IsRelease { get; } = !string.Equals(
|
||||
EntryAssembly.GetCustomAttribute<AssemblyConfigurationAttribute>()?.Configuration,
|
||||
"Debug", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsAdministrator
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return false;
|
||||
try
|
||||
{
|
||||
using WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
catch (SecurityException) { return false; }
|
||||
catch (UnauthorizedAccessException) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly EntryAssembly => Assembly.GetEntryAssembly() ?? typeof(Config).Assembly;
|
||||
|
||||
public static string GetFullVersionString() =>
|
||||
$"{Version} - {(IsRelease ? "Release" : "Debug")}{(IsAdministrator ? " - 管理员" : string.Empty)}";
|
||||
|
||||
private static Version GetVersion()
|
||||
{
|
||||
string? fileVersion = EntryAssembly.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version;
|
||||
return System.Version.TryParse(fileVersion, out Version? version)
|
||||
? version : EntryAssembly.GetName().Version ?? new Version(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static DateTime GetAppStartTime()
|
||||
{
|
||||
using Process process = Process.GetCurrentProcess();
|
||||
return process.StartTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using MAX.Framework.Interfaces;
|
||||
using System.IO;
|
||||
|
||||
namespace Store.Management.Framework.Configs;
|
||||
|
||||
/// <summary>应用目录约定。路径读取不创建目录,保存配置时自动创建所需目录。</summary>
|
||||
public sealed class FolderConfig : IFolderConfig
|
||||
{
|
||||
/// <summary>供启动配置使用;容器也可通过 IFolderConfig 解析目录配置。</summary>
|
||||
public static FolderConfig Instance { get; } = new();
|
||||
|
||||
public FolderConfig() { }
|
||||
|
||||
string IFolderConfig.ConfigFolder => ConfigFolder;
|
||||
string IFolderConfig.DataFolder => DataFolder;
|
||||
string IFolderConfig.LoggingFolder => LoggingFolder;
|
||||
string IFolderConfig.CacheFolder => CacheFolder;
|
||||
|
||||
public const string AppName = "Store.Management";
|
||||
|
||||
/// <summary>安装目录,用于读取随程序分发的资源。</summary>
|
||||
public static string ExeFolder => AppContext.BaseDirectory;
|
||||
|
||||
/// <summary>当前用户的数据根目录:%LOCALAPPDATA%\Store.Management。</summary>
|
||||
public static string AppFolder => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppName);
|
||||
|
||||
public static string ConfigFolder => Path.Combine(AppFolder, "Config");
|
||||
public static string DataFolder => Path.Combine(AppFolder, "Data");
|
||||
public static string LoggingFolder => Path.Combine(AppFolder, "Logs");
|
||||
public static string DatabaseFolder => Path.Combine(AppFolder, "Db");
|
||||
public static string CacheFolder => Path.Combine(AppFolder, "Cache");
|
||||
public static string AppSettingsFilePath => GetSettingsFilePath("AppSettings.json");
|
||||
|
||||
/// <summary>获取配置文件路径,仅接受文件名,防止写出配置目录。</summary>
|
||||
public static string GetSettingsFilePath(string fileName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||||
if (fileName is "." or ".." || fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
|
||||
|| fileName.EndsWith('.') || fileName.EndsWith(' '))
|
||||
throw new ArgumentException("配置文件名不能包含路径、非法字符或以点、空格结尾。", nameof(fileName));
|
||||
return Path.Combine(ConfigFolder, fileName);
|
||||
}
|
||||
|
||||
/// <summary>按需在宿主启动时创建应用目录;创建失败向调用者报告。</summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
foreach (string path in new[] { ConfigFolder, DataFolder, LoggingFolder, DatabaseFolder, CacheFolder })
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,12 @@ public partial class DialogWindow : WindowBase, IDialogWindow
|
||||
|
||||
protected override void OnContentChanged(object oldContent, object newContent)
|
||||
{
|
||||
// InitializeComponent 设置初始内容时,窗口样式尚未加载。
|
||||
if (!isConstructorSuccessful)
|
||||
{
|
||||
base.OnContentChanged(oldContent, newContent);
|
||||
return;
|
||||
}
|
||||
this.Style = dialogWindowStyle;
|
||||
if (newContent is UserControl control)
|
||||
{
|
||||
@@ -124,9 +130,9 @@ public partial class DialogWindow : WindowBase, IDialogWindow
|
||||
|
||||
protected override void OnStyleChanged(Style oldStyle, Style newStyle)
|
||||
{
|
||||
if (newStyle.TargetType == typeof(Window))
|
||||
if (newStyle?.TargetType == typeof(Window))
|
||||
{
|
||||
if (isConstructorSuccessful)
|
||||
if (isConstructorSuccessful && !ReferenceEquals(dialogWindowStyle, newStyle))
|
||||
{
|
||||
dialogWindowStyle.Merge(newStyle);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Store.Management.Framework.Converters;
|
||||
|
||||
/// <summary>空字符串或 null 显示占位内容,其他值折叠;仅用于单向绑定。</summary>
|
||||
public sealed class StringNullOrEmpty2VisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is null || value is string { Length: 0 } ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> Binding.DoNothing;
|
||||
}
|
||||
@@ -1,51 +1,150 @@
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Services.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using Store.Management.Framework.Extensions;
|
||||
using MAX.Framework.Logger;
|
||||
using NLog;
|
||||
using Prism.Autofac;
|
||||
using Prism.Ioc;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Framework.Configs;
|
||||
using Store.Management.Framework.Extensions;
|
||||
using Store.Management.Framework.Services;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Store.Management.Framework;
|
||||
|
||||
public abstract class FrameworkApplication : PrismApplication
|
||||
{
|
||||
private Mutex? _mutex;
|
||||
private bool _ownsMutex;
|
||||
private int _fatalHandling;
|
||||
private int _shutdownRequested;
|
||||
public static bool ShuttingDown { get; private set; }
|
||||
public static bool IsAppInitialized { get; private set; }
|
||||
public static event Action<Exception>? FatalErrorRaised;
|
||||
|
||||
protected FrameworkApplication()
|
||||
{
|
||||
protected FrameworkApplication()
|
||||
{
|
||||
DispatcherUnhandledException += OnDispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
}
|
||||
|
||||
}
|
||||
/// <summary>返回 null 可允许多开;默认限制同一登录会话中的应用实例。</summary>
|
||||
public virtual string? GetMutexName() => @"Local\Store.Management";
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
//string? name = GetMutexName();
|
||||
//if (!string.IsNullOrWhiteSpace(name))
|
||||
//{
|
||||
// _mutex = new Mutex(true, name, out _ownsMutex);
|
||||
// if (!_ownsMutex)
|
||||
// {
|
||||
// MessageBox.Show("程序已在运行。", Config.AppName, MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
// Shutdown();
|
||||
// return;
|
||||
// }
|
||||
//}
|
||||
FolderConfig.Initialize();
|
||||
base.OnStartup(e);
|
||||
ApplicationLogService.Initialize();
|
||||
NLogger.Instance.Info($"软件启动:{Config.AppFullName},版本:{Config.GetFullVersionString()},会话:{Config.SessionID},启动时间:{Config.AppStartTime:O},Windows:{Config.WindowsVersion}");
|
||||
DISource.Resolver = Resolve;
|
||||
}
|
||||
catch (Exception ex) { Fatal(ex); }
|
||||
}
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
DISource.Resolver = Resolve;
|
||||
base.OnStartup(e);
|
||||
}
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
IsAppInitialized = true;
|
||||
NLogger.Instance.Info("软件初始化成功,进入主页面");
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
(Container as IContainerExtension).RegisterDialog<DialogWindow>();
|
||||
base.OnInitialized();
|
||||
}
|
||||
protected override void RegisterRequiredTypes(IContainerRegistry registry)
|
||||
{
|
||||
base.RegisterRequiredTypes(registry);
|
||||
registry.RegisterDialogWindow<Controls.DialogWindow>();
|
||||
}
|
||||
|
||||
private object Resolve(Type type, string name)
|
||||
{
|
||||
if (type == null || Container is null) return null!;
|
||||
return name != null ? Container.Resolve(type, name) : Container.Resolve(type);
|
||||
}
|
||||
|
||||
private object Resolve(Type type, string name)
|
||||
{
|
||||
if (type == null || Container is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Fatal(e.Exception);
|
||||
}
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
return Container.Resolve(type, name);
|
||||
}
|
||||
private void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Exception exception = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject?.ToString());
|
||||
// CLR 终止期间同步记录,不等待 UI 线程。
|
||||
if (e.IsTerminating) ApplicationLogService.WriteFatal(exception, "AppDomain.UnhandledException");
|
||||
else Fatal(exception);
|
||||
}
|
||||
|
||||
return Container.Resolve(type);
|
||||
}
|
||||
private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
ApplicationLogService.WriteFatal(e.Exception, "TaskScheduler.UnobservedTaskException");
|
||||
e.SetObserved();
|
||||
}
|
||||
|
||||
/// <summary>先同步保存报告,再通知订阅者并关闭应用。</summary>
|
||||
public static void Fatal(Exception exception)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
string? path = ApplicationLogService.WriteFatal(exception, "Fatal");
|
||||
if (Current is not FrameworkApplication app || Interlocked.Exchange(ref app._fatalHandling, 1) != 0) return;
|
||||
foreach (Action<Exception> handler in FatalErrorRaised?.GetInvocationList() ?? [])
|
||||
{
|
||||
try { handler(exception); }
|
||||
catch (Exception ex) { ApplicationLogService.WriteFatal(ex, "FatalErrorRaised"); }
|
||||
}
|
||||
void ShowErrorAndShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
MessageBox.Show($"软件发生致命错误,即将关闭。\n{exception.Message}\n\n日志:{path ?? "写入失败,请查看调试输出"}",
|
||||
Config.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally { TotalShutdown(-1); }
|
||||
}
|
||||
if (app.Dispatcher.CheckAccess()) ShowErrorAndShutdown();
|
||||
else if (!app.Dispatcher.HasShutdownStarted) app.Dispatcher.BeginInvoke((Action)ShowErrorAndShutdown);
|
||||
}
|
||||
|
||||
/// <summary>通过 WPF 正常退出,执行退出清理。</summary>
|
||||
public static void TotalShutdown(int exitCode = 0)
|
||||
{
|
||||
if (Current is not FrameworkApplication app || Interlocked.Exchange(ref app._shutdownRequested, 1) != 0) return;
|
||||
ShuttingDown = true;
|
||||
if (app.Dispatcher.CheckAccess()) app.Shutdown(exitCode);
|
||||
else if (!app.Dispatcher.HasShutdownStarted) app.Dispatcher.BeginInvoke((Action)(() => app.Shutdown(exitCode)));
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
ShuttingDown = true;
|
||||
try { base.OnExit(e); }
|
||||
finally
|
||||
{
|
||||
DispatcherUnhandledException -= OnDispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnDomainUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
||||
DISource.Resolver = null!;
|
||||
try { if (_ownsMutex) _mutex?.ReleaseMutex(); }
|
||||
finally
|
||||
{
|
||||
_mutex?.Dispose();
|
||||
try { NLogger.Instance.Info($"软件关闭,退出码:{e.ApplicationExitCode}"); }
|
||||
finally { LogManager.Shutdown(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib">
|
||||
<sys:Double x:Key="DefaultControlHeight">28</sys:Double>
|
||||
<sys:Double x:Key="SmallControlHeight">10</sys:Double>
|
||||
<Thickness x:Key="DefaultControlPadding">10,5</Thickness>
|
||||
<Thickness x:Key="DefaultInputPadding">8,0</Thickness>
|
||||
<CornerRadius x:Key="DefaultCornerRadius">2</CornerRadius>
|
||||
<sys:Double x:Key="{x:Static SystemParameters.VerticalScrollBarButtonHeightKey}">30</sys:Double>
|
||||
<sys:Double x:Key="LargeFontSize">24</sys:Double>
|
||||
<sys:Double x:Key="HeadFontSize">20</sys:Double>
|
||||
<sys:Double x:Key="SubHeadFontSize">16</sys:Double>
|
||||
<sys:Double x:Key="TextFontSize">12</sys:Double>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,60 @@
|
||||
using MAX.Framework.Logger;
|
||||
using NLog;
|
||||
using NLog.Config;
|
||||
using NLog.Targets;
|
||||
using Store.Management.Framework.Configs;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Store.Management.Framework.Services;
|
||||
|
||||
/// <summary>默认运行日志与不依赖容器的同步崩溃报告。</summary>
|
||||
public static class ApplicationLogService
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
// 在加载文件配置前注入目录,配置自动重载时仍可读取。
|
||||
GlobalDiagnosticsContext.Set("LoggingFolder", FolderConfig.LoggingFolder);
|
||||
// 尊重宿主提供的 NLog 配置。
|
||||
if (LogManager.Configuration != null) return;
|
||||
var target = new FileTarget("application")
|
||||
{
|
||||
FileName = Path.Combine(FolderConfig.LoggingFolder, "${shortdate}.log"),
|
||||
Layout = "${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}",
|
||||
Encoding = Encoding.UTF8,
|
||||
MaxArchiveDays = 30
|
||||
};
|
||||
var configuration = new LoggingConfiguration();
|
||||
configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, target);
|
||||
LogManager.Configuration = configuration;
|
||||
}
|
||||
|
||||
/// <returns>报告路径;所有目录均写入失败时返回 null。</returns>
|
||||
public static string? WriteFatal(Exception exception, string source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(exception);
|
||||
string id = $"{DateTime.UtcNow:yyyyMMdd-HHmmssfff}-{Guid.NewGuid():N}";
|
||||
string report = $"Id: {id}\nTime: {DateTimeOffset.Now:O}\nSource: {source}\nApp: {Config.AppFullName}\nVersion: {Config.Version}\nSession: {Config.SessionID}\nStarted: {Config.AppStartTime:O}\nWindows: {Config.WindowsVersion}\n{exception}";
|
||||
Debug.WriteLine(report);
|
||||
string? savedPath = null;
|
||||
foreach (string directory in new[] { FolderConfig.LoggingFolder, Path.Combine(Path.GetTempPath(), FolderConfig.AppName, "Logs") })
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
savedPath = Path.Combine(directory, $"fatal-{id}.log");
|
||||
File.WriteAllText(savedPath, report, Encoding.UTF8);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex) { savedPath = null; Debug.WriteLine(ex); }
|
||||
}
|
||||
try
|
||||
{
|
||||
LogManager.GetLogger("GlobalLogger").Fatal(exception, "{0}: {1}; Report: {2}", source, id, savedPath);
|
||||
LogManager.Flush(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
catch (Exception ex) { Debug.WriteLine(ex); }
|
||||
return savedPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Prism.Mvvm;
|
||||
using Store.Management.Framework.Configs;
|
||||
|
||||
namespace Store.Management.Framework.Services;
|
||||
|
||||
public interface ISettingsServiceBase<T> where T : class
|
||||
{
|
||||
T Model { get; }
|
||||
string FilePath { get; }
|
||||
T GetDefaultModel();
|
||||
void LoadSettings();
|
||||
Task LoadSettingsAsync();
|
||||
void SaveSettings();
|
||||
Task SaveSettingsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 配置服务。缺失文件保留当前配置,加载失败保留旧模型并抛出异常。
|
||||
/// 保存通过同目录临时文件替换目标;模型变更应在所属 UI 线程调用。
|
||||
/// </summary>
|
||||
public abstract class JsonSettingsServiceBase<T> : BindableBase, ISettingsServiceBase<T> where T : class
|
||||
{
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
private T? _model;
|
||||
/// <summary>默认按模型类型名保存到用户配置目录,派生类可覆盖文件名或完整路径。</summary>
|
||||
public virtual string FilePath => FolderConfig.GetSettingsFilePath(typeof(T).Name + ".json");
|
||||
public abstract T GetDefaultModel();
|
||||
public T Model
|
||||
{
|
||||
get => _model ??= GetDefaultModel() ?? throw new InvalidOperationException("默认配置不能为空。");
|
||||
protected set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
SetProperty(ref _model, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadSettings()
|
||||
{
|
||||
T? loaded = LoadModelAsync().GetAwaiter().GetResult();
|
||||
if (loaded is not null) Model = loaded;
|
||||
}
|
||||
public void SaveSettings() => SaveSettingsAsync().GetAwaiter().GetResult();
|
||||
|
||||
public async Task LoadSettingsAsync()
|
||||
{
|
||||
T? loaded = await LoadModelAsync();
|
||||
if (loaded is not null) Model = loaded;
|
||||
}
|
||||
|
||||
private async Task<T?> LoadModelAsync()
|
||||
{
|
||||
T? loaded = null;
|
||||
await _gate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
string path = GetFullPath();
|
||||
try
|
||||
{
|
||||
string json = await File.ReadAllTextAsync(path).ConfigureAwait(false);
|
||||
loaded = JsonConvert.DeserializeObject<T>(json)
|
||||
?? throw new JsonSerializationException($"配置文件不能为 null:{path}");
|
||||
}
|
||||
catch (FileNotFoundException) { }
|
||||
catch (DirectoryNotFoundException) { }
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public async Task SaveSettingsAsync()
|
||||
{
|
||||
// 在首次等待前读取模型,避免在后台线程初始化默认模型。
|
||||
string json = JsonConvert.SerializeObject(Model, Formatting.Indented);
|
||||
await _gate.WaitAsync().ConfigureAwait(false);
|
||||
string? temporaryPath = null;
|
||||
try
|
||||
{
|
||||
string path = GetFullPath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
temporaryPath = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
|
||||
await File.WriteAllTextAsync(temporaryPath, json).ConfigureAwait(false);
|
||||
File.Move(temporaryPath, path, overwrite: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { if (temporaryPath is not null && File.Exists(temporaryPath)) File.Delete(temporaryPath); }
|
||||
finally { _gate.Release(); }
|
||||
}
|
||||
}
|
||||
|
||||
private string GetFullPath()
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(FilePath);
|
||||
return Path.GetFullPath(FilePath);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HandyControl" Version="3.6.0.9" />
|
||||
<Content Remove="C:\Users\chaih\.nuget\packages\handycontrol\3.6.2.1\contentFiles\any\net8.0-windows7.0\LangProvider.tt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HandyControl" Version="3.6.2.1" />
|
||||
<ProjectReference Include="..\Core\MAX.Core\MAX.Core.csproj" />
|
||||
<ProjectReference Include="..\Core\MAX.Framework\MAX.Framework.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Store.Management.Framework.Utilities;
|
||||
|
||||
/// <summary>按订阅顺序等待异步处理器;异常向调用者传播并停止后续处理。</summary>
|
||||
public sealed class AsyncEvent<TEventArgs> where TEventArgs : EventArgs
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly List<Func<object?, TEventArgs, Task>> _handlers = [];
|
||||
|
||||
public void Subscribe(Func<object?, TEventArgs, Task> callback)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(callback);
|
||||
lock (_gate) { _handlers.Add(callback); }
|
||||
}
|
||||
|
||||
public void Unsubscribe(Func<object?, TEventArgs, Task> callback)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(callback);
|
||||
lock (_gate)
|
||||
{
|
||||
int index = _handlers.LastIndexOf(callback);
|
||||
if (index >= 0) _handlers.RemoveAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(object? sender, TEventArgs eventArgs)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(eventArgs);
|
||||
Func<object?, TEventArgs, Task>[] snapshot;
|
||||
lock (_gate) { snapshot = _handlers.ToArray(); }
|
||||
foreach (var callback in snapshot) await callback(sender, eventArgs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Store.Management.Framework.Utilities;
|
||||
|
||||
/// <summary>在释放时执行一次清理操作,可安全地重复或并发释放。</summary>
|
||||
public sealed class DisposeAction : IDisposable
|
||||
{
|
||||
private Action? _action;
|
||||
|
||||
public DisposeAction(Action action)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(action);
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public void Dispose() => Interlocked.Exchange(ref _action, null)?.Invoke();
|
||||
}
|
||||
Reference in new issue
Block a user