☰
PDMS-AVEVA-E3D二次开发教程(12):PML.NET 让 PML 调用 C#——PMLNetCallable、IMPORT 与 USING NAMESPACE 的三条限制
2026/10/17 7:06:24 网站建设 项目流程

版本声明:本篇基线为AVEVA E3D Design 3.1(文档版本线存在 1.1 / 2.1 / 3.1,教程统一以 3.1 为准);前代产品AVEVA PDMS 自 2024 年 4 月 1 日起不再销售、不再提供支持。PML.NET 的机制、PMLNetCallable标注规则、IMPORT/USING NAMESPACE语法与官方三条限制,均出自官方 .NET 定制手册(NCUG 5.07.x)。代码示例中凡涉及命名空间与特性名的地方,均按官方记载构造并标注"需在本地 E3D Design 环境验证"。

一句话结论

PML.NET 的核心是三重闸门 + 一条禁令:程序集要标注PMLNetCallable才会被加载、类与方法要标注才会可见、命名空间要一次写全才能解析;而那条禁令是——C# 永远不能反向直接调用 PML,只能通过事件机制通知回去。

〇、认知问题(读完本篇你应该能回答)

  1. PML.NET 是什么?官方给它的定位原文大意如何?
  2. PMLNetCallable标注的作用范围有几层?漏标一层会怎样?
  3. IMPORT到哪里去找程序集?PMLNetEngine在其中扮演什么角色?
  4. USING NAMESPACE为什么"不允许写部分命名空间"?写错了会发生什么?
  5. 官方列出的三条限制分别是什么?每一条对架构设计意味着什么?
  6. 为什么 DB 引用可以传给 .NET,而DIRECTION/ORIENTATION不行?

一、机制解析

1.1 PML.NET 的官方定位

官方对 PML.NET 的定位原文大意是:PML.NET 是对 PML 的一项增强,让 PML 程序员可以调用 .NET 代码,从而把 .NET 编译代码的优势与 PML 的灵活性结合起来。官方同时说明:.NET 定制有两条路径,一条是 .NET Addin,另一条是 PML.NET;两者都提供"在运行时把 .NET 程序集(dll)动态载入模块"的机制。

把这两句放在一起读,就能得到 PML.NET 的准确边界:

  • 它是**“PML 主动调 C#”**的方向,不是"C# 主动调 PML"(后者根本不允许,见 1.5);
  • 它是运行期动态加载,不是编译期引用——所以它天然适合"按需增强某段 PML 流程"。

选型判断:功能由用户点界面发起 → 走 Addin;功能由某段 PML 流程发起 → 走 PML.NET。第 09 篇已经建立了这个判断,本篇是把后一半讲透。

1.2PMLNetCallable:三重闸门

官方给出两条紧密相关的规则:

  1. 只有被标注PMLNetCallable的程序集才会被加载;
  2. 只有标注为PMLNetCallable的类与方法才会被考虑。

合起来就是三重闸门:

层标注对象漏标的后果
1程序集整个 dll 不被加载
2类类不可见,无法实例化
3方法方法不可见,调用时报"找不到方法"

这是本系列铁律 3 的来源:只有标注PMLNetCallable的类与方法才会被 PML.NET 加载可见。它导致的典型症状是"方法明明写了、编译过了,PML 里就是调不到"——排查顺序永远是从外到内:程序集标了吗?类标了吗?方法标了吗?

另外注意:构造函数也属于"类的方法"范畴。如果类本身可见但实例化失败,先看构造函数是否也可被调用。

1.3IMPORT与PMLNetEngine

程序集通过IMPORT语法加载,由PMLNetEngine用反射加载。官方的查找路径是:

  • %AVEVA_DESIGN_EXE%目录;
  • 该目录的子目录;
  • 映射驱动器或UNC 路径。

