在开发过程中,很多操作需要先在后台检查复杂的业务条件,如果满足某些预警条件,才弹出确认窗口向用户二次确认,点确定继续执行,点取消直接中止;条件不满足则直接执行业务,无需弹窗。
之前【056】使用警告弹窗提示用户信息使用的是库存转总账的 4034d041 警告表单(600x152),界面偏大且不太美观;而标准单据内置的 PDFormMessage.ShowConfirmDialog 又是静态写死在按钮客户端事件上的,无法在后台动态校验业务分支。
本篇整理 U9 原汁原味的标准弃审/删除同款确认小窗(FormId: c690cbc6-fb39-4130-bf13-ddd7e8e0517c,320x105 像素,带小蓝问号),并封装为通用的 U9ConfirmHelper 工具类。支持单次消费动态令牌(防重复弹窗)、DOM 隐藏域即时清理、以及上下文数据(支持任意 object 如 long[]、DTO 等)暂存与一键消费。
一、通用辅助类 U9ConfirmHelper(直接复制进项目即可)
需引用程序集:UBF.System.dll(位于Portal\bin)以及UFSoft.UBF.UI.Controls.Interface.dll、UFSoft.UBF.UI.FormProcess.dll
注意:必须使用UFSoft.UBF.Security.BaseU9DESEncryptHelper(UTF-8 编码),与弹窗窗体的解密端保持完全对称,杜绝中文乱码。
using System;
using System.Web.UI;
using UFSoft.UBF.Security;
using UFSoft.UBF.UI;
using UFSoft.UBF.UI.ControlModel;
using UFSoft.UBF.UI.FormProcess;
using UFSoft.UBF.UI.IView;
namespace UFIDA.U9.SH.HaLi.QXUIPlugIn
{
/// <summary>
/// U9 原生确认弹窗用户选择结果
/// </summary>
public enum ConfirmResult
{
/// <summary>
/// 未触发确认或非本次弹窗回发
/// </summary>
None = 0,
/// <summary>
/// 用户点击了【确定】或【是】
/// </summary>
Confirmed = 1,
/// <summary>
/// 用户点击了【否】(仅在三态弹窗中)
/// </summary>
Denied = 2
}
/// <summary>
/// U9 原生确认弹窗通用辅助工具(带蓝色小问号,支持二态【确定/取消】与三态【是/否/取消】)
/// 支持:动态一次性令牌防重复、DOM隐藏域即时清理、上下文数据安全暂存与一键消费
/// </summary>
public static class U9ConfirmHelper
{
private const string TokenPrefix = "__U9ConfirmToken_";
private const string DataPrefix = "__U9ConfirmData_";
/// <summary>
/// 弹出 U9 系统原生 320x105 确认框(DeleteAlertWebPart,带【确定】【取消】按钮)
/// 用户点击【确定】后自动触发局部 Ajax 回发;点击【取消】或右上角【X】关闭时彻底静默,原地停留
/// </summary>
/// <param name="part">当前 IPart 页面对象(或 WebPart 实例)</param>
/// <param name="actionKey">业务动作标识(如 "SplitPlanOrder"、"UndoApprove",用于区分不同弹窗)</param>
/// <param name="message">弹窗提示内容</param>
/// <param name="title">弹窗标题(默认"确认")</param>
/// <param name="contextData">可选:需要随本次确认流转暂存的任意上下文数据(如实体、数组、字典、JSON等,类型为 object)</param>
/// <param name="targetControl">可选:若希望确认后回发到特定按钮的 Click 事件,传入该按钮;若不传则走默认页面生命周期</param>
public static void ShowConfirm(IPart part, string actionKey, string message, string title = "确认", object contextData = null, IUFControl targetControl = null)
{
if (part == null || string.IsNullOrEmpty(actionKey)) return;
BaseWebForm webForm = part as BaseWebForm;
if (webForm == null || webForm.Page == null) return;
// 1. 生成单次唯一消费令牌(GUID)并暂存状态,防止静态参数残留导致死循环
string token = string.Format("{0}_{1}", actionKey, Guid.NewGuid().ToString("N"));
part.CurrentState[TokenPrefix + actionKey] = token;
// 2. 若传入了上下文数据,一并暂存(原生 object)
if (contextData != null)
{
part.CurrentState[DataPrefix + actionKey] = contextData;
}
// 3. 确定局部回发目标(推荐显式传入 targetControl;若未传则沿控件树通用检索表单的主 UpdatePanel,杜绝整页刷新)
Control postBackCtrl = ResolveAjaxTarget(webForm, targetControl as Control);
string postBackTarget = postBackCtrl != null ? postBackCtrl.ClientID.Replace("_", "$") : string.Empty;
// 4. 优先采用 U9 官方规范挂载到 TopLevelContainer,确保在异步回发中百分之百被输出给前端
Control contextCtrl = (Control)webForm.TopLevelContainer ?? (targetControl as Control) ?? (Control)webForm.Page;
if (contextCtrl == null) return;
// 5. 构造原生弹窗 URL(DeleteAlertWebPart)
string formId = "c690cbc6-fb39-4130-bf13-ddd7e8e0517c";
#pragma warning disable 0618
string encMsg = BaseU9DESEncryptHelper.Encrypt(message ?? "");
#pragma warning restore 0618
string simpleUrl = webForm.ResolveClientUrl("~/ufsoft/simple.aspx");
string url = string.Format("{0}?lnk={1}&chromeType=4&message={2}", simpleUrl, formId, encMsg);
// 6. 前端纯 JS 脚本(点确定触发局部回发;点取消或点右上角【X】均静默关闭,绝不回发)
string script = string.Format(
@"ShowModalDialogForAll('{0}', '{1}', 105, 320, true, false, function(ret) {{
if (ret === 'OK' || ret === true) {{
__doPostBack('{2}', '{3}');
setTimeout(function() {{
var arg = document.getElementById('__EVENTARGUMENT');
if (arg) arg.value = '';
}}, 10);
}}
}});",
url,
title,
postBackTarget,
token
);
AtlasHelper.RegisterAtlasStartupScript(contextCtrl, contextCtrl.GetType(), "confirm_" + Guid.NewGuid().ToString("N"), script, true);
}
/// <summary>
/// 弹出 U9 系统原生 350x105【是/否/取消】三态确认框(ConfirmUIFormWebPart,带原生【是】【否】【取消】三个按钮)
/// 用户点击【是】或【否】后自动触发局部 Ajax 回发;点击【取消】或右上角【X】关闭时彻底静默,原地停留
/// 配合 GetConfirmResult 使用:Confirmed 代表点【是】,Denied 代表点【否】
/// </summary>
/// <param name="part">当前 IPart 页面对象(或 WebPart 实例)</param>
/// <param name="actionKey">业务动作标识</param>
/// <param name="message">弹窗提示内容</param>
/// <param name="title">弹窗标题(默认"确认当前操作")</param>
/// <param name="contextData">可选:需要随本次确认流转暂存的任意上下文数据</param>
/// <param name="targetControl">可选:若希望确认后回发到特定按钮的 Click 事件,传入该按钮;若不传则走默认页面生命周期</param>
public static void ShowYesNoCancelConfirm(IPart part, string actionKey, string message, string title = "确认当前操作", object contextData = null, IUFControl targetControl = null)
{
if (part == null || string.IsNullOrEmpty(actionKey)) return;
BaseWebForm webForm = part as BaseWebForm;
if (webForm == null || webForm.Page == null) return;
// 1. 生成单次唯一消费令牌(GUID)并暂存状态
string token = string.Format("{0}_{1}", actionKey, Guid.NewGuid().ToString("N"));
part.CurrentState[TokenPrefix + actionKey] = token;
// 2. 若传入了上下文数据,一并暂存
if (contextData != null)
{
part.CurrentState[DataPrefix + actionKey] = contextData;
}
// 3. 确定局部回发目标
Control postBackCtrl = ResolveAjaxTarget(webForm, targetControl as Control);
string postBackTarget = postBackCtrl != null ? postBackCtrl.ClientID.Replace("_", "$") : string.Empty;
// 4. 挂载点
Control contextCtrl = (Control)webForm.TopLevelContainer ?? (targetControl as Control) ?? (Control)webForm.Page;
if (contextCtrl == null) return;
// 5. 构造原生三态弹窗 URL(ConfirmUIFormWebPart,宽 350 高 105,带【是】【否】【取消】三个按钮)
string formId = "297c8c52-d2cf-4c58-93bc-84d17a2176a4";
string simpleUrl = webForm.ResolveClientUrl("~/ufsoft/simple.aspx");
string encTitle = System.Web.HttpUtility.UrlEncode(title ?? "确认当前操作", System.Text.Encoding.UTF8);
string encMessage = System.Web.HttpUtility.UrlEncode(message ?? "", System.Text.Encoding.UTF8);
string url = string.Format("{0}?lnk={1}&chromeType=4&ShowType=ShowModal&Title={2}&Message={3}", simpleUrl, formId, encTitle, encMessage);
// 6. 前端纯 JS 脚本:
// 点【是】(ret === 'Yes') ➔ 回发 _Yes
// 点【否】(ret === 'No') ➔ 回发 _No
// 点【取消】(ret === 'Cancel') 或 点右上角【X】(ret 为空/undefined) ➔ 彻底静默,绝不回发!
string script = string.Format(
@"ShowModalDialogForAll('{0}', '{1}', 105, 350, true, false, function(ret) {{
if (!ret || ret === 'Cancel') return;
if (ret === 'Yes') {{
__doPostBack('{2}', '{3}_Yes');
}} else if (ret === 'No') {{
__doPostBack('{2}', '{3}_No');
}}
setTimeout(function() {{
var arg = document.getElementById('__EVENTARGUMENT');
if (arg) arg.value = '';
}}, 10);
}});",
url,
title,
postBackTarget,
token
);
AtlasHelper.RegisterAtlasStartupScript(contextCtrl, contextCtrl.GetType(), "confirm3_" + Guid.NewGuid().ToString("N"), script, true);
}
/// <summary>
/// 检查当前请求针对指定动作的确认结果(适用于三态弹窗或二态开启 postBackOnCancel 的场景)
/// 命中后会立即原子销毁 Token(单次消费),防止重复执行与参数残留
/// </summary>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <returns>ConfirmResult: None(非本次回发), Confirmed(点确定/是), Denied(点否), Cancelled(点取消)</returns>
public static ConfirmResult GetConfirmResult(IPart part, string actionKey)
{
object dummy;
return GetConfirmResult(part, actionKey, out dummy);
}
/// <summary>
/// 检查当前请求针对指定动作的确认结果,并一并取出暂存的上下文数据
/// </summary>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <param name="contextData">输出:弹窗前暂存的上下文数据</param>
/// <returns>ConfirmResult 枚举值</returns>
public static ConfirmResult GetConfirmResult(IPart part, string actionKey, out object contextData)
{
contextData = null;
if (part == null || string.IsNullOrEmpty(actionKey)) return ConfirmResult.None;
BaseWebForm webForm = part as BaseWebForm;
if (webForm == null || webForm.Page == null) return ConfirmResult.None;
// 1. 读取期待的有效 Token
string expectedToken = part.CurrentState[TokenPrefix + actionKey] as string;
if (string.IsNullOrEmpty(expectedToken)) return ConfirmResult.None;
// 2. 核对回发参数
string returnToken = webForm.Page.Request.Params["__EVENTARGUMENT"];
if (string.IsNullOrEmpty(returnToken)) return ConfirmResult.None;
// 2.1 命中【确定/是】
if (returnToken.Equals(expectedToken + "_OK", StringComparison.OrdinalIgnoreCase) ||
returnToken.Equals(expectedToken + "_Yes", StringComparison.OrdinalIgnoreCase) ||
returnToken.Equals(expectedToken, StringComparison.OrdinalIgnoreCase))
{
part.CurrentState[TokenPrefix + actionKey] = null;
contextData = part.CurrentState[DataPrefix + actionKey];
part.CurrentState[DataPrefix + actionKey] = null;
return ConfirmResult.Confirmed;
}
// 2.2 命中【否】(三态专属)
if (returnToken.Equals(expectedToken + "_No", StringComparison.OrdinalIgnoreCase))
{
part.CurrentState[TokenPrefix + actionKey] = null;
contextData = part.CurrentState[DataPrefix + actionKey];
part.CurrentState[DataPrefix + actionKey] = null;
return ConfirmResult.Denied;
}
return ConfirmResult.None;
}
/// <summary>
/// 泛型重载:检查当前请求针对指定动作的确认结果,并将暂存的上下文数据自动转换为指定类型 T
/// </summary>
/// <typeparam name="T">目标数据类型</typeparam>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <param name="contextData">输出:转换后的强类型数据</param>
/// <returns>ConfirmResult 枚举值</returns>
public static ConfirmResult GetConfirmResult<T>(IPart part, string actionKey, out T contextData)
{
object raw;
ConfirmResult res = GetConfirmResult(part, actionKey, out raw);
if (res != ConfirmResult.None && raw != null)
{
if (raw is T)
{
contextData = (T)raw;
}
else
{
try
{
contextData = (T)Convert.ChangeType(raw, typeof(T));
}
catch
{
contextData = default(T);
}
}
}
else
{
contextData = default(T);
}
return res;
}
/// <summary>
/// 检查当前请求是否为指定动作的确认回发(不关心暂存数据)
/// 命中后会立即原子销毁 Token(单次消费),防止重复执行与参数残留
/// </summary>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <returns>若为确认回发返回 true,否则返回 false</returns>
public static bool IsConfirmed(IPart part, string actionKey)
{
object dummy;
return TryGetConfirmed(part, actionKey, out dummy);
}
/// <summary>
/// 检查当前请求是否为指定动作的确认回发,并一并取出弹窗前暂存的原生 object 上下文数据
/// 命中后会立即原子销毁 Token 与暂存数据(单次消费),防止重复执行与参数残留
/// </summary>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <param name="contextData">输出:弹窗前暂存的原生 object 数据(如 long[]、DTO等),由调用方直接转换;未暂存或非确认回发时为 null</param>
/// <returns>若为确认回发返回 true,否则返回 false</returns>
public static bool TryGetConfirmed(IPart part, string actionKey, out object contextData)
{
contextData = null;
if (part == null || string.IsNullOrEmpty(actionKey)) return false;
BaseWebForm webForm = part as BaseWebForm;
if (webForm == null || webForm.Page == null) return false;
// 1. 读取期待的有效 Token
string expectedToken = part.CurrentState[TokenPrefix + actionKey] as string;
if (string.IsNullOrEmpty(expectedToken)) return false;
// 2. 核对回发参数(兼容普通 token 与带 _OK 后缀的 token)
string returnToken = webForm.Page.Request.Params["__EVENTARGUMENT"];
if (string.IsNullOrEmpty(returnToken)) return false;
if (!expectedToken.Equals(returnToken, StringComparison.OrdinalIgnoreCase) &&
!(expectedToken + "_OK").Equals(returnToken, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// 3. 【核心防线】立即原子销毁 Token!确保该次确认终生只消费一次
part.CurrentState[TokenPrefix + actionKey] = null;
// 4. 取出并清理暂存数据(原生 object,直接返回给调用方转换)
contextData = part.CurrentState[DataPrefix + actionKey];
part.CurrentState[DataPrefix + actionKey] = null;
return true;
}
/// <summary>
/// 泛型重载:检查指定动作确认回发,并将暂存的上下文数据自动转换为指定类型 T
/// </summary>
/// <typeparam name="T">目标数据类型(如 long[], string, DTO等)</typeparam>
/// <param name="part">当前 IPart 页面对象</param>
/// <param name="actionKey">业务动作标识</param>
/// <param name="contextData">输出:转换后的强类型数据</param>
/// <returns>若为确认回发返回 true,否则返回 false</returns>
public static bool TryGetConfirmed<T>(IPart part, string actionKey, out T contextData)
{
object rawData;
if (TryGetConfirmed(part, actionKey, out rawData))
{
if (rawData is T)
{
contextData = (T)rawData;
}
else
{
try
{
contextData = (T)Convert.ChangeType(rawData, typeof(T));
}
catch
{
contextData = default(T);
}
}
return true;
}
contextData = default(T);
return false;
}
/// <summary>
/// 解析用于 ASP.NET AJAX 局部异步回发的目标控件(UniqueID)
/// 1. 优先使用显式传入的 targetControl;
/// 2. 若未传入 targetControl,自动沿控件树通用检索表单的主 UpdatePanel(消除对特定 Card0/Toolbar 名称的死板依赖)
/// </summary>
private static Control ResolveAjaxTarget(BaseWebForm webForm, Control targetControl)
{
if (targetControl != null)
{
return targetControl;
}
if (webForm != null)
{
// U9 BuildCommonControls 默认将主 UpdatePanel 加在 form.Controls 中
foreach (Control c in webForm.Controls)
{
if (IsUpdatePanel(c)) return c;
}
// 兜底:在 TopLevelContainer 子树中递归检索 UpdatePanel 或主卡片容器
Control topCtrl = webForm.TopLevelContainer as Control;
if (topCtrl != null)
{
Control foundUpdatePanel = FindFirstUpdatePanel(topCtrl);
if (foundUpdatePanel != null) return foundUpdatePanel;
return topCtrl;
}
}
return null;
}
/// <summary>
/// 递归探测控件树中的第一个 UpdatePanel
/// </summary>
private static Control FindFirstUpdatePanel(Control parent)
{
if (parent == null) return null;
if (IsUpdatePanel(parent)) return parent;
foreach (Control child in parent.Controls)
{
Control found = FindFirstUpdatePanel(child);
if (found != null) return found;
}
return null;
}
/// <summary>
/// 检查控件是否为 ASP.NET AJAX 的 UpdatePanel 类型(按类型名称与基类递归比对,避免直接依赖 System.Web.Extensions 程序集)
/// </summary>
private static bool IsUpdatePanel(Control ctrl)
{
if (ctrl == null) return false;
Type t = ctrl.GetType();
while (t != null && t != typeof(object))
{
if (t.Name.Equals("UpdatePanel", StringComparison.OrdinalIgnoreCase)) return true;
t = t.BaseType;
}
return false;
}
}
}二、用法一:在 UI 插件中使用(如 WebPartExtend / BaseUIPlugIn)
UI 插件中,按钮通常是动态扩展到操作下拉菜单(DDBtnOperate)里的。点击按钮时先在 Click 事件中校验条件并弹窗,确认后的逻辑在 BeforeRender 生命周期中处理。1. 点击按钮事件中(发起弹窗)
private void OnSplitPlanOrderClick(object sender, MenuItemClickEventArgs e)
{
if (_part == null) return;
_part.OnDataCollect(this);
// 1. 获取勾选行
long[] ids = _part.Model.PlanOrder.GetSelectedRecordIDs();
if (ids != null && ids.Length > 0)
{
// 场景 A:勾选了数据,无需弹窗,直接执行勾选行的拆分!
DoSplitByIds(ids);
return;
}
// 2. 未勾选行:校验条件,满足预警条件时弹出原生确认小窗
string filterJson = BuildCurrentFilterJson(); // 获取查询条件
string tip = string.Format("未勾选数据,将自动对【当前查询结果】中的自制计划订单进行整批拆分,是否继续?");
// 显式传入操作下拉按钮 ddBtn(走局部 Ajax 异步回发)并随确认流转暂存查询条件 filterJson
IUFDropDownButton ddBtn = CommonFunction.FindControl(_part, "Card0", "DDBtnOperate") as IUFDropDownButton;
U9ConfirmHelper.ShowConfirm(_part, "SplitPlanOrderAll", tip, "整批拆分确认", filterJson, ddBtn);
}2. 在 BeforeRender 中接收确认结果(消费执行)
用户在弹窗中点【取消】或点【X】关闭时,前端不产生回发,界面完全保持原样;点【确定】后回发,在此处执行。
public override void BeforeRender(IPart Part, EventArgs args)
{
base.BeforeRender(Part, args);
// 一行代码接收确认并自动取出暂存数据!
// 内部会自动校验 Token 并立即原子销毁 Token,单次消费,绝对不会重复执行或死循环弹窗
if (U9ConfirmHelper.TryGetConfirmed(Part, "SplitPlanOrderAll", out string batchFilterJson))
{
DoSplitByFilter(batchFilterJson);
}
}三、用法二:在客开页面/客开按钮中使用(Click_Extend 事件自闭环)
典型业务场景:在 U9 建模设计器中为客开按钮【计算折扣价】(
BtnCalcDiscount)生成的标准二开代码桩(位于XXXWebPart_Extend.cs分部类中的BtnCalcDiscount_Click_Extend)。
- 首次点击:单据明细行尚未有折扣价,直接无感计算并写入,不弹窗打扰用户;
- 二次点击:已存在计算过的折扣价,重新计算将会覆盖现有数据,必须弹出 320x105 确认窗口提示;点【确定】后执行覆盖,点【取消】保持现状。
按钮无论放置在上方 Toolbar 还是下方 Card,只需传入
targetControl: this.BtnCalcDiscount,弹窗确认后便会走局部 Ajax 回发精准重新进入当前Click_Extend事件,形成完全独立的自闭环处理:
// 示例:U9 建模设计器标准生成的客开按钮点击事件代码桩(WebPart_Extend.cs)
private void BtnCalcDiscount_Click_Extend(object sender, EventArgs e)
{
// 1. 优先检查:是否为用户在确认窗点击【确定】后的回发
if (U9ConfirmHelper.IsConfirmed(this, "CalcDiscount"))
{
DoCalculateDiscount(allowOverwrite: true); // 用户已确认,直接执行覆盖计算
return;
}
// 2. 正常点击时的业务逻辑:检查当前单据行是否已存在折扣价(防误触二次覆盖)
bool hasExistingDiscount = CheckHasExistingDiscount();
if (hasExistingDiscount)
{
string tip = "当前单据已存在计算过的折扣价,重新计算将覆盖已有价格,是否确认继续?";
// 满足覆盖预警条件,弹出原生 320x105 确认框:
// 显式传入 targetControl 为当前客开按钮自身(回发后走 Ajax 局部刷新重新进入当前 Click 事件,命中第 1 步分支)
U9ConfirmHelper.ShowConfirm(
this,
"CalcDiscount",
tip,
"重算折扣确认",
targetControl: this.BtnCalcDiscount
);
return;
}
// 3. 首次计算(不存在已计算价格):直接无感计算,无需弹窗打扰用户
DoCalculateDiscount(allowOverwrite: false);
}
/// <summary>
/// 检查单据明细行是否已有折扣价
/// </summary>
private bool CheckHasExistingDiscount()
{
// 示例:遍历当前单据明细行视图
foreach (var row in this.Model.OrderLine.Records)
{
if (Convert.ToDecimal(row["DiscountPrice"] ?? 0) > 0)
{
return true; // 发现已有折扣价,需要预警
}
}
return false;
}四、用法三:在客开中使用原生三态【是 / 否 / 取消】弹窗(带原生【是】【否】【取消】三个按钮)
典型业务场景:在单据界面点击客开按钮【创建收款单】(
BtnCreateRecBill)。
- 数据未修改:当前勾选的明细行(或整个单据)未发生变更,直接调用后台接口创建收款单,无需打扰用户;
数据发生修改(存在脏数据):弹出系统原生 350x105【保存确认】三态对话框(
ConfirmUIFormWebPart),提示用户:"当前单据存在记录修改,是否需要先保存?":
- 点【是】(
Confirmed):先调用页面原生保存(BtnSave_Click_Extend),保存成功后自动继续创建收款单;- 点【否】(
Denied):不调用保存,直接基于已有数据执行创建收款单;- 点【取消】或点右上角【X】:前端 100% 彻底静默拦截,原地停留,绝不执行任何保存或收款单创建!
// 示例:U9 建模设计器标准生成的客开按钮代码桩(WebPart_Extend.cs)
private void BtnCreateRecBill_Click_Extend(object sender, EventArgs e)
{
// 1. 优先检查:是否为用户在三态确认窗口点击后的回发
ConfirmResult result = U9ConfirmHelper.GetConfirmResult(this, "CreateRecBill");
if (result == ConfirmResult.Confirmed)
{
// ----------------------------------------------------
// 分支 1:用户点击了【是】➔ 先调用页面原生保存,成功后再创建收款单
// ----------------------------------------------------
this.BtnSave_Click_Extend(this, EventArgs.Empty);
// 若保存触发了校验异常(如必填项缺失或后端报错),立即中止后续逻辑
if (this.Model.ErrorMessage.hasErrorMessage)
{
return;
}
DoCreateRecBill(); // 保存成功,继续创建收款单
return;
}
else if (result == ConfirmResult.Denied)
{
// ----------------------------------------------------
// 分支 2:用户点击了【否】➔ 不调用保存,直接继续创建收款单
// ----------------------------------------------------
DoCreateRecBill();
return;
}
// ----------------------------------------------------
// 分支 3:用户点击了【取消】或点右上角【X】关闭弹窗
// 前端直接 return 绝不发起任何网络回发,界面原地停留,不会进入当前方法!
// ----------------------------------------------------
// 2. 正常点击时的业务逻辑:检查当前勾选的记录(或当前单据)是否存在脏数据改动
bool isModified = CheckSelectedRowsModified();
if (isModified)
{
// 存在未保存的数据变动,弹出系统原生 350x105 三态确认框(带【是】【否】【取消】三个按钮)
U9ConfirmHelper.ShowYesNoCancelConfirm(
this,
"CreateRecBill",
"当前单据存在记录修改,是否需要先保存?",
"保存确认",
targetControl: this.BtnCreateRecBill
);
return;
}
// 3. 数据没有发生过任何变动:无需弹窗提示,直接执行创建收款单
DoCreateRecBill();
}
/// <summary>
/// 检查当前勾选的记录是否发生过修改(或检查整个页面是否有脏数据)
/// </summary>
private bool CheckSelectedRowsModified()
{
// 方式一:精准检查当前选中的单据行是否有脏数据
// (U9 UIModel 中的 RecordState: Unchanged, Modified, Inserted, Deleted)
if (this.Model.Views["OrderLine"] != null)
{
foreach (IUIRecord row in this.Model.Views["OrderLine"].Records)
{
// 假设施图中有勾选字段(如 SelectRecord / Selected)
bool isSelected = Convert.ToBoolean(row["SelectRecord"] ?? false);
if (isSelected && row.RecordState != RecordState.Unchanged)
{
return true; // 勾选的行被修改过
}
}
}
// 方式二:通用检查当前页面主视图(表头)是否被修改
if (this.Model.Views[0].FocusedRecord != null &&
this.Model.Views[0].FocusedRecord.RecordState != RecordState.Unchanged)
{
return true;
}
return false;
}五、开发与调用注意事项
推荐显式传入
targetControl(保障局部刷新):- 无论客开按钮位于上方工具栏(Toolbar)还是下方卡片容器(Card),调用时强烈建议显式传入当前按钮自身(如
targetControl: this.BtnCreateRecBill); - 这样弹窗确认后系统会走 Ajax 局部刷新精准重新进入当前按钮的 Click 事件,杜绝整页重载闪烁。
- 无论客开按钮位于上方工具栏(Toolbar)还是下方卡片容器(Card),调用时强烈建议显式传入当前按钮自身(如
动作标识
actionKey保持唯一:- 同一页面若存在多个弹窗动作(如“覆盖计算确认”与“生成收款单确认”),请使用不同标识(如
"CalcDiscount"、"CreateRecBill"),防止上下文暂存数据相互覆盖。
- 同一页面若存在多个弹窗动作(如“覆盖计算确认”与“生成收款单确认”),请使用不同标识(如
二态与三态按需选择:
- 二态确认(
ShowConfirm,320×105):用于常规“确定继续 / 取消放弃”操作,界面展示原生【确定】与【取消】; - 三态确认(
ShowYesNoCancelConfirm,350×105):用于“二选一”业务分支(如“是先保存 / 否直接执行 / 取消放弃”),界面展示原生【是】、【否】、【取消】。
- 二态确认(
程序集引用依赖:
- 复制
U9ConfirmHelper.cs时,需确保客开项目已引用UBF.System.dll(位于Portal\bin,提供原生 UTF-8 DES 加密,杜绝弹窗乱码)。
- 复制
六、弹窗效果图示
- 二态弹窗:320x105 像素,经典小蓝问号,右下角【确定】与【取消】,原生弃审同款视觉体验;

- 三态弹窗:350x105 像素,经典小蓝问号,右下角【是】、【否】、【取消】,原生放弃同款视觉体验。

评论 (0)