Skip to content

搜索对话框与关系网格

搜索窗口和关系网格是 Aras 客户端定制中最容易“在一个版本可用、升级后失效”的区域。可靠的做法是把业务条件、服务端查询和版本化 UI adapter 分开。

适用版本: 搜索与网格业务模式可跨版本复用;Dialog、frame 和 grid 调用仅适用于完成兼容测试的目标 Release。

执行层: 客户端 adapter + 服务端 IOM 查询;复杂过滤不能只留在浏览器。

主要依据: R31 原厂手册 Unit 8、Unit 10 与 Unit 20,11.0 SP9 旧手册 Unit 11、Appendix B / D / F,以及本项目提供的 SearchDialog 和关系网格素材。

素材边界

原始素材同时包含多套 Dialog API、内部搜索对象、frame DOM 和实验性 grid 对象。它们是项目经验,不是统一的公共接口。本页只发布能安全解释的模式;具体打开窗口的 API 必须在目标 Release 的客户端代码和回归测试中确认。

固定搜索条件

标注:Classic client compatibility

在字段或关系的 OnSearchDialog 事件中,可以返回一个过滤描述对象。属性名必须来自目标 ItemType,过滤值必须来自可信上下文。

js
function buildFixedSearchFilter(contextItem) {
  if (!contextItem) {
    return {};
  }

  const ownerName = contextItem.getPropertyAttribute(
    "owner_id",
    "keyed_name"
  ) || "";

  if (!ownerName) {
    return {};
  }

  return {
    owner_id: {
      filterValue: ownerName,
      isFilterFixed: true,
    },
    is_current: {
      filterValue: "1",
      isFilterFixed: true,
    },
  };
}

return buildFixedSearchFilter(document.thisItem);

isFilterFixed: true 只是防止用户在当前搜索 UI 中修改条件。服务端仍会按登录用户的权限过滤结果。

用 adapter 打开搜索窗口

标注:Classic client compatibility

不要把 modalDialogHelperArasModules.Dialog 和不同返回结构写进每个业务 Method。页面只依赖一个项目级 adapter:

js
async function chooseRelatedItem(searchDialog, contextItem) {
  if (!searchDialog || typeof searchDialog.open !== "function") {
    throw new Error("当前 Release 没有可用的 SearchDialog adapter。");
  }

  const result = await searchDialog.open({
    itemTypeName: "Example Reference",
    multiselect: false,
    filters: buildFixedSearchFilter(contextItem),
  });

  if (!result) {
    return null;
  }

  if (!/^[0-9a-f]{32}$/i.test(result.id || "")) {
    throw new Error("搜索窗口返回了无效的 Item ID。");
  }

  return result;
}

adapter 负责消化具体版本的差异,并统一返回:

ts
type SearchResult = {
  id: string;
  keyedName: string;
  item?: unknown;
};

升级时只替换 adapter,不改业务 Method。

adapter 的验收场景

  • 单选、双击和确认按钮返回相同结构。
  • 点击取消返回 null,不抛异常。
  • 搜索无结果、无 Get 权限、会话超时均有明确反馈。
  • 多选始终返回数组,单选始终返回对象或 null
  • 固定过滤条件不能被 UI 清除,但服务端 Permission 仍然生效。

不拼接 AML 来限制搜索

标注:Stable

原始素材把当前行的属性直接拼进 where,再生成很长的 idlist。这既可能产生 AML/SQL 注入,也容易触发请求长度和性能问题。

把复杂条件放到 Server Method,并通过 IOM 属性方法构建查询:

csharp
Innovator inn = this.getInnovator();

string contextId = this.getProperty("context_id", "");
if (!System.Text.RegularExpressions.Regex.IsMatch(
        contextId,
        "^[0-9A-Fa-f]{32}$"))
{
    return inn.newError("context_id 格式无效。");
}

Item query = inn.newItem("Example Relationship", "get");
query.setAttribute("select", "id,related_id");
query.setProperty("source_id", contextId);
query.setProperty("is_selectable", "1");

Item result = query.apply();
if (result.isError())
{
    return inn.newError("候选数据查询失败,请联系管理员并提供操作时间。");
}

return result;

如果还要排除当前行,应把当前 related Item ID 作为另一个已验证参数传入,并使用 IOM 条件方法或目标版本已文档化的查询能力。不要把显示名称放进 SQL 风格的 where 字符串。

打开项目表单或业务对话框

标注:Classic client compatibility

打开 Form 前需要完成三件事:

  1. 用 IOM 查询并唯一定位 Form,处理 0 条、多条和错误结果。
  2. 明确窗口是只读还是编辑;编辑能力仍受 Item 锁和 Permission 控制。
  3. 通过项目级 Dialog adapter 打开,并处理 Promise 的返回值。