由此得到三条很实用的工程结论:

  1. "放在哪"比"怎么调"更容易出错。dll 放在%AVEVA_DESIGN_EXE%或其子目录下,是最省事的做法。
  2. IMPORT里写的是程序集,不是路径——查找由引擎按上述路径完成。所以同一个IMPORT语句在不同机器上可能成功、可能失败,取决于 dll 是否落在查找路径里。
  3. UNC 路径可用意味着企业可以把 PML.NET 程序集放到共享位置(与第 09 篇CAF_ADDINS_PATH支持 UNC 是同一套部署思路)。

PMLNetEngine用反射加载,还带来一个使用上的含义:PML 侧的"对象"实际是 .NET 对象的代理。你在 PML 里object出来的东西,行为依赖反射解析到的方法;反射解析不到(未标注、命名空间没写全、方法签名不匹配)就会失败在运行期,而不是编译期。

1.4USING NAMESPACE的完整性与大小写不敏感

命名空间用USING NAMESPACE '<string>'指定,官方的两点规定:

  1. 大小写不敏感;
  2. 不允许书写"部分命名空间"。

第 2 点官方给了明确的错误示范:写USING NAMESPACE 'Aveva.Core',然后再用object namespace.NETObject()形式去取子对象,会报错。

这就是本系列铁律 4 的来源:USING NAMESPACE必须写完整命名空间,不允许部分命名空间。它的原因在于解析机制——USING NAMESPACE指定的是一整段可解析的命名空间,不是一个"前缀";你把父级当成了前缀,解析时拼出来的类型名就不存在。

工程含义:命名空间一律从 C# 源码里复制,不要凭印象缩写。写错大小写无所谓(大小写不敏感),少写一段就必错。

1.5 官方列出的三条限制

官方明确列出了三条限制,逐条看它们对架构的影响:

限制一:无法从 C# 直接调用 PML,只能通过事件机制反向通知。

这一条是最硬的架构约束。它意味着 PML.NET 是单向调用:PML 可以调 C#,但 C# 干活干到一半想"回调 PML 帮我做件事",办不到。可行的做法只有事件机制:C# 侧触发事件,PML 侧在合适的位置响应。这是本系列铁律 9 的来源:C# 不能直接调用 PML,只能通过事件机制反向通知。

工程结论:把"谁主导流程"想清楚再动手。如果业务需要"C# 处理完再让 PML 干活",那么业务的驱动力必须放在 PML 侧——PML 调 C# 做一段计算,拿回结果,再自己继续;而不是让 C# 反过来指挥 PML。

限制二:模块切换不保持 .NET 对象。

这意味着你在设计模块里创建的 .NET 对象,切到别的模块再回来,不会自动保留。工程含义:

  • 不要把跨模块的状态只放在 .NET 对象的内存字段里;
  • 需要保活的状态应落到设置或数据库中;
  • 把这条限制当成"生命周期设计的前提",而不是当成 bug 去绕。

限制三:不能把DIRECTION、ORIENTATION等 PML 对象传给 .NET;但 DB 引用可以以数组或字符串形式传递。

这一条把"哪些数据能过桥"划得很清楚:

  • 不能过桥的:DIRECTION(方向)、ORIENTATION(方位)这类 PML 侧的对象;
  • 可以过桥的:DB 引用,而且有两种形式——数组或字符串。

工程含义非常直接:跨桥的数据要么是 .NET 原生类型(数值、字符串、布尔),要么是 DB 引用的数组/字符串形式。如果业务需要传方向或方位,正确做法是在 PML 侧把它们拆成一组数值(分量)传过去,或者干脆把元素引用传过去、让 C# 自己从数据库读到的几何里取方向。

1.6 官方力证:ML.NET 模型经PMLNETCALLABLE导入

官方发布说明里有一条非常有力的实证:E3D Design 3.1.5.0(2022-04)版本引入了"通过PMLNETCALLABLE对象把用 Microsoft ML.NET 训练好的机器学习模型导入 AVEVA PML"的能力(入口在 Model 模块的 Developer 标签页)。

