资讯详情

资讯详情

C# SOLIDWORKS Manage二次开发Day2:对象模型、查询与UI性能优化

这是学习 C# SOLIDWORKS Manage 二次开发的第二天。第一天把环境搭起来、引用配好、跑通了最简单的登录示例算是勉强摸到了门。今天的目标就不一样了要真正碰业务对象项目、记录、文件还要把一个“项目列表 记录明细”的小工具跑起来。这一天的内容如果踩顺了后面做审批、BOM、变更单这些功能就不会发怵。很多人在 Day2 卡住往往不是 C# 基础不行而是搞不明白 Manage 的对象模型和 PDM、普通数据库差别太大一上来就闷头写代码很容易白忙一天。1. Day1 回顾与 Day2 学习目标设定1.1 Day1 做了什么第一天做的事情老实说不算多主要是三件事。第一确认了 SOLIDWORKS Manage 客户端的安装状态。二次开发的前提是本地至少有一个能正常登录的 Manage 客户端因为 API 底层大量依赖 COM 组件这些组件是随客户端一起注册进操作系统的。没装客户端光在 VS 里引用十几个 dll连new EdmVault5()都可能直接报0x80040154。第二找到了 API 相关程序集。不同版本的 SOLIDWORKS Manage安装目录下的结构略有差异但核心的 dll 一般都在 Manage 安装目录下的api或Client文件夹里。我当时是直接按名称搜了edm.dll、EdaDatabase.dll这几个文件的位置然后逐个记下路径。第三写了一个“裸奔版”登录测试。没有界面没有日志就是Login一下然后弹一个MessageBox告诉你成没成功。Day1 的关键收获是搞清楚了 API 调用的大概套路先创建对象、再调用方法、最后释放 COM 引用。1.2 Day2 定的哪些目标第二天的目标我列了四个都是围绕实际业务来的。一是理解 Manage 的对象模型弄清 Vault、Project、Record、File、Card 这些东西到底是什么关系。这个不搞清楚后面写代码就是瞎猜 API。二是用代码拿到真实的业务数据。具体来说就是从库里面把项目列表读出来再点开其中一个项目读取它下面的记录。记录这个词在 Manage 里是有特定含义的它不是指数据库里的一行而是指平台上的业务表单对象。三是搭一个最小的 WinForm 演示界面。左边放一个项目列表右边放选中项目的记录列表。先不管做得多好看重点是数据链路要通。四是顺手验证 UI 刷新性能。我在 Day1 写循环读数据的时候就发现一旦数据量超过几百条界面就会明显卡顿。这个问题今天必须解决。这四个目标看上去不多但 Day2 做完之后你会发现Manage 二次开发里最常用的“连接—查询—展示—释放”闭环基本就建立起来了。2. 先搞懂 Manage 的对象模型再写代码2.1 两套 API 并行EDM 与传统客户端SOLIDWORKS Manage 的 API 不是一套而是两套体系并行。第一套是 EDM 库 API也就是IEdmVault5、IEdmObjectMgr、IEdmSearch这一系列接口。这套 API 是从 SOLIDWORKS PDM 时代继承下来的它管的是库结构、文件、文件夹、版本、工作流这类偏“档案管理”的东西。第二套是业务数据 API也就是EdaDatabase、EdaRecord、EdaProject这一系列对象。这套才是 Manage 自己的核心它管的是项目、记录、任务、BOM、变更单这类真正的业务数据。我用一个比喻来理解这两层EDM 层像档案柜它负责管理每一份物理文件的存放位置、版本历史和访问权限业务数据层像台账它记录着这份文件属于哪个项目、走完了哪些审批、物料编码是多少。档案柜和台账之间有对应关系但不能混用。很多新手第一天会在这两套 API 之间来回横跳一会儿找IEdmFile5一会儿找EdaRecord最后发现数据对不上。原因就是没搞清楚你查文件应该走 EDM 层查业务记录应该走业务数据层两条路是并行的。2.2 高频用到的几个核心对象Day2 阶段我实际用到的主要对象可以整理成一张关系图来理解但这里先不急着画图我用文字说清楚Vault库整个系统的顶层容器。所有项目、文件夹、业务记录都挂在某个库下面。代码里的入口就是一个IEdmVault5对象。Project项目库下面的第一层业务分组类似于文件夹但又比文件夹多一些项目管理属性。一个项目下面可以挂多个 Record。Record记录Manage 里的业务核心载体。它可以是变更单、物料、供应商、问题报告等取决于系统里配置了哪些业务对象类型。Record 本身是一个对象会挂一整套属性、文件附件、生命周期状态和审批信息。File文件物理文档。它由 EDM 层管理但业务层会通过“记录关联文件”的方式把文件和 Record 绑定。注意一个 Record 可以关联多个 File一个 File 也可能被多个 Record 引用。Card卡片是对象属性展示的界面载体它本身也提供了接口开发时可以通过IEdmCardControl获取到对象在某个卡片上的属性值。理解这几个对象的关系之后再看操作系统的功能就很清晰了新建项目就是创建 Project提交变更就是创建一条变更 Record上传图纸就是给 Record 挂 File。2.3 命名空间与引用配置头一天我配置引用时踩了个小坑这里有必要记录一下。我最初图省事直接搜索SOLIDWORKS.Manage.dll这个文件结果发现根本找不到。后来才明白Manage 的 API 不是集中在某一个 dll 里而是分散在多个程序集中。Day2 我的工程里最终添加了这几个关键引用SolidWorks.Interop.edm.dllEDM 层核心接口IEdmVault5、IEdmObject5都在这里。EdaDatabase.dll业务数据层核心EdaDatabase、EdaRecord在这里。EdaHost.dll用于与 Manage 客户端进程交互部分场景需要用到。EDP.dll数据平台相关的对象如果你要读取卡片视图、任务信息可能需要它。添加引用时VS 的“浏览”对话框可以直接切到 Manage 的安装目录去选不要从 GAC 里找因为 GAC 里装的版本号可能跟当前客户端不匹配。还有一个硬性要求目标框架必须是 .NET Framework 4.6.2 或以上不要用 .NET Core 或 .NET 5。我试过把工程建在 .NET 6 下面编译能过但运行时 COM 互操作行为怪异有的接口直接返回 null有的会抛COMException查了半天没查出原因最后老老实实切回 .NET Framework 4.8。3. 实战连接库、登录与拿第一个业务对象3.1 连接前的本地环境检查写代码之前要先做环境检查这个步骤很多人会跳过但跳过之后出问题基本都是一脸懵。首先确保你的机器上已经装好了 SOLIDWORKS Manage 客户端并且你已经用客户端手动登录过至少一次。这一步很重要因为首次登录会在本地生成一些视图和缓存配置API 登录时可能会依赖这些本地视图。我在测试环境里装完客户端后直接用 API 登录会报错手动登录一次之后就正常了。其次确认库名。Manage 允许一台电脑配置多个库视图你的代码里写的库名必须和客户端左侧树中看到的库名称完全一致大小写也不能错。最后打开任务管理器确认后台有没有EdaServer.exe或相关进程在跑。如果客户端是绿色解压版可能服务没启动API 怎么连都会超时。3.2 连接与登录环境确认没问题后就写登录代码。Day2 我是这样写的using EDP; using EDB; public class ManageConnector { private IEdmVault5 _vault; public bool Connect(string vaultName, string userName, string password) { _vault new EdmVault5(); bool ok _vault.Login(vaultName, userName, password); if (!ok) { int errCode _vault.GetErrorCode(); string errMsg _vault.GetErrorString(errCode); throw new Exception($登录失败错误码: {errCode}描述: {errMsg}); } return true; } }这里有个容易忽略的点new EdmVault5()创建的是 COM 对象不是普通托管对象。所以整个工程必须设置目标平台为x64。SOLIDWORKS Manage 是 64 位程序对应的 COM 组件也是 64 位的如果你的 VS 工程默认是 AnyCPU在 64 位系统上运行时通常没问题但我建议还是显式设置 x64省得以后写测试程序或调试时出现意想不到的问题。登录之后建议做一个健康检查随便取一个对象或者获取一下库名称确认会话确实是通的string info _vault.Name;如果_vault.Name能拿到非空字符串说明连接没问题。3.3 遍历项目列表登录成功后下一步就是拿项目列表。Manage 的项目列表最直接的方式是遍历库下面的一级节点。我的做法是先拿到库的根文件夹对象再往下遍历子节点。关键代码如下IEdmFolder5 root (IEdmFolder5)_vault.RootFolder; IEdmPos5 pos root.GetFirstSubFolder(); while (pos ! null pos.Next()) { IEdmObject5 obj pos.GetObject(); if (obj.TypeName Project) { string projectName obj.GetName(); Console.WriteLine(projectName); } }注意GetFirstSubFolder返回的IEdmPos5是一个游标对象它的Next()方法用于移动到下一条记录。我第一次写的时候习惯性地用while (pos.Next())结果发现第一条项目被跳过了因为Next()的行为是先移动再判断有没有下一条。这个细节如果不注意写出来的代码总是少一条数据。如果你发现TypeName的返回值是Folder而不是Project也别慌。不同版本的系统里项目在对象树上的表现可能不一样你可以直接打印对象类型看看或者用obj.IsProject()这类辅助方法判断。我建议在项目里写一个通用方法专门判断对象是不是项目private bool IsProject(IEdmObject5 obj) { string typeName obj.TypeName; return typeName.Contains(Project) || obj.IsProject(); }这种兼容写法在早期版本和后期版本上都跑得过。3.4 获取记录Records项目拿到的下一步是读取项目下的记录。这里就回到第 2 节说的两套 API 问题了。EDM 层的IEdmFolder5只能拿到文件夹和文件拿不到业务记录。要拿记录必须通过业务层接口。我的写法是先拿到当前项目的对象 ID再用业务数据 API 去读取IEdmObject5 projectObj ...; // 从遍历中拿到 // 获取记录集合 IEdmObjectMgr objMgr _vault.ObjectManager; long projId projectObj.ObjectId; EdaDatabase db new EdaDatabase(); db.ConnectByVault(_vault); EdaProject project db.GetProject(projId); foreach (EdaObject child in project.GetObjects()) { Console.WriteLine($对象ID: {child.ObjectID}, 名称: {child.Name}); }这里面db.ConnectByVault(_vault)是我这个版本提供的方法作用是用当前登录的会话去连接管理数据库不需要再单独传 SQL Server 账号密码。用这种方式最省事也最安全因为账户是沿用当前登录用户的。如果你用的版本没有ConnectByVault那可能要走Connect方法参数是数据库服务器地址、库名、用户名和密码。但我不太建议直接用数据库账号连一方面是 DBA 通常不愿意把 SQL 账号给到普通开发另一方面是走数据库账号会绕过 Manage 的对象模型和权限体系容易做出越权功能。3.5 文件、记录与业务对象怎么区分Day2 我花了挺长时间才转过弯来文件、记录、业务对象这三者在 API 里不是同一个东西。文件是IEdmFile5它是 EDM 层最基本的对象代表一个物理文件。记录是业务数据层里的EdaObject或者EdaRecord它代表业务表单。业务对象Business Object是系统配置里定义的一类业务实体比如“变更单”“物料”“供应商”每一条实例就是一个记录。举个例子在系统界面里打开一个“物料编码为 ABC-001”的记录你能看到这个物料的所有属性也能看到下面关联的图纸文件。在 API 层ABC-001这个物料是EdaRecord图纸是IEdmFile5二者通过关联关系绑定。要更新物料描述字段你操作的是EdaRecord要下载图纸文件你操作的是IEdmFile5。两者不能混用。4. 查询是二次开发的主战场4.1 基于 Search 的查询刚开始做功能时最常用的查询方式是 EDM 层的 Search API。它的好处是走平台自身的权限体系和索引缓存不会跑到数据库层捣鼓。用 Search 做一个按名称查找项目的示例IEdmSearch9 search _vault.CreateSearch(true); search.SearchName ProjectName; search.Execute(); IEdmSearchCondition4 condition search.CreateCondition( ProjectName, EdmSearchConditionType.EdmSearchEquals, Demo项目); search.SearchConditions.Add(condition); long count search.GetObjectCount(); for (long i 0; i count; i) { IEdmObject5 obj search.GetObject(i); Console.WriteLine(obj.GetName()); }用 Search 的时候有几个注意点。第一CreateSearch(true)里的布尔值表示是否新建一个搜索如果传 false 会使用上一次的搜索配置容易串数据。第二GetObjectCount()返回的是long循环时小心别用int去接数据量大的时候会溢出虽然一般项目到不了int.Max但养成好习惯没坏处。第三搜索条件里的字段名不是你想写什么就写什么的必须是系统里已定义的可搜索属性。我在测试环境写了一个自定义字段名结果搜索直接返回 0 条不报错最后对照卡片属性定义才发现名字写错了。4.2 基于 SQL/视图的查询什么时候用有经验的开发会告诉你Search API 虽好用但它不是万能的。当你需要做报表、做统计分析、或者跨多个业务对象拉数据时Search API 会非常难受。这时候可以直接查询管理数据库。Manage 的数据库是 SQL Server里面有一堆系统表和历史表。最核心的一张表是BusinessObject它记录了所有业务对象的通用属性。自定义属性往往存在相关的属性表里字段名可能是动态生成的。一个比较实用的查询思路是先查BusinessObject拿到对象 ID 和类型再根据类型去关联对应的明细表。SELECT TOP 100 BO.ID, BO.Name, BO.Type FROM BusinessObject AS BO WHERE BO.Type Project ORDER BY BO.CreationDate DESC;不过直连数据库有一个问题你不是总能搞到 SQL Server 账号。我在公司环境里就遇到过DBA 不给账号所有报表需求都只能通过 Manage 的 API 来做。这种情况下我的建议是利用系统自带的“卡片视图”或者“扩展列表”功能先在客户端把想要的数据配置成一个视图再用 API 加载这个视图的数据。这种方式既绕开了数据库权限问题又获得了比较好的查询性能。关于什么时候用 Search、什么时候用 SQL我给一个实用判断表场景推荐方式原因界面快速查找文件名/项目名EDM Search走索引缓存响应快读取某个项目的记录列表业务层 API遵循权限和对象模型跨对象统计分析、导出报表管理库 SQL灵活、支持复杂聚合大数据量分页加载管理库 SQL可控性最好5. 数据展示时的 UI 性能问题Day2 最值得注意的事5.1 卡顿是怎么产生的Day2 上午我搭好了 WinForm 界面把项目列表和记录列表都显示出来了数据量不大看起来一切正常。下午测试人随手导入了几千条记录界面直接卡成 PPT点窗口都拖不动。这个问题的根子有两个。第一个是UI 线程被堵死。我在按钮点击事件里直接写了一个for循环循环里调用 Manage API 读取数据。每次 API 调用都是跨进程的 COM 调用几百上千次调用串行执行UI 线程光等着就等了好几秒。第二个是控件频繁刷新。我的列表用的是DataGridView每读一条记录就Rows.Add一行。DataGridView 每加一行都要触发一次布局计算和重绘几千行下来光排序和重绘就耗掉了大量 CPU。这两个问题叠加在一起卡顿就是必然的。5.2 UI 刷新优化三板斧优化的思路我在 Day2 下午实践了总体来说三板斧够用。第一板斧数据采集放到后台线程。用Task.Run或者BackgroundWorker做耗时操作然后把结果一次性传回 UI 线程。这里我建议用Task.Run加await代码比BackgroundWorker简洁不少。private async void btnLoad_Click(object sender, EventArgs e) { dataGridView1.DataSource null; dataGridView1.Rows.Clear(); var dataTable await Task.Run(() LoadProjectTable()); dataGridView1.DataSource dataTable; } private DataTable LoadProjectTable() { DataTable table new DataTable(); table.Columns.Add(项目ID, typeof(long)); table.Columns.Add(项目名称, typeof(string)); table.Columns.Add(负责人, typeof(string)); // 在这里循环调用 Manage API把数据填充进 table return table; }要点在于Task.Run里做的事情尽量只和后台数据相关不要碰任何 UI 控件。如果你要在后台线程里读数据并实时显示进度就用ProgressT来向 UI 线程报告进度而不要直接操作控件。第二板斧用 DataTable 做数据源批量加载。把DataGridView的DataSource直接绑定到一个DataTable一次绑定完成全部数据展示不要再一行一行add。实测下来几千行数据从“每秒十几行”变成了“一次加载秒开”。第三板斧关闭控件布局刷新。如果你的业务场景确实需要逐行处理可以在添加行的前后挂起布局dataGridView1.SuspendLayout(); // 批量添加行的代码 dataGridView1.ResumeLayout();SuspendLayout和ResumeLayout是 WinForm 自带的机制可以暂时挂起控件的布局逻辑。但是要注意SuspendLayout只能管控件内部布局管不了数据源绑定的刷新所以最佳做法还是第一板斧。还有一个容易被忽略的点Manage API 的 COM 对象不能跨线程乱传。我一开始想省事把_vault对象直接传进Task.Run里用结果时不时抛出“COM object that has been separated from its underlying RCW cannot be used”异常。后来改成在线程内部重新获取 COM 对象问题就消失了。如果你的工程确实需要在线程之间共享对象建议用[STAThread]或Dispatcher.Invoke这类方式把调用封送回去。5.3 做一个人人能用的列表加载封装经过上面的优化后我索性把这块逻辑抽成了一个工具方法方便后面所有窗体复用public static async TaskDataTable LoadWithProgressAsync( FuncDataTable loadAction, IProgressint progress) { return await Task.Run(() { DataTable table loadAction(); progress?.Report(100); return table; }); }调用时var progress new Progressint(p statusLabel.Text $加载中...{p}%); var table await LoadWithProgressAsync(() LoadProjectTable(), progress); dataGridView1.DataSource table;这样写的好处是界面只关心“加载完成了没”数据从哪来、怎么读都封装在LoadProjectTable里后续换成数据库查询也不用改 UI 代码。6. 常见问题与避坑指南Day2 真实踩坑记录Day2 踩的坑基本都是其他项目也会遇到的类型我整理成了一张速查表先给结论再展开说。现象可能原因解决办法登录报0x80040154目标平台不是 x64 / 客户端未安装 / COM 未注册工程设置 x64重装客户端并修复 COM 注册登录失败错误码 1024 之类本地没有库视图 / 库名写错 / 密码错误先用客户端手动登录一次再确认库名大小写Search 返回 0 条字段名写错 / 条件类型不对到卡片属性定义里确认可搜索字段名遍历列表少一条数据while (pos.Next())跳过了第一条改成先取首条再循环界面卡顿UI 线程做耗时 COM 调用 / 频繁刷新 DataGridView用后台线程加 DataTable 批量绑定后台线程报 RCW 异常COM 对象跨线程使用不要跨线程传递 COM 对象在线程内重新获取中文字段显示乱码编码不一致读 XML 或外部数据时统一用 UTF-8 解码6.1 环境相关的坑x64 与 COM 注册环境相关的坑往往是最折磨人的因为报错信息不一定直接指向问题根源。0x80040154 Class not registered这个错误十次有八次是平台目标配置错了。VS 新建工程默认是 AnyCPU在 64 位系统上会以 64 位运行按理说没问题但如果你勾选了“Prefer 32-bit”选项程序就会以 32 位运行而 Manage 的 COM 组件是 64 位注册的直接找不到类。这个问题我在调一个小工具时遇到过项目属性里取消“Prefer 32-bit”之后就好了。另外如果你是绿色安装或者重新安装过客户端有可能 COM 注册信息损坏。解决办法是打开 Manage 安装目录找到对应的注册修复工具或者直接重装客户端。这个办法笨但确实有效。6.2 登录拿不到数据的坑本地视图未初始化第二个高频坑是登录成功但拿不到任何数据。排错顺序是这样的先检查库名是否正确再看当前用户是否有权限。如果都没问题就考虑本地视图缓存没有初始化。我在测试机上遇到过用 API 登录后遍历库结构项目列表是空的但客户端界面里明明有十几个项目。后来强制同步了一次本地视图再去遍历就正常了。还有一种情况是库选择错误。一台机器上可以配置多个库视图你得确认自己登录的库名和客户端登录的是同一个。我在项目里专门写了一个方法登录成功后先输出vault.Name打印出来对比能省不少排查时间。6.3 COM 资源释放的坑别等问题爆发才处理C# 的垃圾回收会自动回收托管对象但COM 对象不归 GC 管。你在循环里创建的所有IEdmObject5、IEdmPos5如果不显式释放内存会只增不减。我 Day2 上午写遍历代码时没注意跑了几轮测试之后内存从 200M 涨到了 2G直接把 VS 干崩了。释放 COM 对象的统一写法是Marshal.ReleaseComObject(obj);不过要提醒一点如果对象是接口之间互相引用的直接ReleaseComObject可能会把引用计数减到 0 导致其他对象失效。所以在释放时我习惯用一个封装方法public static void DisposeComT(ref T comObj) where T : class { if (comObj ! null) { Marshal.ReleaseComObject(comObj); comObj null; } }这个思路可以简单理解为用完一个对象就“还回去”就像借书看完了要放回书架不然书全堆在桌上早晚把你埋了。6.4 关于调试的坑最后说一个调试上的坑。C# 调用 COM 接口时如果有异常默认只抛COMException你大概率看不到内部的详细错误。这时候最好在 VS 里打开“调试” - “选项” - “调试” - “常规”勾选“使用托管兼容模式”和“启用本机代码调试”。前者让调试器更兼容 COM 互操作后者能捕获到本机代码里的异常帮助定位问题。另外强烈建议项目里全局做一个异常拦截。Day2 我把 Main 函数里包了一层 try-catch并在全局事件里记录日志这样程序部署后如果出问题至少能拿到一个可读的堆栈而不是只看到一个“未处理的异常”对话框。最后分享一个 Day2 收获的小经验这一天的核心收获其实就一句话先看清 Manage 有哪两层 API再动手写代码。把 EDM 文件层和业务 Record 层混为一谈的人往往会在第一个实际功能上卡好几天。我自己 Day2 踩得最狠的坑就是试图用文件搜索去做业务记录查询白折腾了大半天后来才发现系统里早就配好了卡片视图一行 API 就能直接加载。后面你再做批注、工作流、BOM 之类的功能时这种“先摸清对象模型再写代码”的习惯会帮你少走非常多弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →