From 30a6a5ea246cff9ee71f9552398bf28c8b97b7f5 Mon Sep 17 00:00:00 2001 From: chfei Date: Fri, 13 Nov 2026 10:16:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0MessageBoxService=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/DataGridEx/DataGridEx.xaml | 9 +- .../FrameworkApplication.cs | 265 +++++++++--------- .../Interfaces/IMessageBoxService.cs | 15 - .../MessageBoxService/IMessageBoxService.cs | 27 ++ .../MessageBoxEnumsConverter.cs | 99 +++++++ .../MessageBoxService/MessageBoxService.cs | 124 ++++++++ .../MessageBoxServiceExtensions.cs | 37 +++ .../MessageBoxService/MessageButton.cs | 7 + .../Services/MessageBoxService/MessageIcon.cs | 8 + .../MessageBoxService/MessageResult.cs | 7 + .../Services/UserService.cs | 29 ++ .../ViewModels/UserEditorDialogViewModel.cs | 8 +- .../ViewModels/UserManagementPageViewModel.cs | 10 +- .../Views/DataGridTestView.xaml | 3 + .../Program.cs | 22 +- 15 files changed, 517 insertions(+), 153 deletions(-) delete mode 100644 Source/Store.Management.Framework/Interfaces/IMessageBoxService.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/IMessageBoxService.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxEnumsConverter.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxService.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxServiceExtensions.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageButton.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageIcon.cs create mode 100644 Source/Store.Management.Framework/Services/MessageBoxService/MessageResult.cs create mode 100644 Source/Store.Management.Framework/Services/UserService.cs diff --git a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml index bbfa67d..19565a5 100644 --- a/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml +++ b/Source/Store.Management.Framework/Controls/DataGridEx/DataGridEx.xaml @@ -4,8 +4,8 @@ xmlns:ext="clr-namespace:Store.Management.Framework.Controls" xmlns:f="clr-namespace:Store.Management.Framework.Controls.Attach;assembly=Store.Management.Framework" xmlns:local="clr-namespace:Store.Management.Framework.Controls" - xmlns:wpf="MAX.WPF" - xmlns:sys="clr-namespace:System;assembly=mscorlib"> + xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:wpf="MAX.WPF"> 12 @@ -498,6 +498,11 @@ + + + 50,100,500,1000,2000 + + diff --git a/Source/Store.Management.Framework/FrameworkApplication.cs b/Source/Store.Management.Framework/FrameworkApplication.cs index e0c5f91..4d548b1 100644 --- a/Source/Store.Management.Framework/FrameworkApplication.cs +++ b/Source/Store.Management.Framework/FrameworkApplication.cs @@ -1,3 +1,4 @@ +using MAX.Common.Helpers; using MAX.Framework.Logger; using NLog; using Prism.Autofac; @@ -15,141 +16,155 @@ 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? FatalErrorRaised; + 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? FatalErrorRaised; + private IStatusBarService StatusBarService; + private IMessageBoxService MessageBoxService; + private static bool FatalShutdown = false; + protected FrameworkApplication() + { + DispatcherUnhandledException += OnDispatcherUnhandledException; + AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + } - protected FrameworkApplication() - { - DispatcherUnhandledException += OnDispatcherUnhandledException; - AppDomain.CurrentDomain.UnhandledException += OnDomainUnhandledException; - TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; - } + /// 返回 null 可允许多开;默认限制同一登录会话中的应用实例。 + public virtual string? GetMutexName() => @"Local\Store.Management"; - /// 返回 null 可允许多开;默认限制同一登录会话中的应用实例。 - 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) - { - 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 OnInitialized() + { + base.OnInitialized(); + IsAppInitialized = true; + NLogger.Instance.Info("软件初始化成功,进入主页面"); + StatusBarService = Container?.Resolve(); + MessageBoxService = Container?.Resolve(); + } - protected override void OnInitialized() - { - base.OnInitialized(); - IsAppInitialized = true; - NLogger.Instance.Info("软件初始化成功,进入主页面"); - } + protected override void RegisterRequiredTypes(IContainerRegistry registry) + { + base.RegisterRequiredTypes(registry); + registry.RegisterDialogWindow(); + } - protected override void RegisterRequiredTypes(IContainerRegistry registry) - { - base.RegisterRequiredTypes(registry); - registry.RegisterDialogWindow(); - } + 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!; - return name != null ? Container.Resolve(type, name) : Container.Resolve(type); - } + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + StatusBarService.Report(e.Exception.Message, StatusLevel.Error); + Fatal(e.Exception); - private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) - { - e.Handled = true; - Fatal(e.Exception); - } + } - 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); - } + private void OnDomainUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + Exception exception = e.ExceptionObject as Exception ?? new Exception(e.ExceptionObject?.ToString()); + StatusBarService.Report(exception.Message, StatusLevel.Error); + // CLR 终止期间同步记录,不等待 UI 线程。 + if (e.IsTerminating) ApplicationLogService.WriteFatal(exception, "AppDomain.UnhandledException"); + else Fatal(exception); + } - private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - ApplicationLogService.WriteFatal(e.Exception, "TaskScheduler.UnobservedTaskException"); - e.SetObserved(); - } + private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + ApplicationLogService.WriteFatal(e.Exception, "TaskScheduler.UnobservedTaskException"); + e.SetObserved(); + } - /// 先同步保存报告,再通知订阅者并关闭应用。 - 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 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); - } + /// 先同步保存报告,再通知订阅者并关闭应用。 + public 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 handler in FatalErrorRaised?.GetInvocationList() ?? []) + { + try { handler(exception); } + catch (Exception ex) { ApplicationLogService.WriteFatal(ex, "FatalErrorRaised"); } + } + void ShowErrorAndShutdown() + { + if (FatalShutdown) + { + try + { + MessageBox.Show($"软件发生致命错误,即将关闭。\n{exception.Message}\n\n日志:{path ?? "写入失败,请查看调试输出"}", + Config.AppName, MessageBoxButton.OK, MessageBoxImage.Error); + } + finally { TotalShutdown(-1); } + } + else + { + MessageBoxService.ShowError(exception.Message); + } + } + if (app.Dispatcher.CheckAccess()) ShowErrorAndShutdown(); + else if (!app.Dispatcher.HasShutdownStarted) app.Dispatcher.BeginInvoke((Action)ShowErrorAndShutdown); + } - /// 通过 WPF 正常退出,执行退出清理。 - 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))); - } + /// 通过 WPF 正常退出,执行退出清理。 + 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 - { - try { Container?.Resolve().Dispose(); } - finally { 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(); } - } - } - } + protected override void OnExit(ExitEventArgs e) + { + ShuttingDown = true; + try + { + try { Container?.Resolve().Dispose(); } + finally { 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(); } + } + } + } } diff --git a/Source/Store.Management.Framework/Interfaces/IMessageBoxService.cs b/Source/Store.Management.Framework/Interfaces/IMessageBoxService.cs deleted file mode 100644 index 03a67c3..0000000 --- a/Source/Store.Management.Framework/Interfaces/IMessageBoxService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Windows; -using System.ComponentModel; - -namespace Store.Management.Framework.Interfaces -{ - - public interface IMessageBoxService - { - - //MessageResult Show(string messageBoxText, string caption, MessageBoxButton button, MessageIcon icon, MessageResult defaultResult); - } -} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/IMessageBoxService.cs b/Source/Store.Management.Framework/Services/MessageBoxService/IMessageBoxService.cs new file mode 100644 index 0000000..54d26a6 --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/IMessageBoxService.cs @@ -0,0 +1,27 @@ +using MAX.Core.Commons; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Store.Management.Framework +{ + public interface IMessageBoxService + { + MessageResult Show(string messageBoxText, string caption, MessageButton button, MessageIcon icon, MessageResult defaultResult); + + + void ShowInformation(string content); + void ShowWarning(string content); + void ShowError(string content); + void ShowError(string content, Exception exception); + bool ShowYesNoQuestion(string content); + bool ShowYesNoWarning(string content); + bool ShowYesNoError(string content); + bool ShowOkCancelQuestion(string content); + bool ShowOkCancelWarning(string content); + bool ShowOkCancelError(string content); + MessageResult ShowYesNoCancelQuestion(string content); + MessageResult ShowYesNoCancelWarning(string content); + MessageResult ShowYesNoCancelError(string content); + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxEnumsConverter.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxEnumsConverter.cs new file mode 100644 index 0000000..07e3f33 --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxEnumsConverter.cs @@ -0,0 +1,99 @@ +using System.ComponentModel; +using System.Windows; + +namespace Store.Management.Framework +{ + public static class MessageBoxEnumsConverter + { + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static bool? ToBool(this MessageBoxResult result) + { + switch (result) + { + case MessageBoxResult.Cancel: return null; + case MessageBoxResult.No: return false; + default: return true; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static bool? ToBool(this MessageResult result) + { + switch (result) + { + case MessageResult.Cancel: return null; + case MessageResult.No: return false; + default: return true; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageBoxResult ToMessageBoxResult(this MessageResult result) + { + switch (result) + { + case MessageResult.Cancel: return MessageBoxResult.Cancel; + case MessageResult.No: return MessageBoxResult.No; + case MessageResult.Yes: return MessageBoxResult.Yes; + case MessageResult.OK: return MessageBoxResult.OK; + default: return MessageBoxResult.None; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageResult ToMessageResult(this MessageBoxResult result) + { + switch (result) + { + case MessageBoxResult.Cancel: return MessageResult.Cancel; + case MessageBoxResult.No: return MessageResult.No; + case MessageBoxResult.Yes: return MessageResult.Yes; + case MessageBoxResult.OK: return MessageResult.OK; + default: return MessageResult.None; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageBoxButton ToMessageBoxButton(this MessageButton button) + { + switch (button) + { + case MessageButton.OKCancel: return MessageBoxButton.OKCancel; + case MessageButton.YesNo: return MessageBoxButton.YesNo; + case MessageButton.YesNoCancel: return MessageBoxButton.YesNoCancel; + default: return MessageBoxButton.OK; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageButton ToMessageButton(this MessageBoxButton button) + { + switch (button) + { + case MessageBoxButton.OKCancel: return MessageButton.OKCancel; + case MessageBoxButton.YesNo: return MessageButton.YesNo; + case MessageBoxButton.YesNoCancel: return MessageButton.YesNoCancel; + default: return MessageButton.OK; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageBoxImage ToMessageBoxImage(this MessageIcon icon) + { + switch (icon) + { + case MessageIcon.Error: return MessageBoxImage.Error; + case MessageIcon.Information: return MessageBoxImage.Information; + case MessageIcon.Question: return MessageBoxImage.Question; + case MessageIcon.Warning: return MessageBoxImage.Warning; + default: return MessageBoxImage.None; + } + } + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + public static MessageIcon ToMessageIcon(this MessageBoxImage icon) + { + switch (icon) + { + case MessageBoxImage.Error: return MessageIcon.Error; + case MessageBoxImage.Information: return MessageIcon.Information; + case MessageBoxImage.Question: return MessageIcon.Question; + case MessageBoxImage.Warning: return MessageIcon.Warning; + default: return MessageIcon.None; + } + } + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxService.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxService.cs new file mode 100644 index 0000000..128b946 --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxService.cs @@ -0,0 +1,124 @@ +using MAX.Core.DependencyInjection; +using Store.Management.Framework.Configs; +using System.Windows; +using MessageBox = HandyControl.Controls.MessageBox; + +namespace Store.Management.Framework +{ + public class MessageBoxService : IMessageBoxService, ISingletonDependency + { + protected readonly Guid id = Guid.NewGuid(); + + public string MessageTitle { get; set; } = string.Empty; + + public MessageBoxService() + { + MessageTitle = AppConfig.AppDisplayName; + } + + public virtual MessageResult Show(string messageBoxText, string caption, MessageButton button, MessageIcon icon, MessageResult defaultResult) + { + Window? window = Application.Current?.Windows.OfType().FirstOrDefault(w => w.IsActive); + if (window is null) + { + return MessageBox.Show( + messageBoxText: messageBoxText, + caption: caption, + button: button.ToMessageBoxButton(), + icon: icon.ToMessageBoxImage(), + defaultResult: defaultResult.ToMessageBoxResult() + ).ToMessageResult(); + } + else + { + return MessageBox.Show( + owner: window, + messageBoxText: messageBoxText, + caption: caption, + button: button.ToMessageBoxButton(), + icon: icon.ToMessageBoxImage(), + defaultResult: defaultResult.ToMessageBoxResult() + ).ToMessageResult(); + } + } + + public void ShowInformation(string content) + { + this.ShowMessage(content, MessageTitle, MessageButton.OK, MessageIcon.Information, MessageResult.OK); + } + + public void ShowWarning(string content) + { + this.ShowMessage(content, MessageTitle, MessageButton.OK, MessageIcon.Warning, MessageResult.OK); + } + + public void ShowError(string content, Exception exception) + { + if (exception is null) + { + ShowError(content); + } + else + { + this.ShowMessage(content + "\n" + exception.Message, MessageTitle, MessageButton.OK, MessageIcon.Error, MessageResult.OK); + } + } + + public void ShowError(string content) + { + this.ShowMessage(content, MessageTitle, MessageButton.OK, MessageIcon.Error, MessageResult.OK); + } + + public bool ShowYesNoQuestion(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.YesNo, MessageIcon.Question, MessageResult.No); + return result == MessageResult.Yes; + } + + public bool ShowYesNoWarning(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.YesNo, MessageIcon.Warning, MessageResult.No); + return result == MessageResult.Yes; + } + + public bool ShowYesNoError(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.YesNo, MessageIcon.Error, MessageResult.No); + return result == MessageResult.Yes; + } + + public bool ShowOkCancelQuestion(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.OKCancel, MessageIcon.Question, MessageResult.OK); + return result == MessageResult.OK; + } + + public bool ShowOkCancelWarning(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.OKCancel, MessageIcon.Warning, MessageResult.OK); + return result == MessageResult.OK; + } + + public bool ShowOkCancelError(string content) + { + MessageResult result = this.ShowMessage(content, MessageTitle, MessageButton.OKCancel, MessageIcon.Error, MessageResult.OK); + return result == MessageResult.OK; + } + + public MessageResult ShowYesNoCancelQuestion(string content) + { + return this.ShowMessage(content, MessageTitle, MessageButton.YesNoCancel, MessageIcon.Question, MessageResult.Cancel); + } + + public MessageResult ShowYesNoCancelWarning(string content) + { + return this.ShowMessage(content, MessageTitle, MessageButton.YesNoCancel, MessageIcon.Warning, MessageResult.Cancel); + } + + public MessageResult ShowYesNoCancelError(string content) + { + return this.ShowMessage(content, MessageTitle, MessageButton.YesNoCancel, MessageIcon.Error, MessageResult.Cancel); + } + + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxServiceExtensions.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxServiceExtensions.cs new file mode 100644 index 0000000..90108ed --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageBoxServiceExtensions.cs @@ -0,0 +1,37 @@ +namespace Store.Management.Framework +{ + public static class MessageBoxServiceExtensions + { + extension(IMessageBoxService service) + { + public MessageResult ShowMessage(string messageBoxText, string caption, MessageButton button, MessageIcon icon, MessageResult defaultResult) + { + VerifyService(service); + return service.Show(messageBoxText, caption, button, icon, defaultResult); + } + public bool? ShowMessage(string messageBoxText) + { + return service.ShowMessage(messageBoxText, string.Empty); + } + public bool? ShowMessage(string messageBoxText, string caption) + { + return service.ShowMessage(messageBoxText, caption, MessageButton.OK).ToBool(); + } + public MessageResult ShowMessage(string messageBoxText, string caption, MessageButton button) + { + return service.ShowMessage(messageBoxText, caption, button, MessageIcon.None); + } + public MessageResult ShowMessage(string messageBoxText, string caption, MessageButton button, MessageIcon icon) + { + VerifyService(service); + return service.Show(messageBoxText, caption, button, icon, MessageResult.None); + } + } + + static void VerifyService(IMessageBoxService service) + { + if (service == null) + throw new ArgumentNullException(nameof(service)); + } + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageButton.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageButton.cs new file mode 100644 index 0000000..1417a41 --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageButton.cs @@ -0,0 +1,7 @@ +namespace Store.Management.Framework +{ + public enum MessageButton + { + OK, OKCancel, YesNoCancel, YesNo, + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageIcon.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageIcon.cs new file mode 100644 index 0000000..a9b8b71 --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageIcon.cs @@ -0,0 +1,8 @@ +namespace Store.Management.Framework +{ + public enum MessageIcon + { + None, Error, Question, Warning, Information, + Hand = Error, Stop = Error, Exclamation = Warning, Asterisk = Information + } +} diff --git a/Source/Store.Management.Framework/Services/MessageBoxService/MessageResult.cs b/Source/Store.Management.Framework/Services/MessageBoxService/MessageResult.cs new file mode 100644 index 0000000..e1df9cb --- /dev/null +++ b/Source/Store.Management.Framework/Services/MessageBoxService/MessageResult.cs @@ -0,0 +1,7 @@ +namespace Store.Management.Framework +{ + public enum MessageResult + { + None, OK, Cancel, Yes, No, + } +} diff --git a/Source/Store.Management.Framework/Services/UserService.cs b/Source/Store.Management.Framework/Services/UserService.cs new file mode 100644 index 0000000..51d7b95 --- /dev/null +++ b/Source/Store.Management.Framework/Services/UserService.cs @@ -0,0 +1,29 @@ +using MAX.Core.DependencyInjection; +using Store.Management.Api; +using Store.Management.Api.Models; +using Store.Management.Api.Users; + +namespace Store.Management.Framework.Services; + +public interface IUserService +{ + Task> SearchAsync(UserSearchRequest request, CancellationToken cancellationToken = default); + Task CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default); + Task UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); +} + +internal sealed class UserService(StoreApiClient client) : IUserService, ISingletonDependency +{ + public Task> SearchAsync(UserSearchRequest request, CancellationToken cancellationToken = default) + => client.Users.SearchAsync(request, cancellationToken); + + public Task CreateAsync(UserCreateRequest request, CancellationToken cancellationToken = default) + => client.Users.CreateAsync(request, cancellationToken); + + public Task UpdateAsync(UserUpdateRequest request, CancellationToken cancellationToken = default) + => client.Users.UpdateAsync(request, cancellationToken); + + public Task DeleteAsync(int id, CancellationToken cancellationToken = default) + => client.Users.DeleteAsync(id, cancellationToken); +} diff --git a/Source/Store.Management/ViewModels/UserEditorDialogViewModel.cs b/Source/Store.Management/ViewModels/UserEditorDialogViewModel.cs index 6452a10..d3eb287 100644 --- a/Source/Store.Management/ViewModels/UserEditorDialogViewModel.cs +++ b/Source/Store.Management/ViewModels/UserEditorDialogViewModel.cs @@ -1,15 +1,15 @@ using Prism.Commands; using Prism.Services.Dialogs; -using Store.Management.Api; using Store.Management.Api.Transport; using Store.Management.Api.Users; using Store.Management.Framework.Controls; +using Store.Management.Framework.Services; using System.ComponentModel.DataAnnotations; using System.Windows.Input; namespace Store.Management.ViewModels; -public sealed class UserEditorDialogViewModel(StoreApiClient client) : ConfirmDialogViewModel +public sealed class UserEditorDialogViewModel(IUserService userService) : ConfirmDialogViewModel { public const int UserNameLimit = 20; public const int PasswordLimit = 128; @@ -65,11 +65,11 @@ public sealed class UserEditorDialogViewModel(StoreApiClient client) : ConfirmDi { if (Draft is UserCreateRequest newUser) { - await client.Users.CreateAsync(newUser); + await userService.CreateAsync(newUser); } else { - await client.Users.UpdateAsync((UserUpdateRequest)Draft); + await userService.UpdateAsync((UserUpdateRequest)Draft); } } catch (ApiException ex) diff --git a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs index abe95fb..6130c30 100644 --- a/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs +++ b/Source/Store.Management/ViewModels/UserManagementPageViewModel.cs @@ -1,7 +1,6 @@ using Prism.Commands; using Prism.Mvvm; using Prism.Services.Dialogs; -using Store.Management.Api; using Store.Management.Api.Models; using Store.Management.Api.Transport; using Store.Management.Api.Users; @@ -13,7 +12,7 @@ using System.Windows.Controls.Primitives; namespace Store.Management.ViewModels; -public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogService dialogs, IStatusBarService statusBar) : BindableBase +public sealed class UserManagementPageViewModel(IUserService userService, IDialogService dialogs, IStatusBarService statusBar) : BindableBase { public const int KeywordLimit = 100; private string appliedKeyword = ""; @@ -59,13 +58,13 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe { statusBar.Report("正在查询用户…"); UserSearchRequest request = new() { Keyword = keyword, Page = page, PageSize = size }; - PageResult result = await client.Users.SearchAsync(request); + PageResult result = await userService.SearchAsync(request); // 删除末页最后一条记录后,按服务端总数回到有效页。 int lastPage = (int)Math.Clamp((result.Total - 1) / size + 1, 1, page); if (page > lastPage) { request.Page = lastPage; - result = await client.Users.SearchAsync(request); + result = await userService.SearchAsync(request); } appliedKeyword = keyword; pageSize = size; @@ -86,6 +85,7 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe pageSize = size; statusBar.Report(ex.Message, StatusLevel.Error); } + finally { CanInteract = true; @@ -115,7 +115,7 @@ public sealed class UserManagementPageViewModel(StoreApiClient client, IDialogSe try { statusBar.Report("正在删除用户…"); - await client.Users.DeleteAsync(user.Id); + await userService.DeleteAsync(user.Id); } catch (ApiException ex) { diff --git a/Source/Store.Management/Views/DataGridTestView.xaml b/Source/Store.Management/Views/DataGridTestView.xaml index 1c93a2e..5a28be1 100644 --- a/Source/Store.Management/Views/DataGridTestView.xaml +++ b/Source/Store.Management/Views/DataGridTestView.xaml @@ -127,6 +127,9 @@ ShowRowNumber="True" ShowToolbar="True" TotalItemCount="{Binding TotalCount, Mode=OneWay}"> + diff --git a/Tests/Store.Management.App.SmokeTests/Program.cs b/Tests/Store.Management.App.SmokeTests/Program.cs index f5d353e..7e6aec6 100644 --- a/Tests/Store.Management.App.SmokeTests/Program.cs +++ b/Tests/Store.Management.App.SmokeTests/Program.cs @@ -76,6 +76,11 @@ class Program TextBlock text = (TextBlock)shell.FindName("StatusText"); ProgressBar progress = (ProgressBar)shell.FindName("StatusProgress"); Check(text.Text == "就绪" && progress.Visibility == Visibility.Collapsed, "初始状态"); + _ = app.Dispatcher.BeginInvoke(new Action(() => + throw new System.ComponentModel.DataAnnotations.ValidationException("分页参数无效"))); + await Dispatcher.Yield(DispatcherPriority.ApplicationIdle); + Check(shell.IsVisible && text.Text == "分页参数无效" && status.Current.Level == StatusLevel.Error, + "Dispatcher 异常后主窗口继续运行并显示错误"); foreach ((StatusLevel level, string brush, int value) in new[] { (StatusLevel.Info, "PrimaryTextBrush", 0), (StatusLevel.Warning, "WarningBrush", 50), (StatusLevel.Error, "DangerBrush", 75), (StatusLevel.Success, "SuccessBrush", 100) }) @@ -90,7 +95,12 @@ class Program Check(text.Text == "完成" && progress.Visibility == Visibility.Collapsed, "纯文本状态隐藏进度"); using var usersClient = new StoreApiClient(new ApiClientOptions { BaseUrl = new Uri("http://localhost:14312/api/v1/") }, httpClient); usersClient.Session.Clear(); - var usersVm = new UserManagementPageViewModel(usersClient, app.Container.Resolve(), status); + var usersExtension = new Prism.Autofac.AutofacContainerExtension(); + usersExtension.RegisterInstance(typeof(StoreApiClient), usersClient); + usersExtension.FinalizeExtension(); + using var usersContainer = usersExtension.Instance; + var userService = usersContainer.Resolve(); + var usersVm = new UserManagementPageViewModel(userService, app.Container.Resolve(), status); await usersVm.SearchCommand.Execute(); Check(status.Current.Level == StatusLevel.Error && usersVm.CanInteract, "查询失败后显示错误并允许重试"); await usersClient.Auth.LoginAsync(new() { UserName = "admin", Password = "320BB1D43EC042096A83DBAC6DC9294A" }); @@ -100,6 +110,7 @@ class Program Check(usersVm.CanInteract && usersVm.Total == 51 && status.Current.Level == StatusLevel.Success, "重试查询成功"); await usersVm.PageCommand.Execute(new DataGridPageRequest(2, 50)); Check(usersVm.Page == 2 && usersVm.CanInteract && status.Current.Level == StatusLevel.Success, "分页查询成功"); + await userService.DeleteAsync(1); Console.WriteLine("PASS: status bar bindings, colors and background progress reports."); return; } @@ -230,6 +241,13 @@ sealed class LoginHandler : HttpMessageHandler Content = new StringContent("""{"code":200,"data":{"total":51,"items":[{"id":1,"userName":"admin"}]}}""", Encoding.UTF8, "application/json") }; } + if (request.Method == HttpMethod.Post && request.RequestUri?.PathAndQuery == "/api/v1/user/DelUser?id=1") + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("""{"code":200}""", Encoding.UTF8, "application/json") + }; + } if (request.RequestUri?.AbsoluteUri != "http://localhost:14312/api/v1/Auth/Login") throw new Exception("登录路由错误"); var body = System.Text.Json.JsonDocument.Parse(await request.Content!.ReadAsStringAsync(cancellationToken)); @@ -258,4 +276,4 @@ sealed class TestApp(StoreApiClient client) : App base.RegisterTypes(containerRegistry); containerRegistry.RegisterInstance(client); } -} \ No newline at end of file +}