这条事实的价值在于:它证明了PMLNETCALLABLE不是一个只在文档里存在的标注,而是官方自己用来交付新功能的机制。当你想说服同事"这条路是官方主推方向"时,这就是最硬的证据。

二、完整代码与逐行剖析

2.1 C# 侧:被标注的类

// MyPmlLib.cs// 代码来源性质:按官方 NCUG 5.07 记载的 PMLNetCallable 标注规则构造,// 需在本地 E3D Design 环境验证。// 注意:PML.NET 相关命名空间与特性全名,请以本机程序集与// Samples.zip 中的 PMLNetExample 工程为准(社区帖中提到的// Aveva.Pdms.PMLNet -> Aveva.Core.PMLNet 属社区来源,非官方对照表)。usingSystem;// 【闸门 1】程序集级标注:没有它,整个 dll 不会被加载[assembly:PMLNetCallable()]namespaceMyCompany.PmlNet{/// <summary>/// 【闸门 2】类级标注:没有它,类不可见、无法实例化/// </summary>[PMLNetCallable()]publicclassEngineeringCalc{/// <summary>/// 【闸门 3】方法级标注:没有它,方法调不到/// 构造函数同样需要可见性,否则实例化失败/// </summary>[PMLNetCallable()]publicEngineeringCalc(){}/// <summary>纯数值计算:最适合放在 .NET 侧的一类工作</summary>[PMLNetCallable()]publicdoubleSum(doublea,doubleb){returna+b;}/// <summary>字符串处理:把复杂规则放到编译代码里做</summary>[PMLNetCallable()]publicstringNormalize(stringraw){if(string.IsNullOrEmpty(raw)){return"";}returnraw.Trim().ToUpperInvariant();}/// <summary>/// 接收 DB 引用的字符串形式(限制三:DB 引用可以字符串形式传递;/// DIRECTION / ORIENTATION 不能传)。/// 这里只回显,实际业务可在此按引用去查数据库。/// </summary>[PMLNetCallable()]publicstringEchoRef(stringdbrefText){return"received: "+dbrefText;}}}

2.2 逐行剖析

  • [assembly: PMLNetCallable()]:程序集级标注,闸门 1。官方规则是"只有被标注PMLNetCallable的程序集才会被加载"。少了这一行,.cs里其他标注再齐也没用。
  • namespace MyCompany.PmlNet:命名空间的全名要在 PML 侧被完整地写进USING NAMESPACE,所以命名规则要短、稳、无歧义。
  • [PMLNetCallable()] public class EngineeringCalc:类级标注,闸门 2。类必须是public,否则反射看不到。
  • [PMLNetCallable()] public EngineeringCalc():构造函数也要标注。PML 侧要object出实例,走的就是构造函数;这一条最容易漏。
  • [PMLNetCallable()] public double Sum(double a, double b):方法级标注,闸门 3。参数与返回值都用 .NET 原生类型(double),这是最稳的跨桥数据形态。
  • [PMLNetCallable()] public string Normalize(string raw):字符串处理。PML 侧字符串与 .NETstring能直接对应,适合把"复杂字符串规则"从 PML 挪到 C#。
  • [PMLNetCallable()] public string EchoRef(string dbrefText):演示限制三的合法方向——DB 引用以字符串形式过桥。参数是string而不是某个 DB 类型,这是刻意为之。

2.3 PML 侧:IMPORT/USING NAMESPACE/ 实例化与调用

