PAYATHON 2026

Kentico 9 e-Commerce Checkout 添加步骤时类型转换失败

支付小周

结论

ShoppingCartStep 不是 Web Part,不能注册为普通 Web Part,也不能直接放进页面的 Web Part Zone。

出现报错是因为 Kentico 在加载“EntityUse Code”时,将对应的 ASCX 控件视为 Web Part,并尝试把它转换成 CMSAbstractWebPart。但该控件继承的是 ShoppingCartStep,与 CMSAbstractWebPart 没有可用的继承关系,所以转换失败。

正确的处理方式是:

  1. 保持 ASCX 控件继承 ShoppingCartStep。
  2. 使用 Kentico 9 结账页原有的购物车或结账 Web Part 作为宿主。
  3. 在宿主 Web Part 的步骤配置中注册自定义 ASCX 路径,不要把 ASCX 本身注册为 Web Part。
  4. 如果需要把控件直接拖入页面,必须另外编写一个继承 CMSAbstractWebPart 的包装 Web Part。不过,添加结账步骤通常不应采用这种方式。

为什么会出现类型转换错误

当前代码声明如下:

public partial class CMSModules_Ecommerce_Controls_ShoppingCart_ShoppingCartNonProfit
    : ShoppingCartStep
{
}

因此,运行时生成的 ASCX 类型是 ShoppingCartStep。普通 Kentico Web Part 的代码隐藏类则必须直接或间接继承:

CMS.PortalControls.CMSAbstractWebPart

将这个 ASCX 注册为 Web Part 后,Kentico 会执行类似下面的转换:

CMSAbstractWebPart webPart = (CMSAbstractWebPart)loadedControl;

由于 ShoppingCartStep 不是 CMSAbstractWebPart,系统便会报错:

Unable to cast object of type '...shoppingcartnonprofit_ascx'
to type 'CMS.PortalControls.CMSAbstractWebPart'

这个错误与 ShoppingCartCustomData 或 ProcessStep() 无关,问题出在控件的注册和加载方式上。

正确添加结账步骤

1. 不要把步骤控件注册为普通 Web Part

如果已经在 Kentico 的 Web Parts 应用中创建了指向该 ASCX 文件的“EntityUse Code”Web Part,请先从页面模板中移除它。

只删除页面上的 Web Part 实例,就可以消除加载时的类型转换异常。是否删除对应的 Web Part 定义,取决于它是否还有其他用途。

2. 以现有结账步骤为模板创建控件

建议复制 Kentico 9 项目中已有的 ShoppingCartStep 控件结构,包括:

  • ASCX 文件中的指令和控件标记;
  • 代码隐藏文件;
  • designer 文件;
  • 继承 ShoppingCartStep 的类;
  • 原有结账步骤使用的命名空间和引用。

ASCX 中的 Inherits 必须与代码隐藏类完全一致。例如:

<%@ Control Language="C#"
    AutoEventWireup="true"
    CodeFile="ShoppingCartNonProfit.ascx.cs"
    Inherits="CMSModules_Ecommerce_Controls_ShoppingCart_ShoppingCartNonProfit" %>

代码隐藏类继续继承 ShoppingCartStep:

public partial class CMSModules_Ecommerce_Controls_ShoppingCart_ShoppingCartNonProfit
    : ShoppingCartStep
{
    // Step implementation
}

不要为了消除转换错误而把基类改成 CMSAbstractWebPart。这样虽然满足了普通 Web Part 的加载要求,却会失去结账步骤需要的 ShoppingCart、ShoppingCartControl、ProcessStep() 和前后导航机制。

3. 在结账宿主 Web Part 中配置步骤

进入结账页面的 Design 模式,找到承载结账流程的购物车或结账 Web Part,然后打开其配置。

Kentico 9 的结账实现可能因项目模板、升级历史和定制情况而有所不同,但步骤配置通常会引用各个 ShoppingCartStep ASCX 控件。请在该配置中添加自定义步骤,并将控件路径指向你的 ASCX 文件,例如:

~/CMSModules/Ecommerce/Controls/ShoppingCart/ShoppingCartNonProfit.ascx

步骤顺序由结账宿主控件的配置决定,与页面树中新建页面的顺序无关。

