增加测试
This commit is contained in:
1 parent
6d4629e4ac
commit
497e9f1c34
14 files changed
+700
-74
No files matched your search
@@ -7,6 +7,7 @@ namespace Store.Management.Api.Authentication;
|
||||
public sealed class LoginRequest
|
||||
{
|
||||
[Required] public string UserName { get; set; } = "";
|
||||
/// <summary>服务端要求的 UTF-8 密码的 32 位大写 MD5,由调用方计算。</summary>
|
||||
[Required] public string Password { get; set; } = "";
|
||||
[StringLength(100)] public string? DeviceId { get; set; }
|
||||
[StringLength(100)] public string? DeviceName { get; set; }
|
||||
@@ -39,27 +40,38 @@ internal sealed class AuthApi(ApiTransport transport, TokenSession session) : IA
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var result = await transport.GetRequiredAsync<LoginResult>("Auth/Login", Method.Post, request,
|
||||
LoginResult result = await transport.GetRequiredAsync<LoginResult>("Auth/Login", Method.Post, request,
|
||||
authenticated: false, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
if (string.IsNullOrWhiteSpace(result.Token) || result.Token.Contains('\r') || result.Token.Contains('\n') ||
|
||||
!string.Equals(result.TokenType, "Bearer", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.InvalidResponse, "登录响应缺少有效的 Bearer Token。");
|
||||
}
|
||||
session.Set(result.Token);
|
||||
return result;
|
||||
}
|
||||
finally { _gate.Release(); }
|
||||
finally
|
||||
{
|
||||
_gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LogoutAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
var token = session.Token;
|
||||
string? token = session.Token;
|
||||
try
|
||||
{
|
||||
if (token is not null)
|
||||
{
|
||||
await transport.SendAsync<object>("Auth/Logout", Method.Post, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
session.ClearIfCurrent(token);
|
||||
_gate.Release();
|
||||
}
|
||||
finally { session.ClearIfCurrent(token); _gate.Release(); }
|
||||
}
|
||||
|
||||
public Task<CurrentUser> MeAsync(CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Store Management API 客户端
|
||||
|
||||
独立的 net10.0 类库,使用 RestSharp 112.1.0 连接 StoreWeb。WPF 项目已引用该类库。每个服务地址及用户会话创建一个 StoreApiClient,建议在应用容器注册为单例,退出应用时释放。
|
||||
独立的 API 类库(当前继承仓库的 net8.0-windows 目标框架),使用 RestSharp 112.1.0 连接 StoreWeb。WPF 项目已引用该类库。每个服务地址及用户会话创建一个 StoreApiClient,建议在应用容器注册为单例,退出应用时释放。
|
||||
|
||||
## 调用
|
||||
|
||||
@@ -21,7 +21,7 @@ try
|
||||
await api.Auth.LoginAsync(new LoginRequest
|
||||
{
|
||||
UserName = userName,
|
||||
Password = password,
|
||||
Password = passwordMd5, // UTF-8 密码的 32 位大写 MD5,保留原密码首尾空格
|
||||
DeviceId = deviceId, // 可选;稳定设备标识由宿主应用管理
|
||||
DeviceName = Environment.MachineName
|
||||
});
|
||||
@@ -44,7 +44,7 @@ catch (ApiException ex)
|
||||
}
|
||||
```
|
||||
|
||||
示例中的账号、密码、设备标识来自调用方输入。异步方法均支持 CancellationToken;主动取消抛出 OperationCanceledException,本地输入校验抛出 ValidationException。
|
||||
示例中的账号、密码摘要、设备标识由调用方提供;登录窗口已负责计算密码摘要,API 层不重复计算。异步方法均支持 CancellationToken;主动取消抛出 OperationCanceledException,本地输入校验抛出 ValidationException。
|
||||
|
||||
## 文件与职责
|
||||
|
||||
@@ -58,13 +58,13 @@ catch (ApiException ex)
|
||||
|
||||
## 协议与扩展
|
||||
|
||||
对齐 StoreWeb 的 AuthController、TokenResult、UnifyResultFilter 和用户管理控制器。成功码为 200;HTTP 200 内的业务失败同样抛出 ApiException。登录读取 data.token,认证采用 Bearer opaque Token,不解析 JWT,不根据首次登录的有效期自行判定续期。HTTP 或业务 401 清除当前请求所用的会话;旧请求的 401 不清除新 Token。
|
||||
对齐 StoreWeb 的 AuthController、TokenResult、UnifyResultFilter 和用户管理控制器。成功码为 200;HTTP 200 内的业务失败同样抛出 ApiException。先读取响应状态与业务码,再解析成功 data,避免错误响应的数据结构掩盖业务错误。登录读取 data.token,认证采用 Bearer opaque Token,不解析 JWT,不根据首次登录的有效期自行判定续期。HTTP 或业务 401 清除当前请求所用的会话;旧请求的 401 不清除新 Token。
|
||||
|
||||
后续商品模块按 Users 的方式新增独立 DTO、接口和实现,由 StoreApiClient 暴露模块入口。模块通过 Transport.GetRequiredAsync<T> 发送有数据请求,通过 SendAsync<object> 发送无数据操作。路由采用 user/GetAllUser 这样的相对路径,传输层自动添加版本。简单参数使用 query,复杂参数使用 body,HTTP 方法与服务端契约一致。避免按常规 REST 习惯猜测当前服务端的动作路由。
|
||||
|
||||
请求不自动重试,避免添加、修改操作重复执行。禁止自动重定向;服务地址应直接指向最终 API 地址。退出时即使服务端请求失败也清理本地 Token,并将错误交给宿主;此时服务端会话可能仍有效。Session.Clear 可用于本地退出,不撤销服务器会话。
|
||||
|
||||
当前不包含登录界面、Token 磁盘持久化、自动刷新或商品接口。实际地址由宿主提供,尚未进行真实服务联调。
|
||||
登录界面由宿主项目提供;API 层不包含 Token 磁盘持久化、自动刷新或商品接口。实际地址由宿主提供,尚未进行真实服务联调。
|
||||
|
||||
## 验证
|
||||
|
||||
@@ -75,4 +75,4 @@ dotnet build Store.Management.slnx
|
||||
dotnet run --project Tests/Store.Management.Api.SmokeTests/Store.Management.Api.SmokeTests.csproj
|
||||
```
|
||||
|
||||
模拟 HTTP 测试覆盖登录路由、Bearer、分页序列化与解析、输入边界、业务错误、无效 JSON、403、HTTP 与业务 401、退出及主动取消,不会调用真实服务。
|
||||
模拟 HTTP 测试覆盖登录路由、Bearer、分页序列化与解析、输入边界、业务错误、无效 JSON、403、HTTP 与业务 401、无效 Token/data、旧请求 401 会话隔离、登录退出串行、退出失败清理及主动取消,不会调用真实服务。
|
||||
@@ -20,7 +20,7 @@ public sealed class ApiTransport : IDisposable
|
||||
options.Validate();
|
||||
_session = session;
|
||||
_version = options.Version;
|
||||
var settings = new RestClientOptions(new Uri(options.BaseUrl.AbsoluteUri.TrimEnd('/') + "/"))
|
||||
RestClientOptions settings = new RestClientOptions(new Uri(options.BaseUrl.AbsoluteUri.TrimEnd('/') + "/"))
|
||||
{
|
||||
Timeout = options.Timeout,
|
||||
FollowRedirects = false
|
||||
@@ -32,30 +32,62 @@ public sealed class ApiTransport : IDisposable
|
||||
IReadOnlyDictionary<string, string?>? query = null, bool authenticated = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (string.IsNullOrWhiteSpace(route) || route.StartsWith('/') || route.Contains("..") ||
|
||||
route.Contains(':') || route.Contains('?') || route.Contains('#') || route.Contains('\\'))
|
||||
{
|
||||
throw new ArgumentException("接口必须使用相对路由,例如 user/GetAllUser。", nameof(route));
|
||||
if (body is not null) Validator.ValidateObject(body, new ValidationContext(body), true);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var token = authenticated ? _session.Token : null;
|
||||
if (authenticated && token is null) throw new ApiException(ApiErrorKind.Unauthorized, "请先登录。", code: 401);
|
||||
var request = new RestRequest($"{_version}/{route}", method);
|
||||
if (token is not null) request.AddHeader("Authorization", $"Bearer {token}");
|
||||
if (body is not null) request.AddJsonBody(body);
|
||||
}
|
||||
if (body is not null)
|
||||
{
|
||||
Validator.ValidateObject(body, new ValidationContext(body), true);
|
||||
}
|
||||
string? token = authenticated ? _session.Token : null;
|
||||
if (authenticated && token is null)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.Unauthorized, "请先登录。", code: 401);
|
||||
}
|
||||
RestRequest request = new RestRequest($"{_version}/{route}", method);
|
||||
if (token is not null)
|
||||
{
|
||||
request.AddHeader("Authorization", $"Bearer {token}");
|
||||
}
|
||||
if (body is not null)
|
||||
{
|
||||
request.AddJsonBody(body);
|
||||
}
|
||||
if (query is not null)
|
||||
foreach (var pair in query)
|
||||
if (pair.Value is not null) request.AddQueryParameter(pair.Key, pair.Value);
|
||||
var response = await _client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
{
|
||||
foreach (KeyValuePair<string, string?> pair in query)
|
||||
{
|
||||
if (pair.Value is not null)
|
||||
{
|
||||
request.AddQueryParameter(pair.Key, pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
RestResponse response = await _client.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (response.ResponseStatus == ResponseStatus.TimedOut)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.Timeout, "请求超时,请稍后重试。", innerException: response.ErrorException);
|
||||
}
|
||||
if ((int)response.StatusCode == 0)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.Network, "无法连接服务器,请检查网络与服务地址。", innerException: response.ErrorException);
|
||||
}
|
||||
|
||||
ApiResult<T>? result = null;
|
||||
// 先判断状态,再解析 data;错误响应的数据结构可能与成功响应不同。
|
||||
ApiResult<JsonElement>? result = null;
|
||||
JsonException? parseError = null;
|
||||
try { result = JsonSerializer.Deserialize<ApiResult<T>>(response.Content ?? "", Json); }
|
||||
catch (JsonException ex) { parseError = ex; }
|
||||
try
|
||||
{
|
||||
result = JsonSerializer.Deserialize<ApiResult<JsonElement>>(response.Content ?? "", Json);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
parseError = ex;
|
||||
}
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized || result?.Code == 401)
|
||||
{
|
||||
_session.ClearIfCurrent(token);
|
||||
@@ -63,13 +95,31 @@ public sealed class ApiTransport : IDisposable
|
||||
response.StatusCode, 401, result?.TraceId);
|
||||
}
|
||||
if (!response.IsSuccessful)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.Http, result?.Msg ?? $"服务器请求失败({(int)response.StatusCode})。",
|
||||
response.StatusCode, result?.Code, result?.TraceId);
|
||||
}
|
||||
if (result is null)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.InvalidResponse, "服务器响应格式无效。", response.StatusCode, innerException: parseError);
|
||||
}
|
||||
if (result.Code != 200)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.Business, result.Msg ?? "操作失败。", response.StatusCode, result.Code, result.TraceId);
|
||||
return result.Data;
|
||||
}
|
||||
if (result.Data.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
try
|
||||
{
|
||||
return result.Data.Deserialize<T>(Json);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new ApiException(ApiErrorKind.InvalidResponse, "服务器响应 data 格式无效。",
|
||||
response.StatusCode, result.Code, result.TraceId, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<T> GetRequiredAsync<T>(string route, Method method, object? body = null,
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
using MAX.Core.Commons;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Services.Dialogs;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Store.Management.Framework.Controls;
|
||||
|
||||
public static class IDialogServiceExtension
|
||||
{
|
||||
public static void ShowDialog<TParameter>(this IDialogService dialogService, string name, TParameter parameter, Action<IDialogResult> callback) where TParameter : class
|
||||
{
|
||||
dialogService.ShowDialog(name, new DialogParameters
|
||||
{
|
||||
{"parameter", parameter},
|
||||
}, callback);
|
||||
}
|
||||
public static void Show<TParameter>(this IDialogService dialogService, string name, TParameter parameter, Action<IDialogResult> callback) where TParameter : class
|
||||
{
|
||||
dialogService.Show(name, new DialogParameters
|
||||
{
|
||||
{"parameter", parameter},
|
||||
}, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 确定取消对话框 ViewModel
|
||||
/// </summary>
|
||||
public class ConfirmDialogViewModel : BindableBase, IDialogAware
|
||||
{
|
||||
public ConfirmDialogViewModel()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private string title = "";
|
||||
private bool _enableConfirm = true;
|
||||
private bool _ShowConfirm = true;
|
||||
private bool _ShowCancel = true;
|
||||
private bool _ShowErrorMsg = true;
|
||||
private string _ErrorMsg = "";
|
||||
private string confirmText = "确定";
|
||||
private string cancelText = "取消";
|
||||
private string backText = "返回";
|
||||
private bool allowEscape = true;
|
||||
private bool allowEnter = true;
|
||||
private bool isLoading = false;
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => title;
|
||||
protected set => SetProperty(ref title, value);
|
||||
}
|
||||
|
||||
public bool AllowEscape
|
||||
{
|
||||
get => allowEscape;
|
||||
set => SetProperty(ref allowEscape, value);
|
||||
}
|
||||
|
||||
public bool AllowEnter
|
||||
{
|
||||
get => allowEnter;
|
||||
set => SetProperty(ref allowEnter, value);
|
||||
}
|
||||
public bool IsLoading
|
||||
{
|
||||
get => isLoading;
|
||||
set => SetProperty(ref isLoading, value, () =>
|
||||
{
|
||||
AllowEscape = !value;
|
||||
AllowEnter = !value;
|
||||
EnableConfirm = !value;
|
||||
});
|
||||
}
|
||||
//<UserControl.Resources>
|
||||
// <SolidColorBrush x:Key="RegionBrush" Color="Red"/>
|
||||
// </UserControl.Resources>
|
||||
private SolidColorBrush _Underpaint = new SolidColorBrush(Color.FromRgb(238, 238, 238));
|
||||
/// <summary>
|
||||
/// 背景底色
|
||||
/// </summary>
|
||||
public SolidColorBrush Underpaint
|
||||
{
|
||||
get => _Underpaint;
|
||||
set => SetProperty(ref _Underpaint, value);
|
||||
}
|
||||
|
||||
public event Action<IDialogResult> RequestClose;
|
||||
protected void OnRequestClose(IDialogResult result)
|
||||
{
|
||||
RequestClose?.Invoke(result);
|
||||
}
|
||||
|
||||
public virtual bool CanCloseDialog()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void OnDialogClosed()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public virtual void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool EnableConfirm
|
||||
{
|
||||
get => _enableConfirm;
|
||||
set => SetProperty(ref _enableConfirm, value);
|
||||
}
|
||||
|
||||
public bool ShowConfirm
|
||||
{
|
||||
get => _ShowConfirm;
|
||||
set => SetProperty(ref _ShowConfirm, value);
|
||||
}
|
||||
public bool ShowCancel
|
||||
{
|
||||
get => _ShowCancel;
|
||||
set => SetProperty(ref _ShowCancel, value);
|
||||
}
|
||||
public bool ShowErrorMsg
|
||||
{
|
||||
get => _ShowErrorMsg;
|
||||
set => SetProperty(ref _ShowErrorMsg, value);
|
||||
}
|
||||
|
||||
public string ConfirmText
|
||||
{
|
||||
get => confirmText;
|
||||
set => SetProperty(ref confirmText, value);
|
||||
}
|
||||
public string CancelText
|
||||
{
|
||||
get => cancelText;
|
||||
set => SetProperty(ref cancelText, value);
|
||||
}
|
||||
public string BackText
|
||||
{
|
||||
get => backText;
|
||||
set => SetProperty(ref backText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 错误提示
|
||||
/// </summary>
|
||||
public string ErrorMsg
|
||||
{
|
||||
get => _ErrorMsg;
|
||||
set => SetProperty(ref _ErrorMsg, value);
|
||||
}
|
||||
|
||||
public virtual ICommand ConfirmCommand => new DelegateCommand(_Confirm);
|
||||
|
||||
protected virtual void _Confirm()
|
||||
{
|
||||
DialogResult ret = Confirm();
|
||||
if (ret != null)
|
||||
{
|
||||
RequestClose?.Invoke(ret);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual DialogResult Confirm()
|
||||
{
|
||||
return new DialogResult(ButtonResult.OK);
|
||||
}
|
||||
|
||||
public virtual ICommand CancelCommand => new DelegateCommand(_Cancel);
|
||||
|
||||
protected virtual void _Cancel()
|
||||
{
|
||||
DialogResult ret = Cancel();
|
||||
if (ret != null)
|
||||
{
|
||||
RequestClose?.Invoke(ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestClose?.Invoke(new DialogResult(ButtonResult.Cancel));
|
||||
}
|
||||
}
|
||||
protected virtual DialogResult Cancel()
|
||||
{
|
||||
return new DialogResult(ButtonResult.Cancel);
|
||||
}
|
||||
|
||||
public virtual void Loaded(object content)
|
||||
{
|
||||
var bb = Application.Current.TryFindResource("BackgroundBrush") as SolidColorBrush;
|
||||
if (bb != null)
|
||||
{
|
||||
Underpaint = bb;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class AsyncConfirmDialogViewModel : ConfirmDialogViewModel
|
||||
{
|
||||
public override ICommand ConfirmCommand => new AsyncDelegateCommand(_Confirm);
|
||||
|
||||
protected new async Task _Confirm()
|
||||
{
|
||||
DialogResult ret = await Confirm();
|
||||
if (ret != null)
|
||||
{
|
||||
OnRequestClose(ret);
|
||||
}
|
||||
}
|
||||
|
||||
protected new virtual async Task<DialogResult> Confirm()
|
||||
{
|
||||
return await Task.FromResult(new DialogResult(ButtonResult.OK));
|
||||
}
|
||||
}
|
||||
|
||||
public interface IDialogClassParameters
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
/// 确定取消对话框
|
||||
/// </summary>
|
||||
/// <typeparam name="TParameter">参数Model</typeparam>
|
||||
public class ConfirmDialogViewModel<TParameter> : ConfirmDialogViewModel where TParameter : class
|
||||
{
|
||||
protected TParameter Parameter { get; set; }
|
||||
public override void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
base.OnDialogOpened(parameters);
|
||||
if (parameters.TryGetValue("parameter", out TParameter parameter))
|
||||
{
|
||||
Parameter = parameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ConfirmDialogViewModel<TParameter, TResult> : ConfirmDialogViewModel where TParameter : class where TResult : DialogResult/*, new()*/
|
||||
{
|
||||
protected TParameter Parameter { get; set; }
|
||||
public override void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
base.OnDialogOpened(parameters);
|
||||
if (parameters.TryGetValue("parameter", out TParameter parameter))
|
||||
{
|
||||
Parameter = parameter;
|
||||
}
|
||||
}
|
||||
protected override void _Confirm()
|
||||
{
|
||||
TResult ret = Confirm();
|
||||
if (ret != null)
|
||||
{
|
||||
base.OnRequestClose(ret);
|
||||
}
|
||||
}
|
||||
protected new abstract TResult Confirm();
|
||||
|
||||
protected override void _Cancel()
|
||||
{
|
||||
TResult ret = Cancel();
|
||||
if (ret != null)
|
||||
{
|
||||
base.OnRequestClose(ret);
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnRequestClose(new DialogResult(ButtonResult.Cancel));
|
||||
}
|
||||
}
|
||||
protected new abstract TResult Cancel();
|
||||
}
|
||||
|
||||
|
||||
//public abstract class ConfirmDialogParameterViewModel : ConfirmDialogViewModel<ConfirmDialogParameterViewModel.Parameter>
|
||||
//{
|
||||
// public abstract class Parameter;
|
||||
//}
|
||||
@@ -1,13 +1,14 @@
|
||||
using Prism.Ioc;
|
||||
using Prism.Services.Dialogs;
|
||||
using MAX.Notification;
|
||||
using Store.Management.Notifications;
|
||||
using Prism.Ioc;
|
||||
using Prism.Mvvm;
|
||||
using Prism.Services.Dialogs;
|
||||
using Store.Management.Api;
|
||||
using Store.Management.Framework;
|
||||
using Store.Management.Notifications;
|
||||
using Store.Management.Services;
|
||||
using Store.Management.ViewModels;
|
||||
using Store.Management.Views;
|
||||
using System.Windows;
|
||||
using Store.Management.Api;
|
||||
namespace Store.Management;
|
||||
public partial class App : FrameworkApplication
|
||||
{
|
||||
@@ -22,51 +23,61 @@ public partial class App : FrameworkApplication
|
||||
BaseUrl = new Uri("http://localhost:14312/api/")
|
||||
});
|
||||
|
||||
protected override Window CreateShell() => null!;
|
||||
protected override Window CreateShell()
|
||||
{
|
||||
return Container.Resolve<MainWindow>();
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
// 登录弹窗关闭与主窗口创建之间不触发 WPF 的最后窗口退出。
|
||||
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
Container.Resolve<IDialogService>().ShowDialog(
|
||||
nameof(LoginWindow), new DialogParameters(), result =>
|
||||
{
|
||||
if (result.Result != ButtonResult.OK)
|
||||
{
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
base.OnInitialized();
|
||||
//base.OnInitialized();
|
||||
//// 登录弹窗关闭与主窗口创建之间不触发 WPF 的最后窗口退出。
|
||||
//ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
//Container.Resolve<IDialogService>().ShowDialog(
|
||||
// nameof(LoginWindow), new DialogParameters(), result =>
|
||||
// {
|
||||
// if (result.Result != ButtonResult.OK)
|
||||
// {
|
||||
// Shutdown();
|
||||
// return;
|
||||
// }
|
||||
|
||||
MainWindow = Container.Resolve<MainWindow>();
|
||||
ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||
MainWindow.Show();
|
||||
});
|
||||
}
|
||||
// MainWindow = Container.Resolve<MainWindow>();
|
||||
// ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||
// MainWindow.Show();
|
||||
// });
|
||||
}
|
||||
|
||||
protected override void RegisterTypes(IContainerRegistry registry)
|
||||
protected override void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
registry.RegisterInstance(CreateApiClient());
|
||||
registry.Register<LoginViewModel>(provider => new LoginViewModel(provider.Resolve<StoreApiClient>().Auth));
|
||||
registry.RegisterSingleton<SingletonProbe>();
|
||||
registry.Register<TransientProbe>();
|
||||
registry.RegisterSingleton<NotificationExampleState>();
|
||||
registry.Register<IPublisher>(provider => new NotificationPublisher(
|
||||
new PrismNotificationServiceProvider(provider)));
|
||||
// 显式提供处理器集合,展示一条通知分发给多个业务模块。
|
||||
registry.Register<IEnumerable<INotificationHandler<ProductSavedNotification>>>(provider =>
|
||||
new INotificationHandler<ProductSavedNotification>[]
|
||||
{
|
||||
new ProductCacheHandler(provider.Resolve<NotificationExampleState>()),
|
||||
new ProductAuditHandler(provider.Resolve<NotificationExampleState>())
|
||||
});
|
||||
registry.RegisterDialog<LoginWindow, LoginViewModel>();
|
||||
registry.RegisterDialog<FrameworkTestDialog, FrameworkTestDialogViewModel>();
|
||||
|
||||
containerRegistry.RegisterDialogWindow<Framework.Controls.DialogWindow>();
|
||||
|
||||
ViewModelLocationProvider.Register<MainWindow, MainWindowViewModel>();
|
||||
|
||||
|
||||
//registry.RegisterInstance(CreateApiClient());
|
||||
// registry.Register<LoginViewModel>(provider => new LoginViewModel(provider.Resolve<StoreApiClient>().Auth));
|
||||
// registry.RegisterSingleton<SingletonProbe>();
|
||||
// registry.Register<TransientProbe>();
|
||||
// registry.RegisterSingleton<NotificationExampleState>();
|
||||
// registry.Register<IPublisher>(provider => new NotificationPublisher(
|
||||
// new PrismNotificationServiceProvider(provider)));
|
||||
// // 显式提供处理器集合,展示一条通知分发给多个业务模块。
|
||||
// registry.Register<IEnumerable<INotificationHandler<ProductSavedNotification>>>(provider =>
|
||||
// new INotificationHandler<ProductSavedNotification>[]
|
||||
// {
|
||||
// new ProductCacheHandler(provider.Resolve<NotificationExampleState>()),
|
||||
// new ProductAuditHandler(provider.Resolve<NotificationExampleState>())
|
||||
// });
|
||||
// registry.RegisterDialog<LoginWindow, LoginViewModel>();
|
||||
// registry.RegisterDialog<FrameworkTestDialog, FrameworkTestDialogViewModel>();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try { Container?.Resolve<StoreApiClient>().Dispose(); }
|
||||
finally { base.OnExit(e); }
|
||||
//try { Container?.Resolve<StoreApiClient>().Dispose(); }
|
||||
//finally { base.OnExit(e); }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
<Window
|
||||
<f:WindowBase
|
||||
x:Class="Store.Management.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:f="Store.Management.Framework"
|
||||
xmlns:views="clr-namespace:Store.Management.Views"
|
||||
Title="Store.Management · 框架能力测试"
|
||||
Title="{Binding Title}"
|
||||
Width="960"
|
||||
Height="720"
|
||||
MinWidth="720"
|
||||
MinHeight="560"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<views:FrameworkTestPage />
|
||||
</Window>
|
||||
</f:WindowBase>
|
||||
@@ -1,6 +1,19 @@
|
||||
using Prism.Mvvm;
|
||||
using Store.Management.Framework.Controls;
|
||||
using System.Windows;
|
||||
namespace Store.Management;
|
||||
public partial class MainWindow : Window
|
||||
|
||||
public partial class MainWindow : WindowBase
|
||||
{
|
||||
public MainWindow() => InitializeComponent();
|
||||
public MainWindow() => InitializeComponent();
|
||||
}
|
||||
|
||||
public class MainWindowViewModel : BindableBase
|
||||
{
|
||||
private string _title="TTTT";
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set => SetProperty(ref _title, value);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
Reference in new issue
Block a user