-- 12-01-call-dotnet.pml —— 从 PML 侧调用上面那个 .NET 类 -- 代码来源性质:按官方 NCUG 5.07 记载的 IMPORT / USING NAMESPACE 语法构造, -- 需在本地 E3D Design 环境验证。 -- 前置条件:MyPmlLib.dll 位于 %AVEVA_DESIGN_EXE% 目录或其子目录下 -- (官方查找路径:%AVEVA_DESIGN_EXE% 目录、其子目录、映射驱动器或 UNC 路径) -- 步骤 1:装载程序集。写的是程序集,不是完整路径; -- 实际从哪里找由 PMLNetEngine 按上述查找路径解析。 import 'MyPmlLib.dll' -- 步骤 2:指定命名空间。必须写完整命名空间(不允许部分命名空间); -- 大小写不敏感,但少写一段必错。 using namespace 'MyCompany.PmlNet' -- 步骤 3:实例化。形式为 object <完整命名空间>.<类名>() !calc = object MyCompany.PmlNet.EngineeringCalc() -- 步骤 4:调用被标注的方法,取回值 !sum = !calc.Sum(12.5, 7.5) q var !sum !txt = !calc.Normalize(' branch-1001 ') q var !txt -- 步骤 5:把 DB 引用以字符串形式传过去(限制三的合法用法) !refText = !!ce.name !echo = !calc.EchoRef(!refText) q var !echo

逐行剖析

  • import 'MyPmlLib.dll':IMPORT装载程序集。官方查找路径是%AVEVA_DESIGN_EXE%目录、其子目录、映射驱动器或 UNC 路径。所以部署位置决定成败:dll 不在查找路径里,IMPORT就会失败。
  • using namespace 'MyCompany.PmlNet':必须与 C# 的namespace全名逐段一致(大小写不敏感)。官方明确不允许部分命名空间——不能只写MyCompany。
  • !calc = object MyCompany.PmlNet.EngineeringCalc():实例化形式为object <完整命名空间>.<类名>()。这里注意:命名空间在全名形式下一次写全,而不是"先用父级再用子对象"。官方给出的错误示范正是"USING NAMESPACE 'Aveva.Core'+object namespace.NETObject()"这种写法。
  • !calc.Sum(12.5, 7.5):调用被标注的方法。参数是 .NET 原生类型,这是最稳的过桥形态。
  • !calc.Normalize(' branch-1001 '):字符串参数。PML 的单引号字符串直接对应 .NETstring。
  • !refText = !!ce.name+!calc.EchoRef(!refText):把 DB 引用以字符串形式传给 .NET——这正是官方限制三允许的方向。如果你要传的是DIRECTION/ORIENTATION,这条路走不通,必须改成"传分量数值"或"传元素引用让 C# 自己读"。
  • q var !sum等:把结果输出到 Command Window 核对。第一版验证不要做复杂逻辑,先确认"调用通了、值对了"。

2.4 跨桥数据形态对照表

数据能否过桥推荐形态
数值(double/int)能直接用 .NET 原生数值类型
字符串能PML 单引号字符串 ↔ .NETstring
布尔能对应 .NET 布尔类型
DB 引用能数组或字符串形式(官方明确)
DIRECTION/ORIENTATION等 PML 对象不能拆成分量数值传,或改传元素引用让 C# 侧自己读

设计原则:把跨桥接口设计成"传原生类型 + 传引用"的组合——引用过桥、数值过桥,几何对象不过桥。这样接口最稳,也最符合官方的限制说明。

2.5 命名空间写错时怎么自查

官方给出的错误示范值得单独走一遍,因为这是最有教育意义的失败案例:

-- 【错误示范,不要照抄】 -- 1) 只写了父级命名空间 using namespace 'Aveva.Core' -- 2) 然后试图用"父级 + 子对象"的形式取类型 !obj = object namespace.NETObject()

现象:第 2 步解析失败。原因:USING NAMESPACE指定的必须是完整命名空间,不允许用"部分命名空间"再去拼子对象。正确做法:把完整命名空间一次写全(例如using namespace 'Aveva.Core.某一个完整命名空间'这样的形态),实例化时也用完整命名空间限定。

三、常见报错与排查

3.1 方法"写了却调不到"

  • 现象:C# 编译通过,PML 侧调用报"找不到方法/对象不存在"。
  • 原因:三重闸门漏了一层——程序集没标、类没标、或方法没标PMLNetCallable;也可能是构造函数没标导致实例化失败。
  • 排查:从外到内逐层核对:①[assembly: PMLNetCallable()]在不在;②类上有没有标注;③目标方法与构造函数有没有标注;④方法是否public。

