[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

目       录

第十章           宿主程序详细设计... 2

10.1        配置文件设计... 3

10.2        加载设备驱动... 4

10.3        加载界面视图... 8

10.4        加载数据导出... 12

10.5        加载服务组件... 14

10.6        全局异常监测... 17

10.7        小结... 19

第十章     宿主程序详细设计

前几章对设备驱动、IO实例、外部接口和总体控制器等进行了详细介绍,这些都是框架平台的有机组成部分,这部分相当于后台服务的支撑组件,通过模块化实现框架平台的搭建;宿主程序也是框架平台的一部分,作为承载插件的一个软件平台,是人机交互的唯一接口,通过鼠标点击完成各种指令,是插件式框架平台最终要实现的部分。在《第2章 框架总体的设计》的“2.1 宿主程序设计”中对宿主程序的整体功能和界面进行了规划和设计,但是并没有涉及到细节层面,要实现这些设计的功能,包括3方面工作:界面的实现,也就是UI布局,涉及到少量的代码控制;与插件(设备驱动、图形显示、数据导出和服务组件)进行交互,把需要的插件加载到宿主程序中,最终传递给后台服务;与《第8章 总体控制器的设计》中的IDeviceController总控制器接口进行交互,可以理解为与后台服务的支撑组件进行交互,接收宿主程序的输入,一般为插件信息、操作响应等。交互的结构示意图如下:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

宿主程序接受来自人员的动作,通过配置文件完成加载插件或者把已经加载的插件与总控器进行交互。当然,宿主程序也可能与其他辅助事务进行交互。

10.1     配置文件设计

加载插件的方式有很多种,可以通过遍历指定目录下的程序集,找到相应的插件接口类型,并且加载到框架平台,现在有很多编辑软件都是采用的这样方式。但是我感觉这种方式多少有些暴力,不能任何人来到你家门前就允许他进门的。所以,SuperIO框架平台采用一种更友好的方式,通过配置文件加载插件。把二次开发好的插件信息配置到相应的文件中,只有插件信息“合法”的情况下才会根据情况加载插件到框架平台中。

基于这样思想,就需要对配置文件进行设计,以什么样的文件格式保存信息,以及都保存什么样的信息。

配置文件的格式采用XML方式,对.NET Framework的System.Configuration.Configuration工具类进行二次封装。先定义一个接口,对操作配置文件进行规范,接口定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

配置文件保存什么样的信息,取决于应用过程中所需要的信息,不同的插件可能用到的配置信息不一样。那么先定义一个基础的配置信息,包括:插件文件路径、实例类信息(命令空间和类名)、标题和标注等信息,以便可以通过反射工具类加载插件。配置信息类定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

使用配置文件操作基类生成的文件格式如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

10.2     加载设备驱动

设备驱动的配置文件与基础配置文件不一样,主要涉及到两部分:可挂载的设备驱动信息和已经挂载到框架平台的驱动信息。

可挂载的设备驱动信息在AssemblyDeviceSectionGroup配置组中进行配置,当挂载新的设备驱动的时候,如增加设备,就会从这个配置组中加载信息,以便操作人员进行选择。配置组下的设备驱动配置信息定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

当调用增加设备窗体的时候会从配置文件读取程序集信息,如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

当触发增加设备事件的时候,会创建新的设备驱动,代码实现如下:

public static IRunDevice CreateDeviceInstance(int devid, int devaddr, string devname, int assemblyid, string assemblyname, string instance, CommunicationType type, DeviceType devType, object iopara1, object iopara2)
{
IObjectBuilder builder = new TypeCreator();
IRunDevice dev = builder.BuildUp<IRunDevice>(Application.StartupPath + "\\SuperIO\\DeviceConfig\\" + assemblyname, instance);
dev.DeviceParameter.DeviceAddr = devaddr;
dev.DeviceParameter.DeviceName = devname;
dev.DeviceRealTimeData.DeviceName = devname;
if (type == CommunicationType.COM)
{
dev.DeviceParameter.COM.Port = (int)iopara1;
dev.DeviceParameter.COM.Baud = (int)iopara2;
}
else if (type == CommunicationType.NET)
{
dev.DeviceParameter.NET.RemoteIP = (string)iopara1;
dev.DeviceParameter.NET.RemotePort = (int)iopara2;
}
dev.IsRegLicense = true;
dev.CommunicationType = type;
dev.UserLevel = UserLevel.High;
dev.InitDevice(devid);
if (!Device.DebugDevice.IsDebug)
{
//--------------------把设备信息配制到文件中------------------------//
CurrentDeviceSection section = new CurrentDeviceSection();
section.DeviceID = dev.DeviceParameter.DeviceID;
section.AssemblyID = assemblyid;
section.X = 0;
section.Y = 0;
section.Note = String.Empty;
section.CommunicateType = dev.CommunicationType;
DeviceAssembly.AddDeviceToXml(section);
//---------------------------------------------------------------//
}
return dev;
}

已经挂载到框架平台的驱动信息在CurrentDeviceSectionGroup配置组中进行配置,也就是对已经挂载的设备驱动,在下次启动框架平台的时候要自动把这些设备驱动挂载到平台下运行,当发生删除设备驱动事件时从该配置组中删除相关信息。可以设置通讯类型改变设备驱动的通讯模式。配置组下的配制信息定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

当下次启动框架平台时会从这个配置组加载并实例化设备驱动,挂载到框架平台下运行设备驱动实例,在宿主程序中显示设备驱动实例实时运行状态。加载设备驱动代码如下:

public static List<IRunDevice> LoadDevicesFromXml()
{
List<IRunDevice> list = new List<IRunDevice>();
CurrentDeviceSectionGroup curgroup = (CurrentDeviceSectionGroup)_Source.Configuration.GetSectionGroup("CurrentDeviceSectionGroup");
if (curgroup == null)
{
throw new NullReferenceException("获得当前设备配置信息为空");
}
AssemblyDeviceSectionGroup asmgroup = (AssemblyDeviceSectionGroup)_Source.Configuration.GetSectionGroup("AssemblyDeviceSectionGroup");
if (asmgroup == null)
{
throw new NullReferenceException("获得设备程序集信息为空");
}
IObjectBuilder creator = new TypeCreator();
for (int i = 0; i < curgroup.Sections.Count; i++)
{
CurrentDeviceSection cursect = (CurrentDeviceSection)curgroup.Sections[i];
if (cursect.AssemblyID >= 0)
{
for (int j = 0; j < asmgroup.Sections.Count; j++)
{
AssemblyDeviceSection asmsect = (AssemblyDeviceSection)asmgroup.Sections[j];
if (cursect.AssemblyID == asmsect.AssemblyID)
{
string assemblypath = Application.StartupPath + "\\SuperIO\\DeviceConfig\\" + asmsect.AssemblyName;
IRunDevice dev = creator.BuildUp<IRunDevice>(assemblypath, asmsect.Instance);
dev.InitDevice(cursect.DeviceID);
dev.CommunicationType = cursect.CommunicateType;
list.Add(dev);
break;
}
}
}
}
return list;
}

设备驱动整体配置文件如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

10.3     加载界面视图

组态软件会有一个图形和UI引擎来支持图形数字化显示,允许二次开发者通过拖拽UI组件进行图形化设计,并设置UI组件的属性与IO变量进行关联来显示数据信息。

考虑到开发成本和人力成本,SuperIO没有像组态软件的方式实现图形化显示,但是自定义图形化UI显示部分是必须实现的,满足不同用户、不同应用场景的需求。

SuperIO是通过事件和接口的方式来实现自定义图形显示。设备驱动对数据进行打包,打包后的数据可能是:字符串(数组)、类对象、字节数组等,通过调用事件(OnDeviceObjectChangedHandler)把打包后的数据以对象的形式传递给图形显示接口(IGraphicsShow),再反向解析数据信息显示在不同的UI组件上,支持多种显示风格。

那么,这样就支持二次开发者继承图形显示接口(IGraphicsShow),独立开发一个组件(DLL),并且挂载到配置文件中,当鼠标单事菜单的图形显示项时自动以插件的形式加载DLL,并以FormTab的形式显示图形界面。

针对加载界面视图的整个过程涉及到:配制文件、加载视图菜单、单击事件显示视图等。

配置文件与基础配置文件一样,配置文件定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

当框架平台启动时,会自动加载配置文件,并显示在界面视图的菜单中,加载配置文件的代码如下:

private void LoadShowView()
{
IConfigurationSource source = new SuperIO.ShowConfiguration.ShowConfigurationSource();
source.Load();
for (int i = 0; i < source.Configuration.SectionGroups.Count; i++)
{
if (source.Configuration.SectionGroups[i].GetType() == typeof(SuperIO.ShowConfiguration.ShowSectionGroup))
{
SuperIO.ShowConfiguration.ShowSectionGroup group = (SuperIO.ShowConfiguration.ShowSectionGroup)source.Configuration.SectionGroups[i]
Font font = new Font("Tahoma", 12);
SuperIO.ShowConfiguration.ShowSection section=null;
for (int j = 0; j < group.Sections.Count; j++)
{
section = (SuperIO.ShowConfiguration.ShowSection)group.Sections[j];
BarButtonItem bt = new BarButtonItem(this.barManager1, section.Caption);
bt.ItemAppearance.SetFont(font);
bt.Tag = section.Name + "," + section.Instance + "," + section.Caption;
bt.ItemClick += new ItemClickEventHandler(ViewItem_ItemClick);
barGraphicsView.AddItem(bt);
}
break;
}
}
}

当鼠标单击菜单项时会触发ViewItem_ItemClick函数,并加载、显示视图界面,定义的代码如下:

private void ViewItem_ItemClick(object sender, ItemClickEventArgs e)
{
try
{
string[] arr = e.Item.Tag.ToString().Split(',');
SuperIO.ShowConfiguration.ShowConfigurationSource source = new SuperIO.ShowConfiguration.ShowConfigurationSource();
IObjectBuilder builder = new TypeCreator();
Form form = builder.BuildUp<Form>(Application.StartupPath + "\\SuperIO\\ShowConfig\\" + arr[0], arr[1]);
if (this._DeviceController.AddGraphicsShow((IGraphicsShow)form))
{
form.Text = arr[2].ToString();
form.MdiParent = this;
form.Show();
}
else
{
form.Dispose();
}
}
catch (System.Exception ex)
{
MessageBox.Show(ex.Message);
}
}

在这个过程中,有一个问题考虑到,就是多次单击同一个菜单视图项时,不能多次显示同一个界面视图窗体,主要考虑到设计的合理性和执行的效率。_DeviceController.AddGraphicsShow((IGraphicsShow)form)函数的代码定义如下:

public bool AddGraphicsShow(IGraphicsShow graphicsShow)
{
if (!_dataShowController.Contain(graphicsShow.ThisKey))
{
_dataShowController.Add(graphicsShow.ThisKey, graphicsShow);
graphicsShow.MouseRightContextMenuHandler += new MouseRightContextMenuHandler(RunContainer_MouseRightContextMenuHandler);
graphicsShow.GraphicsShowClosedHandler+=new GraphicsShowClosedHandler (GraphicsShow_GraphicsShowClosedHandler);
DeviceMonitorLog.WriteLog(String.Format("<{0}>显示视图已经打开", graphicsShow.ThisName));
return true;
}
else
{
DeviceMonitorLog.WriteLog(String.Format("<{0}>显示视图已经存在", graphicsShow.ThisName));
return false;
}
}

10.4     加载数据导出

一般情况下用不到数据导出插件接口功能,但是在做项目的时候的确会发生多种数据格式进行交互的场景,例如:N个厂家要集成自己的数据信息,但是规定的数据格式又不一样,又迫于用户的威逼,不得不配合工作。那么就可以用数据导出插件来完成。

有人会质疑:这样的功能不能在设备驱动和显示视图中完成吗?当然可以这样操作。但是,我不想把开发稳定的设备驱动和显示视图功能模块随意增加代码,更不愿意为了本来不相干的事情可能影响到本来应该干好的事情,对于有“洁癖”的开发者来讲更不愿意这样干。本来设备驱动在框架平台中就是可变的因子,但是对于数据导出又是相对稳定的部分,所以把数据导出功能再次解耦出来,单独设计成插件组件的方式。

这部分功能设计的比较简单,也是通过配置文件的方式挂载插件,每次启动框架平台都会把配置文件中的数据导出插件加载进来,直到框架平台退出。也就是说挂载相应的插件就有相应的导出数据功能,不加载插件就不会有任何操作。

配置文件与基础配置文件一样,配置文件定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

加载插件的代码定义如下:

public static List<IExportData> GetExportInstances()
{
List<IExportData> exportlist = new List<IExportData>();
ExportConfigurationSource source = new ExportConfigurationSource();
source.Load();
ExportSectionGroup group = (ExportSectionGroup)source.Configuration.GetSectionGroup("Export");
if (group == null)
{
throw new NullReferenceException("获得导出程序集信息为空");
} IObjectBuilder builder = new TypeCreator();
foreach (ExportSection section in group.Sections)
{
IExportData export = builder.BuildUp<IExportData>(Application.StartupPath+ "\\SuperIO\\ExportConfig\\" + section.Name, section.Instance);
exportlist.Add(export);
}
return exportlist;
}

10.5     加载服务组件

设备驱动的职能只负责与硬件设备进行交互,不能把事务性的服务加到设备驱动中,否则可能会影响数据正常的交互;界面视图只负责对采集上来的数据进行实时显示,不能把事务性的服务加到界面视图中,否则会影响人机交互的体验感;数据导出只负责对数据进行格式化并导出到相应的介质中,不能把事务性的服务加到数据导出中,否则数据导出不具备功能界面的交互能力。

服务组件针对特殊的事务性服务场景,请参见《7.外部接口的设计》。服务组件的加载过程与界面视图的加载过程类似。

配置文件与基础配置文件一样,配置文件定义如下图:

[连载]《C#通讯(串口和网络)框架的设计与实现》- 10.宿主程序详细设计

当框架平台启动时,会自动加载配置文件,并显示在服务的菜单中,加载配置文件的代码如下:

private void LoadServices()
{
IConfigurationSource source = new SuperIO.ServicesConfiguration.ServicesConfigurationSource();
source.Load();
List<IAppService> serviceList = new List<IAppService>();
for (int i = 0; i < source.Configuration.SectionGroups.Count; i++)
{
if (source.Configuration.SectionGroups[i].GetType() == typeof(SuperIO.ServicesConfiguration.ServicesSectionGroup))
{
IObjectBuilder builder = new TypeCreator();
SuperIO.ServicesConfiguration.ServicesSectionGroup group = (SuperIO.ServicesConfiguration.ServicesSectionGroup)source.Configuration.SectionGroups[i];
Font font = new Font("Tahoma", 12);
SuperIO.ServicesConfiguration.ServicesSection section=null;
for (int j = 0; j < group.Sections.Count; j++)
{
section = (SuperIO.ServicesConfiguration.ServicesSection)group.Sections[j];
IAppService appService = builder.BuildUp<IAppService>(Application.StartupPath + "\\SuperIO\\ServicesConfig\\" + section.Name, section.Instance);
appService.ServiceType = section.ServiceType;
appService.IsAutoStart = section.IsAutoStart;
serviceList.Add (appService);
if (section.ServiceType == ServiceType.Show)
{
BarButtonItem bt = new BarButtonItem(this.barManager1, section.Caption);
bt.ItemAppearance.SetFont(font);
bt.Tag = appService.ThisKey;
bt.ItemClick += new ItemClickEventHandler(ServiceItem_ItemClick);
barServices.AddItem(bt);
}
}
break;
}
}
_DeviceController.AddAppService(serviceList);
}

_DeviceController.AddAppService(serviceList)代码会把服务插件的实例增加到控制器中,代码如下:

public void AddAppService(List<IAppService> serviceList)
{
foreach (IAppService service in serviceList)
{
if (!_appServiceManager.Contain(service.ThisKey))
{
service.WriteLogHandler += new WriteLogHandler(Service_WriteLogHandler);
if (service.IsAutoStart)
{
service.StartService();
}
_appServiceManager.Add(service.ThisKey, service);
DeviceMonitorLog.WriteLog(String.Format("<{0}>应用服务已经打开", service.ThisName)); }
else
{
DeviceMonitorLog.WriteLog(String.Format("<{0}>应用服务已经存在", service.ThisName));
}
}
}

当单击菜单服务项时会调用ServiceItem_ItemClick函数,会调用服务组件的单击事件函数,代码如下:

public void OnClickAppService(string key)
{
IAppService service = _appServiceManager.Get(key);
if (service != null)
{
service.OnClick();
}
}

10.6     全局异常监测

框架平台的稳定性始终是重中之重,在运行过程中可能发生其他未知异常信息,针对这些异常是无法用已经存在的try…catch…来捕捉的。

在启动框架平台的时候增加了ThreadException和UnhandledException事件对未知异常进行捕捉,并把事件中对异常信息进行详细的记录。

ThreadException事件,此事件允许 Windows 窗体应用程序处理 Windows 窗体线程中所发生的其他未经处理的异常。 请将事件处理程序附加到 ThreadException 事件以处理这些异常,因为这些异常将使您的应用程序处于未知状态。 应尽可能使用结构化异常处理块来处理异常。详情请参见MSDN。

UnhandledException事件,此事件提供未捕获到的异常的通知。 此事件使应用程序能在系统默认处理程序向用户报告异常并终止应用程序之前记录有关异常的信息。 如果有足够的有关应用程序状态的信息,则可以采取其他措施,如保存程序数据以便于以后进行恢复。 建议谨慎行事,因为未经处理的异常时可能会损坏程序数据。详情请参见MSDN。

应用的代码如下:

public class MonitorException
{
[SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
public static void Monitor()
{
Application.ThreadException += new ThreadExceptionEventHandler(MainThreadException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
} public static void UnMonitor()
{
Application.ThreadException -= new ThreadExceptionEventHandler(MainThreadException);
AppDomain.CurrentDomain.UnhandledException -= new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } private static void MainThreadException(object sender, ThreadExceptionEventArgs e)
{
try
{
ShowException(e.Exception);
}
catch(Exception ex)
{
GeneralLog.WriteLog(ex);
}
} private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) {
ShowException((Exception)e.ExceptionObject);
} private static void ShowException(Exception ex)
{
GeneralLog.WriteLog(ex);
}
}

因为这是一个静态事件,所以释放应用程序时必须分离事件处理程序,否则会导致内存泄漏。

10.7     小结

至此,框架平台的雏形就已经完成了,二次开发设备驱动、数据显示、数据导出和服务组件,进行组件挂载,加载、运行的整个流程都可以走通了。

但是,对于二次开发还应该具有调试功能,下一章节中介绍《第11章   调试器设计》

作者:唯笑志在

Email:504547114@qq.com

QQ:504547114

.NET开发技术联盟:54256083

文档下载:http://pan.baidu.com/s/1pJ7lZWf

官方网址:http://www.bmpj.net

上一篇:django发送邮件的坑


下一篇:shell 中命令输入的快!捷!键!