具体字段名称和配置界面应以当前项目已有的结账步骤为准。可以先找到地址、配送或付款等现有步骤的 ASCX 路径,再用相同格式加入自定义控件。

代码调整示例

除了加载方式,原代码还有一个常见问题。Page_Load 会在每次回发时重新读取 ShoppingCartCustomData 并给文本框赋值,这可能覆盖用户刚提交的内容。初始化表单时应检查 IsPostBack。

using System;
using CMS.Ecommerce;

public partial class CMSModules_Ecommerce_Controls_ShoppingCart_ShoppingCartNonProfit
    : ShoppingCartStep
{
    private const string FEDERAL_TAX_ID = "FederalTaxID";
    private const string ENTITY_USE_CODE = "EntityUseCode";

    protected void Page_Load(object sender, EventArgs e)
    {
        lblTitle.Text = "Tax Exempt Order?";

        if (!IsPostBack)
        {
            object fedTaxId =
                ShoppingCart.ShoppingCartCustomData.GetValue("federaltaxid");

            object entityUseCode =
                ShoppingCart.ShoppingCartCustomData.GetValue("entityusecode");

            if (fedTaxId != null)
            {
                tbTaxID.Text = fedTaxId.ToString();
            }

            if (entityUseCode != null)
            {
                tbEntityUseCode.Text = entityUseCode.ToString();
            }
        }
    }

    public override void ButtonBackClickAction()
    {
        ShoppingCartControl.SetTempValue(FEDERAL_TAX_ID, tbTaxID.Text);
        ShoppingCartControl.SetTempValue(
            ENTITY_USE_CODE,
            tbEntityUseCode.Text
        );

        base.ButtonBackClickAction();
    }

    public override bool ProcessStep()
    {
        try
        {
            ShoppingCart.ShoppingCartCustomData.SetValue(
                "federaltaxid",
                tbTaxID.Text.Trim()
            );

            ShoppingCart.ShoppingCartCustomData.SetValue(
                "entityusecode",
                tbEntityUseCode.Text.Trim()
            );

            if (!ShoppingCartControl.IsInternalOrder)
            {
                ShoppingCartInfoProvider.SetShoppingCartInfo(ShoppingCart);
            }

            return true;
        }
        catch (Exception ex)
        {
            lblError.Visible = true;
            lblError.Text = ex.Message;
            return false;
        }
    }
}

如果希望用户点击“返回”后再次进入该步骤时,仍能恢复尚未写入数据库的值,初始化时还需要读取 ShoppingCartControl 中的临时值。具体取值 API 请参考当前 Kentico 9 项目中已有结账步骤的实现,因为不同修订版本或定制代码使用的辅助方法可能不同。

需要检查的事项

控件类型必须与加载位置匹配

这两类控件不能混用:

普通页面 Web Part
    → 继承 CMSAbstractWebPart
    → 放入 Web Part Zone

结账流程步骤
    → 继承 ShoppingCartStep
    → 由结账宿主控件加载

ASCX 路径必须正确

确认步骤配置引用了实际文件,并使用应用相对路径:

~/CMSModules/Ecommerce/Controls/ShoppingCart/ShoppingCartNonProfit.ascx

同时检查文件名大小写、所在目录,以及 ASCX 中的 CodeFile 和 Inherits。

自定义数据键应保持一致

写入和读取时必须使用完全相同的键:

"federaltaxid"
"entityusecode"

建议将键定义为常量,避免出现大小写或拼写不一致。原代码中的:

private const string PAYMENT_OPTION_ID = "OrderPaymenOptionID";

似乎缺少字母 t,而且当前代码没有使用这个常量,可以将其删除或修正。

不要依赖新建页面来定义步骤

在页面树的 Checkout 节点下新建页面,不会自动把该页面变成 ShoppingCartStep。结账步骤通常由结账 Web Part 在内部加载和切换。页面树用于承载整个流程,步骤控件则负责流程中的具体界面和处理逻辑。

因此,解决这个错误不需要强制转换类型,也不需要修改 ProcessStep()。应撤销错误的 Web Part 注册,让现有结账宿主按照 ShoppingCartStep 的方式加载 ShoppingCartNonProfit.ascx。

备注:内容仅供参考。