3.2IMPORT失败

  • 现象:IMPORT报错或后续调用全部失败。
  • 原因:dll 不在查找路径里。
  • 排查:官方查找路径是%AVEVA_DESIGN_EXE%目录、其子目录、映射驱动器或 UNC 路径。先把 dll 拷到%AVEVA_DESIGN_EXE%目录下验证能否工作,再考虑放到子目录或共享路径。确认AVEVA_DESIGN_EXE这个环境变量本身是对的——它是查找路径的基准。

3.3 命名空间解析失败

  • 现象:USING NAMESPACE或实例化时报解析错误。
  • 原因:写了"部分命名空间"(官方明确不允许);或命名空间的段数不对。
  • 排查:从 C# 源码里复制namespace全名;确认它被一次性写全;大小写可以不管(官方明确大小写不敏感),但段数必须一致。

3.4 传DIRECTION/ORIENTATION报错

  • 现象:把 PML 的方向/方位对象传给 .NET 时失败。
  • 原因:官方限制三——不能把DIRECTION、ORIENTATION等 PML 对象传给 .NET。
  • 排查:改成传数值分量(把方向拆成 x/y/z 三个数);或改为传元素引用(DB 引用可以数组或字符串形式过桥),由 C# 侧自行取几何信息。

3.5 想在 C# 里"回调 PML"

  • 现象:C# 侧处理到一半需要 PML 帮忙,发现无从下手。
  • 原因:官方限制一——无法从 C# 直接调用 PML。
  • 排查:改用事件机制反向通知:C# 侧触发事件,PML 侧响应。更根本的做法是重新划分职责边界:把流程主导权交给 PML,让 C# 只做"被调用的一段计算"。

3.6 切模块后对象失效

  • 现象:切换模块回来后,之前创建的 .NET 对象不可用或状态丢失。
  • 原因:官方限制二——模块切换不保持 .NET 对象。
  • 排查:不要依赖 .NET 实例字段做跨模块状态存储;把状态落到设置或数据库中,需要时重建对象。

四、动手练习

  1. 跑通一次最小调用
    用 2.1 的 C# 类与 2.3 的 PML 脚本,完成Sum(12.5, 7.5)的调用并把结果输出到 Command Window。
    判定标准:输出的计算结果正确;Normalize的返回值为大写且已去首尾空格。

  2. 三重闸门逐层拆除实验
    依次去掉:①程序集标注;②类标注;③方法标注。每次只改一处,重新编译部署并调用。
    判定标准:能给出三种不同层次的失败现象(完全没加载 / 类不可见 / 方法找不到),并说明每层的排查动作。

  3. 完整的命名空间 vs 部分命名空间
    先把USING NAMESPACE写成只含父级的形式(复现官方错误示范),再改成完整命名空间。
    判定标准:能准确描述两次运行的差异,并说明"为什么部分命名空间不允许"。

  4. DB 引用过桥 + 方向拆分量
    把当前元素的名称以字符串形式传给 .NET 并回显;再把一个方向拆成三个数值分量传给 .NET 求和。
    判定标准:两次都能取回正确结果;能说明"为什么方向要拆分量而引用可以直接传"(依据官方限制三)。

五、小结与下一篇预告

本篇的全部内容可以压成一张卡片:

  • 三重闸门:程序集 → 类 → 方法(含构造函数)都要标PMLNetCallable;
  • 装载路径:IMPORT装程序集,PMLNetEngine反射加载,查找路径是%AVEVA_DESIGN_EXE%及其子目录、映射驱动器或 UNC;
  • 命名空间:USING NAMESPACE大小写不敏感但必须写全;
  • 三条限制:C# 不能直接调 PML(只能事件反向通知)、模块切换不保持 .NET 对象、DIRECTION/ORIENTATION不能过桥而 DB 引用可以。

