- 你好啊!
- 我也开始写博客了,记一些我遇到的但是搜起来资料很少的。
- (但是这里是要写什么呢?)
Vault Data Standard (VDS)二次开发速查笔记
一、什么是 Autodesk Vault Data Standard(VDS)? 常见误解纠正: 名称 Data Standard 并非指“数据校验(Validation)”,而是 “数据标准化(Standardization)” —— 它的核心使命是: 在用户输入源头,将自由、随意、不一致的数据,转化为结构化、规范、可被系统理解的属性值。 ▸ 它解决什么问题? 在未启用 Data Standard 时,Vault 用户检入文件仅能填写极少数基础字段(如文件名、生命周期状态),大量业务属性(如 型号、流量、材质、明细写法)依赖后续手动补录,极易导致: 属性缺失、错填、单位混乱(1500rpm vs 1500 RPM) 同类文件规格描述格式不一(Q=10m³/h, H=20m vs 10×20) CAD 与 Vault 属性不同步 → 最终使得自动化、检索、BOM 生成等高级功能失效。 ▸ 它如何工作? 当启用 Data Standard 后,在以下关键操作时,自动弹出一个标准化数据录入窗口(常称 Property Dialog): Vault Client 中: 新建/编辑 文件(对应 File.xaml) 新建/编辑 文件夹(对应 Folder.xaml) Inventor / AutoCAD 中: 检入(Check In) 文件时 保存到 Vault 时 该窗口默认采用 左右双栏布局: 区域 内容 控制方式 左侧面板 固定属性(Static Properties)由 XAML 手动定义的控件(如 <TextBox>、<ComboBox>) 修改 File.xaml / Folder.xaml 右侧面板 动态属性 Grid(Dynamic Properties)自动列出该 Vault Category 下关联的所有 Attribute 由 DynamicProperties.xml 控制显隐、顺序、别名 补充说明:除文件(File)与文件夹(Folder)外,以下对象也支持 Data Standard(较少用但存在): ...
当“不值得”成为护城河————观察AI对于行业的渗透
AI 的兴起令人振奋:它在信息检索、文本生成与知识整合上的能力,显著提升了普通用户的工作效率。然而,“AI 是否将取代人类”的疑虑亦如影随形。观察可见,在部分领域,取代已然发生。但这一进程并非均质推进——其路径由经济逻辑主导,而非技术可能性本身。 被率先渗透的行业,普遍具备两个特征:其一,历史利润丰厚,足以支撑高昂的模型开发成本,如金融、法律;其二,核心门槛在于信息处理:金融依赖对海量数据的快速分析,法律依赖对庞杂条文的精准调用,医疗则要求从业者长期积累大量病例经验。这些工作中大量重复性、结构性强的环节,正被 AI 以更高效率、更低成本完成。 相较之下,另一些领域尚未经历同等程度的冲击。其原因并非技术不可逾越,而在于经济可行性不足。以系统级设计为例——规划一条柔性产线时,需权衡“换型时间缩短两分钟是否值得追加五十万元投入”,或“为操作者预留三十厘米空间致整体效率下降百分之二,但工伤风险降低百分之四十,是否合理”。此类决策依赖对业务目标、人性需求与风险边界的综合判断。表面观之,似为 AI 难以企及之域;实则,相关模型在理论上可构建——通过强化学习结合数字孪生,量化多维目标并输出帕累托解集。 障碍不在能力,而在成本:数据采集、系统集成与定制训练的投入,远超当前所能兑现的经济回报。设计行业与金融行业从业者薪资的显著差异,正折射出资本对 ROI 的天然取舍。 需强调的是,“当下不值得”不等于“永远不可行”。技术本身持续演进,传感器成本下降、数据积累深化,终将压缩经验数字化的边际成本。届时,许多所谓“直觉”或将被证实为高维模式匹配——与 AI 的机制并无本质区别。老师傅凭异响判断轴承故障,实为长期实践中形成的“声纹—失效”映射;若企业愿系统模拟万次故障并记录声学特征,专用模型完全可能超越人类表现。区别仅在于:前者是沉没成本,后者需显性投入。 当资本在头部高利润领域完成初步整合、边际收益递减时,其必然向次级高潜力领域延伸。设计、医疗、教育等曾被视为“中产堡垒”的行业,或将迎来更深度的流程重构。此时,所谓“护城河”的实质,更接近一种暂时的经济洼地。 唯一可能构成实质性阻滞的,是责任归属问题。现行制度要求关键设计文件须由注册工程师签字,终身担责。此非技术局限,而系制度选择——人类社会需要明确的问责主体,以维系系统稳定。重大事故后,公众要求一个可指认的负责者,以疏导焦虑、恢复秩序。这种“替罪羊机制”具有深层社会功能:正如历史上诸多群体对立被用作内部凝聚的工具,责任归属的本质,是为复杂系统失效提供一个可操作的出口。只要此机制持续存在,人类的签字权便难以被彻底替代。 至于奢侈品手工艺的存续,其逻辑亦属经济理性:行业规模有限,虽单品溢价高,但总体利润不足以吸引大规模 AI 投入;从业者遂采取主动收缩策略——限制产能、不再扩招,仅由少数匠人分享剩余价值,维持个体收益水平。这并非技术护城河,而是一种清醒的退守。 综上,AI 的演进轨迹由经济可行性划定:它优先取代高利润、高标准化、高数据密度的环节;对低 ROI 领域的渗透,则取决于资本流动的阶段性需求。人类经验的数字化并非不可行,而是成本与收益的权衡问题。 最终,行业演化或呈现两种路径: 高 ROI 领域走向“极简人力 + AI 大规模复制”,单位产值利润率反升; 低 ROI 领域走向“小规模、高单价、少人力”的收缩模式; 而横亘其间的责任归属问题,则构成一道暂时难以逾越的制度性边界。 技术不会决定未来,但会放大我们已有的选择。而真正的挑战在于:当效率的边界不断拓展,我们是否仍保有定义“值得”的能力。
从词向量到“人工天才”:我的LLM认知思辨录
本文是在与AI助手深度对话后,对我个人理解大语言模型(LLM)过程的梳理与总结。它不代表学术观点,仅是一个探索者的思想航行日志。 一、起点:从“词频统计”到“语义宇宙” 我的思考始于一个最朴素的问题:如何判断两篇文章是否相关? 最直观的想法是统计共有的词语——这就是“词袋模型”。但它有一个显而易见的缺陷:无法理解语义。正是在这里,我遇到了第一个关键概念:词向量。 在我的想象中,词向量就像是为机器建造了一个高维的语义宇宙。每个词不再是孤立的符号,而是这个宇宙中的一颗星星: 语义相近的星(如“国王”和“王后”)会在宇宙中彼此靠近 语义关系(如“国王-男人+女人≈女王”)通过星星之间的相对方位来体现 但很快我发现了一个问题:这个词向量宇宙是静态的。无论上下文如何,“苹果”这颗星的位置,总是固定在“水果”和“科技”的模糊中点。这显然不符合我们对语言的理解——同一个词在不同语境下应有不同的含义。 二、突破:三重变换与“动态侦探” 为了解决静态词向量的局限,我接触到了Transformer架构——当代LLM的核心引擎。为了理解它,我构建了这样一个比喻: 词向量像一本权威词典:每个词都有个固定不变的定义 大语言模型像一位顶级侦探:他能根据具体情境,动态理解每个词的真实含义 这位“侦探”的思考过程,可以简化为三个关键的矩阵变换: 输入嵌入:将词语转换为初始的“思维符号” Transformer加工:通过自注意力机制,让所有词语的符号相互交流,生成富含上下文的全新表示 输出投影:将最终的思维结果“翻译”成人类语言 这个过程让我意识到:LLM不是在简单预测下一个词,而是在深度理解整个语境后,让最合适的词语自然流淌出来。 三、镜像:当LLM照见人类思维 理解LLM的过程,意外地成为了一面审视人类自身的镜子。 我们都是“模式识别”系统 我回想起自己解数学题的方法:列出已知量和待求量,然后在脑中搜索可能的公式——这本质上就是一种模式识别。LLM的注意力机制不也是在庞大的知识库中进行加权搜索吗? “通才”与“天才”的鸿沟 大多数人和当前的LLM一样,是优秀的“内插器”——在已知模式间进行组合。而天才,或许就是那些能在更高维度进行“外推”,创造出全新模式组合的系统。 顺序的迷思 我们日常交流中经常使用倒装、省略,但彼此仍能理解。这让我怀疑:智能的核心或许不是表面上的词序,而是深层的语义关系网络。 语法顺序只是通往这个网络的康庄大道,但不是唯一的路径。 四、深化:动态智能的未来图景 在对比人与LLM时,一个关键差异浮现出来:我们的思维是动态的,而LLM是静态的。 LLM的“静态心智”:使用固定的激活函数,如同一个永远保持同一种情绪的思考者 人脑的“动态大脑”:受化学物质调节,思考效率随状态波动——有时思如泉涌,有时头脑迟滞 这指向了一个迷人的方向:为LLM引入动态激活机制。比如: 动态稀疏:根据问题难度激活不同数量的神经元 情境化思考:让激活函数能根据任务类型自我调整 神经调制:引入类似“好奇心”的全局信号 这或许是LLM从“博学通才”迈向“创造天才”的关键一步。 五、哲思:智能、意识与存在的终极之问 这场思辨最终将我带向了一些哲学性的边界问题。 如果人脑与LLM在本质上都是“模式处理系统”,那么我们的意识、创造力,是否也只是更复杂算法的涌现? 这个想法让我联想到《模拟人生》的游戏——如果为游戏角色接入LLM,他们将产生“模拟的自主意识”,却永远无法认知自己被创造的事实。那么,我们是否也可能身处某个“上层游戏”之中? 面对这个令人战栗的推论,我找到了自己的答案:即使我们是模拟的,但我们此刻的思考、困惑、爱与恐惧,这些体验本身的质感是100%真实的。 意义不依赖于底层基质(是原子还是比特),而依赖于体验的深度与丰富度。 结语:作为镜子的LLM 回顾这段思考历程,我意识到LLM不仅仅是一项技术,更是一面珍贵的镜子。通过理解它的运作原理,我们得以用新的视角审视自己的思维方式。 从词向量到Transformer,从静态模式匹配到动态条件计算,这条技术发展路径,恰恰映照出我们对“智能”本身不断深化的理解。 或许,未来真正的突破不在于建造更大的模型,而在于为模型注入那种我们称之为“灵感”、“直觉”和“创造力”的动态本质——而这,将需要我们更深刻地理解我们自己。
VSTO线程模型
在VSTO开发领域,每个开发者都曾经历过这样的挫败:精心设计的插件界面在数据加载时陷入卡顿,复杂的计算过程使整个Office程序失去响应。这不是代码质量问题,而是VSTO特有的线程模型带来的挑战。本文将围绕如何利用独立线程和异步调度解决该问题进行剖析。 VSTO线程模型的本质矛盾 Office的COM对象是基于单线程单元(STA, Single Threaded Apartment)模型的。这个模型的好处是保障了线程安全,但也造成了性能瓶颈。具体表现: UI线程和Office线程绑在一起,做耗时数据读取或计算时,界面会卡死甚至完全无响应。 想用多线程访问COM对象? 不行,需要走代理(Proxy)封送(marshalling)调用,开销大且复杂。 这种设计就像双刃剑,让我们陷入两难:一边要保证线程安全,一边又想流畅不卡顿。 突破瓶颈的思路:线程分离 + 异步计算 要打破这个瓶颈,必须实现UI线程与Office线程的分离,并确保所有COM对象的访问都发生在Office线程上。同时,为避免业务逻辑阻塞UI或Office线程,计算应异步在线程池(TaskPool)中进行。 这带来关键问题:如何在不同线程间高效且安全地传递数据? 如何在后台任务中发起Office数据读取,并调度到VSTO主线程执行? 数据处理完成后,如何将结果发回UI线程,满足UI线程禁止跨线程更新界面的要求? 那具体怎么调度线程?如何跨线程安全调用Office对象并更新UI?这里,SynchronizationContext和调度器(Scheduler)派上用场了。 下面用时序图简单描述整体流程: sequenceDiagram UI线程->>+线程池: 发起计算请求 线程池->>+VSTO主线程: 通过Office的SyncContext或Scheduler获取数据 VSTO主线程-->>-线程池: 返回封送后的数据 线程池->>线程池: 执行计算 线程池->>+UI线程: 通过UI的SyncContext或Dispatcher更新界面 SynchronizationContext 的选型与实践 为 VSTO 主线程设置上下文 SynchronizationContext是一种线程抽象机制,提供“将任务调度到指定线程”的能力。VSTO项目启动时默认无上下文,无法通过SynchronizationContext.Current直接获取VSTO主线程上下文。 一种解决方案是自定义上下文,但更简便的做法是利用Office主线程已有的Win32消息泵,使用WindowsFormsSynchronizationContext。尽管名字带“Windows Forms”,它不仅限于WinForms应用,只要线程是STA且运行Win32消息循环,它都能正常工作。 若需更细粒度的任务优先级控制,可考虑DispatcherSynchronizationContext,但它实现复杂且需手动启动消息泵。因职责已隔离,通常用WindowsFormsSynchronizationContext即可。 示例: public partial class ThisAddIn { private static SynchronizationContext _officeSyncContext; private void ThisAddIn_Startup(object sender, System.EventArgs e) { // 为 VSTO 主线程设置上下文,供后续调度使用 _officeSyncContext = new WindowsFormsSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(_officeSyncContext); // 其他初始化代码 } } 在自建 UI 线程中设置上下文 WPF和WinForms要求UI线程必须是STA,然而默认线程池线程为MTA,不适合做UI线程。为避免阻塞Office线程(VSTO_Main,唯一STA线程),只能新建一个专用STA线程运行UI。 Avalonia允许非STA线程,但考虑到VSTO依赖Windows平台和消息泵机制,建议UI线程依然使用STA模式。 WPF/WinForms依赖Win32消息泵,同样可用WindowsFormsSynchronizationContext。 示例: public partial class ThisAddIn { private static SynchronizationContext _uiSyncContext; private void ThisAddIn_Startup(object sender, System.EventArgs e) { // 其它代码 // 创建专用 UI 线程 _uiThread = new Thread(() => { _uiSyncContext = new WindowsFormsSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(_uiSyncContext); }) { Name = "UI Thread", IsBackground = true }; _uiThread.SetApartmentState(ApartmentState.STA); _uiThread.Start(); } } 利用SynchronizationContext.Post可在线程池、VSTO主线程与UI线程间安全传递数据。 ...
AE PID快速入门
本文以中涂洁净间循环风机为例介绍AE PID绘制设备原理图的基本流程。 详细步骤 在Visio中使用空白模板创建新的绘图,单位选择“公制单位”; 在功能区处切换至AE PID选项卡; 点击“编辑”-“初始化”按钮对当前绘图进行初始化。初始化操作将在当前页面中插入一个A0图框,并设置文档页面的网格宽度为2.5 mm。 此外,为文档增加2个AE指定样式。其中,“AE Normal”样式是设备单元的默认样式,“AE Pipeline”是管线的默认样式。 AE样式将使用“思源黑体”作为指定字体,在AE PID插件的安装过程中会自动为系统安装思源黑体。若未能正确显示思源黑体,请尝试手动安装。 若要修改图框大小,用鼠标右键点击图框,并在子类中选择合适的尺寸; 在选项卡中点击“编辑”-“库”加载模具库。 从“AE逻辑”库中拖拽“功能单元”至绘图区; 当您不确定所需的设备对象属于哪一个类型时,可以借助Visio模具的搜索栏进行搜索。这将帮助您快速找到所需的设备对象,无需事先知道其具体类型。 如果您正在使用Windows11系统,搜索功能可能未能如期使用,请按照修复了Visio桌面应用中的形状Windows 11的方法修补。 用鼠标右键点击该“功能单元”,打开“形状数据”面板; 在“形状数据”面板中输入“功能组”值:“GF612”,“功能组名称”值:“中途洁净间循环风机”; 此时,功能单元显示如下: 使用相同的方法从“AE基础”库中拖拽“鼓风机”至功能组内,此时可以看到功能单元边框被高亮为绿色,表示该风机已被加入功能单元。 保持风机的选中状态,在“形状数据”面板中键入“功能元件”值:11,按下回车后该值将显示为“GQ11”; 拖动GQ11上的黄色控制点,可以移动功能元件标签的位置; 为了表示鼓风机配备的电机,从“AE逻辑”库中拖拽“代理功能元件”至功能组内; 选中该代理功能元件,将黄色控制点拖拽至鼓风机上,使代理功能元件与鼓风机相关联; 被关联后,代理功能元件的形状数据处可以看见被关联设备的位号; 当被关联设备发生移动时,关联元件会跟随移动; 保持代理功能元件的选中状态,补充“元件位号”值:“MA01”,描述:“电机”,并将代理功能元件拖拽至鼓风机附近。 继续从“AE基础”库中拖拽“仪表”至功能单元中,并在右键菜单中选择“子类”-“本地面板监视仪表”; 补充形状数据并将仪表移动至合适的位置; 将仪表正中的控制点拖拽至鼓风机上; 使用同样的方法绘制下方的“中控监视操作仪表”; 从“AE管线”库中拖拽“管路”至绘图区,并在“子类”中选择“排出空气”使其显示为柠黄色; 将管路一段连在鼓风机的连接点上,另一端连接至其他对象,并在右键菜单中点击“改变箭头方向”; 多次点击直到箭头朝向如图所示; 使用相同的方法完成另一根管路的绘制; 在选项卡中点击“导出”-“BOM”查看当前图纸的BOM结构; 在选项卡中点击“编辑”-“图例”将在图签上方生成图例; ...
Visio模具库建库指导
分享一些Visio模具库建库过程中值得注意的事项以及原因。 模具的BaseID的唯一性。 BaseID是Master对象的属性之一,Master对象有三个ID:BaseID,UniqueID和ID。BaseID在Master被创建时生成,且不再改变(除非使用程序修改)。因此BaseID非常适合作为维护Master对象时的唯一标识。这是因为,UniqueID会随着Master内容的修改而变化,ID会随着不同的文档而变化。 通常情况下,我们不需要关注BaseID,因为当我们将形状从绘图页拖拽至模具库时,会创建唯一的BaseID。但是,很多管理员在建立新模具时,为了省去添加通用形状和属性,如Tag文本,备注属性等,会直接在模具库中选中已经存在的模具复制粘贴,然后在粘贴后的模具中进行修改。这个时候站贴后的模具将和源模具具有相同的BaseID。为了保证BaseID的唯一性,可以在程序中调用Master.NewBaseID方法获取新的BaseID,或者直接在Openxml文档中键入新的BaseID。 模具的形状界限与网格线重合。 由于Visio中的默认吸附设置包括网格线,当模具的形状界限是网格的整数倍时,模具实例的边界可以吸附在网格线上,这样用者不需要再使用对齐工具也可以快速的实现多个形状的对齐,从而使页面看起来更整洁。 这就要求管理员在设计模具的形状时,充分考虑形状的轮廓尺寸。当然,有些情况下没有办法保证这些线条刚好经过网格线,但是仍然可以通过手动设置形状的Width和Height,使其包围框刚好位于网格线上。 例如,对于图中的两个阀门,尽管电磁气动阀的轮廓远比通用阀门大,但是他们都使用同样的形状界限,即5mmx5mm。(这样做另一个隐藏的好处是:当用户使用电磁气动阀替换通用阀门时,不需要再调整对象的位置,因为他们是同轴的。) 在ShapeSheet的Geometry中使用GUARD和Rel而不是使用绝对尺寸。 当我们在ShapeSheet中修改Geometry属性时,很容易遇到插入一个新的数据行引发形状的Width和Height重新计算,导致才修改好的形状意外改变。因此,在设计初期就应该使用GUARD对形状的Width和Height以及几何数据写保护,防止插入新的几何数据时Visio重新计算引发的意外情况。 在几何数据中使用相对值而不是绝对值,则是为了解决用户手动调整实例尺寸时,代表实例的几何形状可以保持正确的比例,以防出现原来是个原型,现在是个椭圆形的情况。 在创建具有多个子类的对象时,将表示不通子类的多个几何数据创建在同一个形状对象上,而不是使用多个形状对象。 这句话描述的可能比较抽象。有的时候,为了缩减模具库中模具的数量,管理员可能会考虑将多个具有类似的对象设计成一个模具,并通过属性切换显示与隐藏。例如,图中的阀门聚合了多种形式的阀体,并根据用户的选择进行形状的切换。 在实现这个功能时,有两种做法:方法一是在模具的形状组中创建多个形状用以表示不通的阀体,例如三个形状分别表示通用发、三通阀、角式阀;方法二是在一个形状中,插入多个几何数据块。我认为方法二是更好的做法,因为如果用户在使用过程中解散了图纸中实例的组,不会在图纸中生成隐藏的形状对象。尽管这样增加了管理的复杂性,因为无法通过形状的名称判断几何数据块表示的形状。
ReactiveUI ViewModel Properties总结
属性类型 在ReactiveUI中,ViewModel中的属性可以根据其用途划分为三种情况:读写属性(Read-Write Properties)、只读属性(Read-Only Properties)和输出属性(Output Properties)。 读写属性 (Read-Write Properties): 读写属性是可以被服务修改,也可以被用户在View中修改的属性。这类属性是我们通常比较熟悉的普通属性。 例如,用户的姓名可能是被服务加载的,而在加载之后又被用户修改。 只读属性 (Read-Only Properties): 只读属性是在构造函数中被初始化且在之后不再变化的属性。 例如, 用户的ID在一般情况下时不允许变化的。 输出属性 (Output Properties): 输出属性是ReactiveUI中新提出的概念,初次接触ReactiveUI时,可能会将输出属性与只读属性混为一谈。尽管输出属性对于用户而言是只读的,但是对于属性本身是可变的。这类属性通常由Observable变化而成,表示属性值可能随时间变化。 例如,用户负债率随用户的总资产和总负债变化,但负债率属性本身是不允许被用户修改的。 在ReactiveUI中,使用这三种属性类型可以更清晰地表示属性的特性和用途。读写属性用于需要双向绑定的数据,只读属性用于一次性初始化后不再改变的数据,而输出属性用于表示可能随时间变化的数据流。这种划分有助于更好地理解和管理ViewModel中的属性。 属性的声明与绑定方法 在明确了属性类型的基础上,ViewModel中所有非集合类型的属性都可以按照下面固定的方式进行声明与绑定。 读写属性的声明需要调用ReactiveObject的RaiseAndSetIfChanged方法,该方法实现了INotifydPropertyChanged。 private string name; public string Name { get => name; set => this.RaiseAndSetIfChanged(ref name, value); } 读写属性的绑定使用TView的Bind方法进行双向绑定。 this.WhenActivated(disposable => { this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text) .DisposeWith(disposable); }); 只读属性使用一般的声明方式即可。 public int Id {get;} 绑定时,使用单向绑定。 this.WhenActivated(disposable => { this.OneWayBind(ViewModel, vm => vm.Id, v => v.IdTextBlock.Text) .DisposeWith(disposable); }); 输出属性也是使用固定的方式进行声明,但是在初始化时需要注意。 // 声明 private readonly ObservableAsPropertyHelper<double> _debtAssetRatio; public string DebtAssetRatio => _debtAssetRatio.Value; // WhenAnyValue产生一个Observable,当Debt或Asset变化时,会发出新的Debt/Asset的值。这里需要注意Debt和Asset必须也实现了INotifyPropertyChanged,否则无法观察到它们的变化。 // ToProperty将Observable转变为ObservableAsPropertyHelper。 UserAccountViewModel(){ this.WhenAnyValue(x => x.Debt, x => x.Asset, (debt, asset) => debt/asset) .ToProperty(this, x => x.DebtAssetRatio, out _debtAssetRatio); } 由于输出属性对用户而言也是只读的,所以使用单向绑定。 this.WhenActivated(disposable => { this.OneWayBind(ViewModel, vm => vm.DebtAssetRatio, v => v.DebtAssetRatioTextBlock.Text) .DisposeWith(disposable); }); 集合 在使用集合类型的数据,最简单的情况是View中使用的是不可变的数据集合,例如显示Blog中已归档的文章列表,显示用户银行账户的历史交易信息等。这种情况下由于数据只在构造函数中被初始化,所以可以声明为任意集合类型,例如IEnmerable<T>、 IList<T>、ObservableCollection<T>。再在View中使用OneWayBind绑定。 // ViewModel public IEnumerable<Article> Articles {get;} // View this.WhenActivated(disposable => { this.OneWayBind(ViewModel, vm => vm.Articles, v => v.DataGrid.ItemsSource) .DisposeWith(disposable); }); 对于可变数据集合,则需要将集合声明为ObservableCollection<T>,然后在View中使用OneWayBind绑定。要注意的是T也需要实现INotifydPropertyChanged,否则T属性的变化无法触发View更新。 ...
Visual Studio Installer实现覆盖安装新版本的方法
当使用Visual Studio Installer进行打包时,要实现安装时自动卸载旧版本然后安装新版本,需要同时设置以下各点: Deployment Project Properties -> DetectNewerInstalledVersion -> True Deployment Project Properties -> RemovePreviousVersions -> True Deployment Project Properties -> Version 其中,Version被安装程序用来判断是否继续执行安装程序,所以Version值应大于上一个版本。 满足以上条件时,执行安装程序可以顺利执行,且控制面板中可以看到更新后的版本号。但是,安装程序仍然可能没有正确执行。这是因为项目的主输出并没有被正确拷贝。这往往是因为没有正确设置项目(程序项目,非部署项目)的AssemblyInfo。主输出中的Version将被用来比较是否需要拷贝主输出到安装目录,所以当上一版本的主输出版本为0.2.1.0时,即使已经设置部署项目的Version为0.2.2.0,由于此时主输出的版本仍然为0.2.1.0,安装过程中不会拷贝新生成的主输出到安装目录。 主输出的Version设置位于程序项目的AssemblyInfo.cs文件中。只有当此文件中的AssemblyVersion或AssemblyFileVersion的值大于上一个版本的值时,才会覆盖原安装目录的dll。
如何更新Visio文档中的模具
在工程设计领域,流程图设计是许多企业不可或缺的一环。Visio,作为备受推崇的流程图设计工具,正在成为越来越多企业的首选,以期通过规范化的流程设计来提升整体工作效率。 尽管Visio提供了丰富的内置模具库,但在实际应用中,通用模具往往难以满足企业独特的业务需求。为了更好地适应企业的特殊流程和标准,许多企业纷纷转向定制模具库的方向,这与我们从外企的学习中所观察到的趋势是一致的。然而,由于缺乏指导性的方法,企业在推广和运用Visio时可能会面临一系列挑战。其中一个普遍存在的问题是在初版模具库建立后,如何有效地进行迭代更新。 本文将讨论Visio文档中模具更新的实现。首先介绍用户从模具库拖拽至绘图页时的背后过程,揭示为什么文档中的形状实例不会随模具库的更新而更新。随后介绍手动更新的方法。最后,提供比较两种自动化更新的方法及实现刚方法模具需满足的条件。 拖拽背后的故事 当用户首次从模具库拖拽模具到绘图页(Page)时,Visio在后台完成了多个操作。首先,Visio会在文档的文档模具(Document Stencil)中创建该模具的副本,然后再在绘图页上创建针对该模具形状的实例。由于文档模具默认是隐藏的,所以用户可能无法察觉到这一点。(要显示文档模具,首先需要在“选项”-“自定义功能区”中启用“开发者”选项卡。然后,在“开发者”选项卡-“显示/隐藏”分组中勾选“文档模具”。) 当用户再一次从模具库拖拽同一个模具时,Visio将检查文档模具中是否已存在该模具的副本。如果副本已经存在,Visio将直接创建实例。那么Visio是如何判断模具已存在的?在默认情况下Visio会比较模具的UniqueID属性。因此,即使两个模具具有相同的名称,Visio也可以通过UniqueID判断它们的对应关系。每当用户编辑并保存模具时,模具的UniqueID会发生变化。所以拖拽修改后的模具到绘图页时,可以观察到文档模具中出现了新的副本。这也就是为什么修改了模具库中的模具,绘图页中的实例没有被更新。 这显然与我们的期望不符。我们希望图纸中的实例永远与最新的模具一致。 手动更新 要将实例引用的模具修改为最新的模具,一种已知的方法是使用“主页”-“更改形状”对图纸中的实例进行更改。但是,对于已包含大量实例的文档,这个操作费时费力。尤其是当新版模具与旧模具的差异并不大时,用户很容易发生遗漏。 使用COM批量更新 借助COM组件,我们可以通过创建自动化程序的方式批量选择某一模具的实例,然后调用Shape.ReplaceShape()方法,实现批量更改这些实例的形状。 要获取文档模具中模具在文档中的所有实例,我们可以调用遍历文档中的所有形状,并筛选出Shape.Master等于文档模具中的Master情况。关键代码如下: public IEnumerable<IVShape> GetInstances(IVMaster master) { var instances = document.Pages.OfType<IVPage>() .SelectMany(x => x.Shapes.OfType<IVShape>()).Where(x => x.Master == master).ToList(); return instances; } 要找出模具库中对应的新模具,需要使用到模具的另一个ID属性————BaseID。模具的BaseID是在模具被创建的时候生成的,随后不会发生改变。因此,可以通过BaseID找到模具库中的同源模具。但是,使用这种方式时,要求模具库中的BaseID具有唯一性。一种常见的错误是管理员在创建模具时,采用的不是首先在绘图页绘制模具形状再拖拽至模具库,而是直接将模具库中的模具复制成了新的模具并编辑该模具。此时,模具库中的代表不通类型的模具具有相同的BaseID。 关键代码: public IVMaster GetLatestMaster(IVDocument document, string baseID) { var latestMaster = document.Masters.OfType<IVMaster>() .SingleOrDefault(x => x.BaseID == baseID); return latestMaster; } 然后,遍历这些事例,并将形状替换为新版本的模具。 public void Replace(IEnumerable<IVShape> instances, IVMaster latestMaster) { foreach (var instance in instances) { instance.ReplaceShape(latestMaster); } } 使用COM方式更新的好处是可以直接在原文件中进行修改。然而,由于UI的频繁更新可能导致方法执行时间较长,特别是在复杂的涂装车间原理图中,可能需要数分钟。因此,为了提升用户的使用体验,开发者可能会考虑加入进度条,以直观地显示更新进度。但是Visio使用STA模型且UI更新过程会向主线程封送消息,如果使用WPF组件,UI线程会发生阻塞,因此应使用WinForm。 另一个不利因素是,更新可能会引发连接线(Connector)的几何属性重新计算。也就是说,直角型连接线的折点位置可能会发生改变。 使用OpenXML批量更新 当不要求在原文件中完成更新时,可以考虑直接修改OpenXML文件。OpenXML是一种基于XML的文件格式,在2013年被引入Visio。OpenXML格式的Visio文档后缀为".vsdx"。要查看OpenXML格式的详细内容,可以修改文档后缀为".zip"后打开压缩包。 .\visio\masters目录下存储了文档模具的相关内容。masters.xml文件中列出了文档模具中的模具的部分属性。 其中,对我们有用的是Master节点的BaseID属性和Rel子节点的r:id属性。前者的作用已在前文中提及。r:id属性可以通过查看_rels文件夹下的masters.xml.rels确定与此Master关联的MasterContents文件。MasterContents文件定义了模具的形状。关键代码(XmlHelper部分的代码参考以编程方式处理Visio文件格式): public IEnumerable<XlElement> GetMasterElements(Package package) { // mastersPart指masters.xml var mastersPart = package.GetPart(XmlHelper.MastersPartUri); // 筛选出BaseID属性为baseID的Master节点 var masterElements = XmlHelper.GetXElementsByName(mastersPart, "Master"). return masterElements; } public PackagePart GetMasterContentsFile(Package package, string baseID){ var masterElement = GetMasterElement(package).SingleOrDefault(x=>x.Attribute("BaseID")!.Value == baseID); // 通过子节点Rel获取r:id var relElement = masterElement.Descendants(XmlHelper.MainNs + "Rel").First(); var relId = relElement.Attribute(XmlHelper.RelNs + "id")!.Value; var rel = mastersPart.GetRelationship(relId); // masterPart指master{i}.xml var masterPart = package.GetPart(PackUriHelper.ResolvePartUri(rel.SourceUri, rel.TargetUri)); return masterPart; } 当我们更新文档中的模具时,实际上只需要将Master节点和MasterContents节点替换为修改后的模具库中的对应内容。关键代码: public void Replace(Package drawingDoc, Package stencilDoc, string baseID){ var mastersPartDrawing = sourcePackage.GetPart(XmlHelper.MastersPartUri); foreach (var masterEleDrawing in GetMasterElements(drawingDoc)) { // 查看模具库中是否存在对应的Master var masterEleStencil = GetMasterElements(stencilDoc).SingleOrDefault(x=>x.Attribute("BaseID")!.Value == baseID); if (masterEleStencil == null) continue; // 使用模具库中的Master节点替换文档中的Master节点。但是由于模具库中的Rel关系和可能与文档中的不一致,所以为了不去修改masters.xml.rel文件,仍使用原文档中的Rel节点 var relEleDrawing = masterEleDrawing.Descendants(XmlHelper.MainNs + "Rel").First(); masterEleStencil.Descendants(XmlHelper.MainNs + "Rel").First().ReplaceWith(relEleDrawing); masterEleDrawing.ReplaceWith(masterEleStencil); // 替换MasterContents var contentsPartDrawing = GetMasterContentsFile(drawingDoc, baseID); var contentsPartStencil = GetMasterContentsFile(stencilDoc, baseID); XmlHelper.SaveXDocumentToPart(contentsPartDrawing, XmlHelper.GetXmlFromPart(contentsPartStencil)); } XmlHelper.RecalculateDocument(drawingDoc); XmlHelper.SaveXDocumentToPart(mastersPartDrawing, XmlHelper.GetXmlFromPart(mastersPartDrawing)); } 查看完整代码 ...
解决Visio使用Shape Replace方法显示OBJ的BUG 2
很不幸,又遇到了同样的问题。但这次问题涉及到一个容器对象,该容器内的某个形状链接了容器的某个属性值。当使用Shape.Replace()方法进行更新时,该属性又被更新成“OBJ”。 因此,按照之前提到的方法,我们首先删除了我们自定义的容器的基本属性,例如SelectMode、DisplayMode、CalWH等。然后重新执行更新程序。此时,“OBJ”被正确的属性值所取代。 通过Visio应用程序的编辑模具功能,重新定义先前的基本属性,再次执行更新程序。不幸的是OBJ又出现了。 最终我们发现,当同时满足以下两个条件时,会产生上述的BUG: IsTextEditTarget=False GlueType=8