js
function getFormDefinition(inn, formName) {
  const query = inn.newItem("Form", "get");
  query.setAttribute("select", "id,width,height");
  query.setProperty("name", formName);

  const result = query.apply();
  if (result.isError()) {
    throw new Error("目标表单查询失败,请联系管理员并提供操作时间。");
  }

  if (result.getItemCount() !== 1) {
    throw new Error(`Form 必须唯一:${formName}`);
  }

  return result.getItemByIndex(0);
}

async function openBusinessDialog(dialogAdapter, currentItem) {
  const inn = currentItem.getInnovator();
  const form = getFormDefinition(inn, "Example Review Dialog");

  return dialogAdapter.openForm({
    title: "审核信息",
    formId: form.getID(),
    item: currentItem,
    editMode: false,
    width: Number(form.getProperty("width", "800")),
    height: Number(form.getProperty("height", "500")),
  });
}

dialogAdapter.openForm 是项目契约,不代表 Aras 原生函数名。它应该在一个单独文件中调用目标 Release 实际支持的 Dialog API。

深链接

标注:Classic client compatibility

某些 Release 支持通过 StartItem 打开指定 Item。不要保存完整环境 URL;由部署配置提供 Client 根地址。

js
function buildStartItemUrl(clientBaseUrl, itemTypeName, itemId) {
  if (!/^[0-9a-f]{32}$/i.test(itemId || "")) {
    throw new Error("Item ID 格式无效。");
  }

  const url = new URL(clientBaseUrl);
  url.searchParams.set("StartItem", `${itemTypeName}:${itemId}`);
  return url.toString();
}

发布前验证目标 Release 是否仍支持该路由,并确认未登录、无 Get 权限和 Item 不存在时的行为。

关系行:先更新缓存,再渲染

标注:Stable(Item DOM) + Private API(grid 绘制)

右键动作常见的错误是:先 apply() 创建子 Item,再把 ID 填进尚未保存的父 Item。用户如果取消父 Item,会留下孤儿数据。

优先选择以下事务模型:

  1. 在父 Item 的客户端 DOM 中创建或更新关系。
  2. 父 Item 保存时,让关系与父 Item 在同一次 Aras 事务中提交。
  3. 若子 Item 必须提前落库,服务端 Method 应同时完成创建、关联和失败回滚,并有幂等键。
  4. 网格即时显示由 adapter 负责;adapter 失效不能影响数据正确性。

不要把“当前循环的最后一行”当作目标行。所有操作都必须用事件提供的 relationship ID 唯一定位。

当前搜索条件与导出

标注:Private API

_getSearchQueryAML() 的下划线已经提示它是内部实现。若项目确实要导出“当前搜索结果”,建议采用以下契约:

  • adapter 从当前 Release 读取结构化筛选条件。
  • 只允许白名单属性、运算符、排序和分页。
  • 将条件作为数据传给服务端,而不是把一段 AML 塞进另一段 XML 字符串。
  • 服务端重新按登录用户权限执行查询。
  • 返回文件流或纯文本结果,不返回任意 HTML。

不要使用 document.write

把 Server Method 返回的 message 写入新窗口会把未转义内容当作 HTML 执行,形成 XSS。展示文本请用 textContent,下载内容请使用受控的文件响应。

网格样式

标注:Private API

直接修改 grid_Experimental.layout.cells、固定列号或内部 headerStyles 只适用于一个确定的客户端构建。若只是提示可编辑列,优先考虑:

  • Form/TGV/CUI 能提供的配置;
  • 属性元数据中的可编辑状态;
  • 项目自有 CSS class 与受控渲染 extension;
  • 最后才是按 Release 隔离的 grid adapter。

adapter 测试至少要覆盖列重排、隐藏列、多语言 label、空网格、分页和升级后的 DOM 变化。

为什么不要这样做

原始模式标注风险
两套 SearchDialog API 写在同一 MethodReject两套代码都会执行,返回结构也不一致
直接访问 parent.iteminDom、固定 iframe IDPrivate API强依赖窗口层级和页面加载顺序
把属性拼进 where="..."RejectAML/SQL 注入、引号错误、权限边界不清
生成数千个 ID 的 idlistRejectURL/请求过长、查询计划差、无法分页
grid.items_Experimental.set(...) 到处出现Private API升级成本扩散,UI 与数据可能不同步
直接输出服务端 HTMLRejectXSS 与内容注入

验证清单

  • [ ] Dialog adapter 标明目标 Release 和返回契约。
  • [ ] 所有 ID、类型名和过滤属性都经过格式或白名单校验。
  • [ ] 用户输入没有拼进 AML、XPath、SQL 或 HTML。
  • [ ] 取消父 Item 不会遗留孤儿记录。
  • [ ] 无权限用户无法通过直接 AML 绕过 UI 过滤。
  • [ ] grid adapter 不可用时,数据仍能正确保存和刷新。

官方延伸阅读

本站内容仅供学习与参考