下一篇13 出图自动化:从"数据"转向"交付物"。讲 E3D 从模型自动生成图纸与报表的官方定位、AVEVA Isodraft与Isogen的等轴测链路、Draw 模块 DWG 导出的真实交付实务(含社区反馈的字体格式问题),以及批量化出图的组织方式。需要提前说明的是:官方公开文档中未找到 Draft / Isodraft 的批量化 PML API 章节,下一篇会如实交代这一点并给出不依赖虚构 API 的工程做法。

FAQ

Q1:PML.NET 是什么?官方给它的定位原文大意如何?
官方把 PML.NET 定位为"对 PML 的一项增强,使 PML 程序员能够调用 .NET 代码",从而把 .NET 编译代码的优势与 PML 结合起来。官方同时说明 .NET 定制有两条路径——.NET Addin 与 PML.NET——两者都提供"在运行时把 .NET 程序集(dll)动态载入模块"的机制。因此 PML.NET 的方向是"PML 主动调 C#"、方式是运行期动态加载;而"C# 主动调 PML"是不被允许的。

Q2:PMLNetCallable标注的作用范围有几层?漏标一层会怎样?
有三层:程序集、类、方法(构造函数属于方法一层,同样需要标注)。官方的两条规则是"只有被标注PMLNetCallable的程序集才会被加载"与"只有标注为PMLNetCallable的类与方法才会被考虑"。漏标程序集则整个 dll 不加载;漏标类则类不可见、无法实例化;漏标方法(或构造函数)则调用时报"找不到"。排查时必须从外到内逐层核对。

Q3:IMPORT到哪里去找程序集?PMLNetEngine在其中扮演什么角色?
程序集通过IMPORT语法加载,由PMLNetEngine用反射加载;官方给出的查找路径是%AVEVA_DESIGN_EXE%目录、其子目录、映射驱动器或 UNC 路径。所以IMPORT里写的是程序集名,实际从哪里加载由引擎按上述路径解析——部署位置直接决定成败(先放进%AVEVA_DESIGN_EXE%目录是最省事的验证手段)。因为加载是反射完成的,PML 侧拿到的其实是 .NET 对象的代理,解析失败会发生在运行期而不是编译期。

Q4:USING NAMESPACE为什么"不允许写部分命名空间"?写错了会发生什么?
官方规定USING NAMESPACE大小写不敏感,但不允许书写"部分命名空间",并给出了明确的错误示范:写USING NAMESPACE 'Aveva.Core',再用object namespace.NETObject()的形式去取子对象,会报错。原因是USING NAMESPACE指定的是"一整段可解析的命名空间",不是前缀;把父级当前缀使用,拼出来的类型名就不存在。正确做法是从 C# 源码复制完整命名空间并一次写全。

Q5:官方列出的三条限制分别是什么?每一条对架构设计意味着什么?
第一条:无法从 C# 直接调用 PML,只能通过事件机制反向通知——意味着流程主导权必须放在 PML 侧,C# 只能做"被调用的一段计算"。第二条:模块切换不保持 .NET 对象——意味着跨模块状态不能只放在 .NET 实例字段里,要落到设置或数据库。第三条:不能把DIRECTION、ORIENTATION等 PML 对象传给 .NET,而 DB 引用可以以数组或字符串形式传递——意味着跨桥接口应当设计成"原生类型 + 引用"的组合。

Q6:为什么 DB 引用可以传给 .NET,而DIRECTION/ORIENTATION不行?
这是官方明确划定的边界:DB 引用可以以数组或字符串形式传给 .NET,而DIRECTION、ORIENTATION这类 PML 对象不能传给 .NET。实践上的应对方式有两条:一是把方向/方位在 PML 侧拆成数值分量(例如 x、y、z)再传过去;二是改传元素引用(DB 引用过桥是允许的),由 C# 侧自己从数据库里取几何信息。设计接口时遵循"原生类型与引用能过桥、PML 几何对象不过桥"这条经验,基本不会踩到限制三。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询