">程序配置与 <bindingRedirect/>
在完成部分接口把程序打包成exe时重新校对了一下 NewtonJson 的版本,之前在 Nuget 上下载的是 13 版本,但是 GStarCAD 目录下的版本是 12。将版本调整之后 exe 无法正常使用:
未经处理的异常:SystemI0.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Json,Version=13.0.0.0 Culture=neutral Pub1icKeyToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040)--->SystemIO.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Ison,Version=12.0.0.0,Culture=neutral, PublicKe yToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040)
再次检查 Nuget 版本已经调整过来了,但是编译之后的 exe 还是会报错 NewtonJson 版本错误。其实只需要调整一下 app.config
文件:
<?xml version="1.0" encoding="utf-8" ?> <configuration > <startup > <supportedRuntime version ="v4.0" sku =".NETFramework,Version=v4.8" /> </startup > <runtime > <assemblyBinding xmlns ="urn:schemas-microsoft-com:asm.v1" > <dependentAssembly > <assemblyIdentity name ="Newtonsoft.Json" publicKeyToken ="30ad4fe6b2a6aeed" culture ="neutral" /> <bindingRedirect oldVersion ="0.0.0.0-13.0.0.0" newVersion ="13.0.0.0" /> </dependentAssembly > </assemblyBinding > </runtime > </configuration >
问题在于 <bindingRedirect/>
这个标签,微软官方API文档 这里有比较详细的解释。
将一个程序集版本重定向到另一个版本。
<bindingRedirect oldVersion ="existing assembly version" newVersion ="new assembly version" />
oldVersion
:指定最初请求的程序集的版本。 程序集版本号的格式为 major.minor.build.revision
。 该版本号的每个部分的有效值介于 0 和 65535 之间。newVersion
:指定要用来取代最初请求的版本的程序集版本(格式为:n.n.n.n
) ,此值可以指定 oldVersion 之前的版本。官方还给出了一个示例演示如何将一个程序集版本重定向到另一个版本:
<configuration > <runtime > <assemblyBinding xmlns ="urn:schemas-microsoft-com:asm.v1" > <dependentAssembly > <assemblyIdentity name ="myAssembly" publicKeyToken ="32ab4ba45e0a69a1" culture ="neutral" /> <bindingRedirect oldVersion ="1.0.0.0" newVersion ="2.0.0.0" /> </dependentAssembly > </assemblyBinding > </runtime > </configuration >
所以把这个标签删掉,程序就可以正常运行了,这是一个知识盲点记录下来。
读取 .dat
文件数据 所需要读取的 PaperSet.dat
长相如下,包含了国标图幅的各个数据尺寸:
Name B L a c e k BD LD A0 841 1189 25 10 20 1 16 12 A1 594 841 25 10 20 1 12 8 A2 420 594 25 10 10 1 8 6 A3 297 420 25 5 10 1 2 2 A4 210 297 25 5 10 1 2 2 A0X2 1189 1682 25 10 20 3 16 24 A0X3 1189 2523 25 10 20 3 16 36 A1X3 841 1783 25 10 20 3 12 24 A1X4 841 2378 25 10 20 3 12 32 A2X3 594 1261 25 10 20 3 8 18 A2X4 594 1682 25 10 20 3 8 24 A2X5 594 2102 25 10 20 3 8 24 A3X3 420 891 25 10 10 2 6 12 A3X4 420 1189 25 10 10 2 6 16 A3X5 420 1486 25 10 10 2 6 10 A3X6 420 1783 25 10 10 3 6 12 A3X7 420 2080 25 10 10 3 6 14 A4X3 297 630 25 5 10 2 2 8 A4X4 297 841 25 5 10 2 2 12 A4X5 297 1051 25 5 10 2 2 12 A4X6 297 1261 25 5 10 3 2 12 A4X7 297 1471 25 5 10 3 2 14 A4X8 297 1682 25 5 10 3 2 16 A4X9 297 1892 25 5 10 3 2 18
观察数据结构,每行数据均以若干空格分隔图幅名称、长度、宽度等等数据,所以我们需要逐行去读取,先简单声明一个图幅信息类包含图幅名称和长宽:
public class Paper { public Paper (string Name, double Width, double Length ) { this .Name = Name; this .Length = Length; this .Width = Width; } public string Name { get ; } public double Length { get ; } public double Width { get ; } }
之后通过 StreamReader
逐行读取(首行不读取)即可:
List<Paper> result = new List<Paper>(); string path = Path.Combine(installPath, @"MCADSetting\PaperSet.dat" );using (StreamReader reader = new StreamReader(path)){ int index = 0 ; string line; while ((line = reader.ReadLine()) != null ) { if (index++ == 0 ) continue ; string [] item = line.Split(new [] { ' ' , '\t' }, StringSplitOptions.RemoveEmptyEntries); if (item.Length < 2 ) continue ; Paper paper = new Paper(item[0 ], Double.Parse(item[1 ]), Double.Parse(item[2 ])); result.Add(paper); } }
String.Split
方法可以参考微软的这篇 如何在 C# 中使用 String.Split 分隔字符串 , StringSplitOptions.RemoveEmptyEntrie
s 参数来排除返回数组中的任何空字符串。要对返回的集合进行更复杂的处理,可使用 LINQ 来处理结果序列。
CADNET & PLM 接口使用 GetCADApplication 获取 CAD 应用程序,首先明确我们的思路:
若 CAD 程序已经存在,则直接跳转至已打开的 CAD 程序 否则新建 CAD 应用程序 try { IGcadApplication app = Marshal.GetActiveObject("GStarCAD.Application" ) as IGcadApplication; app.Visible = true ; } catch (Exception exception){ app = new GcadApplicationClass(); app.Visible = true ; ...... }
这里捕获用到了 System.Runtime.InteropServices
中的 Marshal.GetActiveObject()
方法。
注意 app.Visible
可以让后台的应用程序前置显示,在忘记关闭 Quit()
时会出现多个后台运行程序,在项目的前期测试造成了不少麻烦。
CloseFile 该命令会传入一个图纸路径 filePath
,关闭该指定路径的图纸。
关闭图纸的业务场景相对简单,只需要判断两点:
当前 CAD 是否打开过任何图纸 在已打开过的图纸中是否有和 filePath
相对应的图纸 最后对找到的图纸进行关闭操作即可。
if (app.Documents.Count <= 0 ) return ; foreach (GcadDocument doc in app.Documents){ if (string .Equals(doc.FullName, filePath, StringComparison.OrdinalIgnoreCase)) { doc.Close(); return ; } } return ;
需要注意的是这里需要用 doc.FullName
而非 doc.Name
,以规避出现诸如 C:\test.dwg
和 D:\test.dwg
在 Name
中均为 test.dwg
的末端图纸路径重名的情况。
OpenFile 与前面的关闭命令类似,该命令会传入一个图纸路径 filePath
,若已经打开图纸则直接返回,否则打开该指定路径的图纸。
if (app.Documents.Count > 0 ){ foreach (GcadDocument doc in app.Documents) { if (string .Equals(doc.FullName, path, StringComparison.OrdinalIgnoreCase)) return ; } } app.Documents.Open(path);
SaveFile 保存文件也是类似的,先传入一个图纸路径 filePath
,若该图纸还未被打开则直接返回,否则保存该指定路径的图纸。
多的一种情况是 CAD 在图纸为只读模式的前提下逻辑上应禁止保存,这点注意一下即可。
if (appController.CadApp.Documents.Count <= 0 ) return Message.SendMessage(false , "没有图纸被打开,保存失败" ); foreach (GcadDocument doc in appController.CadApp.Documents){ if (string .Equals(doc.FullName, path, StringComparison.OrdinalIgnoreCase)) { if (doc.ReadOnly) return Message.SendMessage(false , "指定图纸为只读模式,保存失败" ); ; doc.Save(); } } return ;
GetOpeningFile 获取打开文件直接调用 ActiveDocument
即可。
但是当一张图纸新建之后并未保存(诸如 Drawing1.dwg
这样),需要系统变量 DWGTITLED
来判断是否为这种尚未保存的情况:
DWGTITLED = 0
:Drawing has not been namedDWGTITLED = 1
:Drawing has been namedif (appController.CadApp.Documents.Count <= 0 ) return ; GcadDocument doc = appController.CadApp.ActiveDocument; int titled = 0 ;string dwgTitled = doc.GetVariable("DWGTITLED" ).ToString();if (!int .TryParse(dwgTitled, out titled)) return ; return titled == 0 ? null : doc.FullName;
NewtonSoftJson自定义Convertor序列化Json 这个项目一个比较关键的过程就是去解析图纸数据并转换为 Json 格式,前面 PLM 接口负责解析数据,那么该如何将这些数据转换为 Json 呢?
官方样例 查阅官网的 Custom JsonConverter ,给出了一个比较完整的示例:
public class KeysJsonConverter : JsonConverter { private readonly Type[] _types; public KeysJsonConverter (params Type[] types ) { _types = types; } public override void WriteJson (JsonWriter writer, object value , JsonSerializer serializer ) { JToken t = JToken.FromObject(value ); if (t.Type != JTokenType.Object) { t.WriteTo(writer); } else { JObject o = (JObject)t; IList<string > propertyNames = o.Properties().Select(p => p.Name).ToList(); o.AddFirst(new JProperty("Keys" , new JArray(propertyNames))); o.WriteTo(writer); } } public override object ReadJson (JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) { throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter." ); } public override bool CanRead { get { return false ; } } public override bool CanConvert (Type objectType ) { return _types.Any(t => t == objectType); } } public class Employee { public string FirstName { get ; set ; } public string LastName { get ; set ; } public IList<string > Roles { get ; set ; } }
Employee employee = new Employee { FirstName = "James" , LastName = "Newton-King" , Roles = new List<string > { "Admin" } }; string json = JsonConvert.SerializeObject(employee, Formatting.Indented, new KeysJsonConverter(typeof (Employee)));Console.WriteLine(json); Employee newEmployee = JsonConvert.DeserializeObject<Employee>(json, new KeysJsonConverter(typeof (Employee))); Console.WriteLine(newEmployee.FirstName);
抽象转换器类 首先继承 JsonConverter
构建抽象类 AbstractJsonConverter
。该类主要用于实现 JsonConverter
的方法并留下 RewriteWriter()
作为真正重写转换方法的入口:
abstract public class AbstractJsonConverter : JsonConverter { protected readonly Type _type; public AbstractJsonConverter (Type type ) { _type = type; } public override void WriteJson (JsonWriter writer, object value , JsonSerializer serializer ) { if (!(value is IDatabase db)) { writer.WriteNull(); return ; } RewriteWriter(writer, db, serializer); } public override object ReadJson (JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) { throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter." ); } public override bool CanRead => false ; public override bool CanWrite => true ; public override bool CanConvert (Type objectType ) { return typeof (IDatabase) == _type; } abstract protected void RewriteWriter (JsonWriter writer, IDatabase db, JsonSerializer serializer ) ; }
在这个过程中 CanConvert
方法的实现遇到了困难,因为该项目以 .exe
可执行程序为载体,所以调用诸如 IDatabase
这样的类型时获取到的 type
是COM组件而非具体的类型。
为了获取真正的类型我尝试过使用原生的类型获取方法、反射获取方法以及 VisualBasic 的内置方法,但均以失败告终,只好以官方示例为准构造时手动传入类型做判断。
维拓转换器类 真正的维拓转换器类我们只需要关心如何实现 RewriteWriter()
即可:
public class WeiTuoDBJsonConverter : AbstractJsonConverter { public WeiTuoDBJsonConverter (Type type ) : base (type ) { } protected override void RewriteWriter (JsonWriter writer, IDatabase db, JsonSerializer serializer ) { writer.WriteStartObject(); writer.WritePropertyName(dataText); WriteDataBaseJson(writer, db, serializer); writer.WriteEndObject(); } void WriteDataBaseJson (JsonWriter writer, IDatabase db, JsonSerializer serializer ) { if (db is null ) { writer.WriteNull(); return ; } writer.WriteStartArray(); foreach (var paperName in db.PaperNames) { IPaper paper = db[paperName]; writer.WriteStartObject(); writer.WritePropertyName(paperName); writer.WriteStartObject(); WritePaperJson(writer, paper, serializer); writer.WriteEndObject(); writer.WriteEndObject(); } writer.WriteEndArray(); } void WritePaperJson (JsonWriter writer, IPaper paper, JsonSerializer serializer ) { if (paper is null ) { writer.WriteNull(); return ; } var resolver = serializer.ContractResolver as DefaultContractResolver; ITitle title = paper?.Title; writer.WritePropertyName(resolver == null ? nameof (title) : resolver.GetResolvedPropertyName(nameof (title))); WriteTitleJson(writer, title); IBom bom = paper?.Bom; writer.WritePropertyName(resolver == null ? nameof (bom) : resolver.GetResolvedPropertyName(nameof (bom))); writer.WriteStartArray(); WriteBomJson(writer, bom); writer.WriteEndArray(); IFrame frame = paper?.Frame; writer.WritePropertyName(resolver == null ? nameof (frame) : resolver.GetResolvedPropertyName(nameof (frame))); WriteFrameJson(writer, frame, serializer); ...... } void WriteTitleJson (JsonWriter writer, ITitle title ) { if (title is null ) { writer.WriteNull(); return ; } writer.WriteStartObject(); foreach (string name in title.Names) { writer.WritePropertyName(name); writer.WriteValue(title[name]); } writer.WriteEndObject(); } void WriteBomJson (JsonWriter writer, IBom bom ) { if (bom is null ) { writer.WriteNull(); return ; } foreach (var serialNumber in bom.SerialNumbers) { IBomRow bomRow = bom[serialNumber]; writer.WriteStartObject(); foreach (var name in bomRow.Names) { writer.WritePropertyName(name); writer.WriteValue(bomRow[name]); } writer.WriteEndObject(); } } void WriteFrameJson (JsonWriter writer, IFrame frame, JsonSerializer serializer ) { if (frame is null ) { writer.WriteNull(); return ; } writer.WriteStartObject(); ...... writer.WriteEndObject(); } }
解析命令使用 最后使用上就非常简单了,直接在序列化 SerializeObject()
中传入我们定制的转换器即可:
return JsonConvert.SerializeObject(db, new JsonSerializerSettings { Formatting = Formatting.Indented, ContractResolver = new CamelCasePropertyNamesContractResolver(), Converters = new List<JsonConverter> { new WeiTuoDBJsonConverter(typeof (IDatabase)) }
Formatting.Indented
代表换行CamelCasePropertyNamesContractResolver()
代表属性为驼峰命名法转换器优化 虽然可以看到这种方式能够转换,但其实是手动实现解析的,我们真正希望的是可以自动识别并获取相应的转换器,完全通过成员关系序列化对象。
可能这种说法有些抽象,我还是以刚才的例子来讲。WeiTuoDBJsonConverter
可以解析 IDatabase
,但如果后期需要单独解析 ITitle
或者 IBom
则需要重新实现对应的转换器类,现有的代码所有的解析都是作为函数的形式(如 WriteFrameJson()
)放在转换器类中,不够灵活并且可维护性差。我们希望的是每个类都有对应的转换器类,然后需要的时候将这些转换器进行组合。
具体转换器类 下面就以明细表为例来讲讲如何通过逐级实现转换器来达到目标。首先我们先梳理一下逻辑关系:
IFrame |__ IBom |__ IBomRow
如图所示,图框对象 IFrame
包含了 IBom
明细表对象,而 IBom
又包含了若干 IBomRow
明细表行对象。那么首先我们从最底层的 IBomRow
开始实现 BomRowConverter
:
public class BomRowConverter : JsonConverter <IBomRow >{ public override bool CanRead => false ; public override bool CanWrite => true ; public override void WriteJson (JsonWriter writer, IBomRow bomRow, JsonSerializer serializer ) { if (bomRow is null ) { writer.WriteNull(); return ; } writer.WriteStartObject(); foreach (string name in bomRow.Names) { writer.WritePropertyName(name); writer.WriteValue(bomRow[name]); } writer.WriteEndObject(); } public override IBomRow ReadJson (JsonReader reader, Type objectType, IBomRow existingValue, bool hasExistingValue, JsonSerializer serializer ) { throw new NotImplementedException(); } }
一个简单的字典结构,遍历就可以序列化完成。BomConverter
的实现则略有不同:
public class BomConverter : JsonConverter <IBom >{ public override bool CanRead => false ; public override bool CanWrite => true ; public override void WriteJson (JsonWriter writer, IBom bom, JsonSerializer serializer ) { if (bom is null ) { writer.WriteNull(); return ; } writer.WriteStartArray(); for (int r = 0 ; r < bom.RowCount; r++) serializer.Serialize(writer, bom[r]); writer.WriteEndArray(); } public override IBom ReadJson (JsonReader reader, Type objectType, IBom existingValue, bool hasExistingValue, JsonSerializer serializer ) { throw new NotImplementedException(); } }
可以看到对于已有转换器的对象我们摒弃了先前的 writer
,转而通过 serializer.Serialize()
的方式序列化 IBomRow
对象,FrameConverter
同理:
public class FrameConverter : JsonConverter <IFrame >{ public override bool CanRead => false ; public override bool CanWrite => true ; public override void WriteJson (JsonWriter writer, IFrame frame, JsonSerializer serializer ) { if (frame is null ) { writer.WriteNull(); return ; } var properties = typeof (IFrame).GetProperties(BindingFlags.Instance | BindingFlags.Public); var resolver = serializer.ContractResolver as DefaultContractResolver; writer.WriteStartObject(); foreach (PropertyInfo propertyInfo in properties) { writer.WritePropertyName(resolver?.GetResolvedPropertyName(propertyInfo.Name) ?? propertyInfo.Name); writer.WriteValue(propertyInfo.GetValue(frame)); } writer.WriteEndObject(); } public override IFrame ReadJson (JsonReader reader, Type objectType, IFrame existingValue, bool hasExistingValue, JsonSerializer serializer ) { throw new NotImplementedException(); } }
通过反射技术获取属性,遍历属性来完成对子属性的定制序列化。
静态方法输出 后面的事情就水到渠成了,只需要将相应的转换器放给对应的打印函数即可:
public static class StructureDataExtension { private static bool CanConvert (Type interfaceType, JsonSerializerSettings settings ) { return settings != null && settings.Converters.Any(converter => converter.CanConvert(interfaceType)); } private static string SerializeObject (object obj, JsonSerializerSettings settings, params (Type InterfaceType, Type ConverterType )[] types) { settings = settings ?? new JsonSerializerSettings(); var defaultSettings = JsonConvert.DefaultSettings?.Invoke(); foreach ((Type interfaceType, Type converterType) in types) { if (CanConvert(interfaceType, settings) || CanConvert(interfaceType, defaultSettings)) continue ; settings.Converters.Add((JsonConverter)Activator.CreateInstance(converterType)); } return JsonConvert.SerializeObject(obj, settings); } public static string Dump (this IBomRow bomRow, JsonSerializerSettings settings = null ) { if (bomRow == null ) throw new ArgumentNullException(nameof (bomRow)); return SerializeObject(bomRow, settings, (typeof (IBomRow), typeof (BomRowConverter))); } public static string Dump (this IBom bom, JsonSerializerSettings settings = null ) { if (bom == null ) throw new ArgumentNullException(nameof (bom)); return SerializeObject(bom, settings, (typeof (IBom), typeof (BomConverter)), (typeof (IBomRow), typeof (BomRowConverter))); } public static string Dump (this IFrame frame, JsonSerializerSettings settings = null ) { if (frame == null ) throw new ArgumentNullException(nameof (frame)); return SerializeObject(frame, settings, (typeof (IFrame), typeof (FrameConverter))); } }
这里还需要考虑一下 JsonSerializerSettings
的优先级,用户 > 定制默认 > 系统默认,如果已经包含了可以解析的转换器就没有必要再放入相应的转换器了。