搜索对话框与关系网格
搜索窗口和关系网格是 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,过滤值必须来自可信上下文。
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
不要把 modalDialogHelper、ArasModules.Dialog 和不同返回结构写进每个业务 Method。页面只依赖一个项目级 adapter:
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 负责消化具体版本的差异,并统一返回:
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 属性方法构建查询:
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 前需要完成三件事:
- 用 IOM 查询并唯一定位 Form,处理 0 条、多条和错误结果。
- 明确窗口是只读还是编辑;编辑能力仍受 Item 锁和 Permission 控制。
- 通过项目级 Dialog adapter 打开,并处理 Promise 的返回值。
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 根地址。
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,会留下孤儿数据。
优先选择以下事务模型:
- 在父 Item 的客户端 DOM 中创建或更新关系。
- 父 Item 保存时,让关系与父 Item 在同一次 Aras 事务中提交。
- 若子 Item 必须提前落库,服务端 Method 应同时完成创建、关联和失败回滚,并有幂等键。
- 网格即时显示由 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 写在同一 Method | Reject | 两套代码都会执行,返回结构也不一致 |
直接访问 parent.item、inDom、固定 iframe ID | Private API | 强依赖窗口层级和页面加载顺序 |
把属性拼进 where="..." | Reject | AML/SQL 注入、引号错误、权限边界不清 |
生成数千个 ID 的 idlist | Reject | URL/请求过长、查询计划差、无法分页 |
grid.items_Experimental.set(...) 到处出现 | Private API | 升级成本扩散,UI 与数据可能不同步 |
| 直接输出服务端 HTML | Reject | XSS 与内容注入 |
验证清单
- [ ] Dialog adapter 标明目标 Release 和返回契约。
- [ ] 所有 ID、类型名和过滤属性都经过格式或白名单校验。
- [ ] 用户输入没有拼进 AML、XPath、SQL 或 HTML。
- [ ] 取消父 Item 不会遗留孤儿记录。
- [ ] 无权限用户无法通过直接 AML 绕过 UI 过滤。
- [ ] grid adapter 不可用时,数据仍能正确保存和刷新。
