【071】使用系统原生确认弹窗提示用户信息
侧边栏壁纸
  • 累计撰写 72 篇文章
  • 累计收到 1 条评论

【071】使用系统原生确认弹窗提示用户信息

竹秋廿九
2026-09-03 / 0 评论 / 9 阅读 / 正在检测是否收录...
在开发过程中,很多操作需要先在后台检查复杂的业务条件,如果满足某些预警条件,才弹出确认窗口向用户二次确认,点确定继续执行,点取消直接中止;条件不满足则直接执行业务,无需弹窗。
之前【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.dllUFSoft.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;
}

五、开发与调用注意事项

  1. 推荐显式传入 targetControl(保障局部刷新)

    • 无论客开按钮位于上方工具栏(Toolbar)还是下方卡片容器(Card),调用时强烈建议显式传入当前按钮自身(如 targetControl: this.BtnCreateRecBill);
    • 这样弹窗确认后系统会走 Ajax 局部刷新精准重新进入当前按钮的 Click 事件,杜绝整页重载闪烁。
  2. 动作标识 actionKey 保持唯一

    • 同一页面若存在多个弹窗动作(如“覆盖计算确认”与“生成收款单确认”),请使用不同标识(如 "CalcDiscount""CreateRecBill"),防止上下文暂存数据相互覆盖。
  3. 二态与三态按需选择

    • 二态确认ShowConfirm,320×105):用于常规“确定继续 / 取消放弃”操作,界面展示原生【确定】与【取消】;
    • 三态确认ShowYesNoCancelConfirm,350×105):用于“二选一”业务分支(如“是先保存 / 否直接执行 / 取消放弃”),界面展示原生【是】、【否】、【取消】。
  4. 程序集引用依赖

    • 复制 U9ConfirmHelper.cs 时,需确保客开项目已引用 UBF.System.dll(位于 Portal\bin,提供原生 UTF-8 DES 加密,杜绝弹窗乱码)。

六、弹窗效果图示

  • 二态弹窗:320x105 像素,经典小蓝问号,右下角【确定】与【取消】,原生弃审同款视觉体验;

image.png

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

image.png

0

评论 (0)

取消