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

简介: 目       录 第十章           宿主程序详细设计... 2 10.1        配置文件设计... 3 10.2        加载设备驱动... 4 10.3        加载界面视图.

目       录

第十章           宿主程序详细设计... 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总控制器接口进行交互,可以理解为与后台服务的支撑组件进行交互,接收宿主程序的输入,一般为插件信息、操作响应等。交互的结构示意图如下:

 

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

10.1     配置文件设计

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

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

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

 

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

 

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

10.2     加载设备驱动

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

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

 

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

 

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

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

 

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

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;
}

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

10.3     加载界面视图

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

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

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

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

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

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

 

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

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个厂家要集成自己的数据信息,但是规定的数据格式又不一样,又迫于用户的威逼,不得不配合工作。那么就可以用数据导出插件来完成。

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

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

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

 

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

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.外部接口的设计》。服务组件的加载过程与界面视图的加载过程类似。

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

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

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

相关文章
|
3月前
|
存储 安全 Java
程序与技术分享:C#值类型和引用类型的区别
程序与技术分享:C#值类型和引用类型的区别
29 0
|
6天前
|
C# 容器
C#中的命名空间与程序集管理
在C#编程中,`命名空间`和`程序集`是组织代码的关键概念,有助于提高代码的可维护性和复用性。本文从基础入手,详细解释了命名空间的逻辑组织方式及其基本语法,展示了如何使用`using`指令访问其他命名空间中的类型,并提供了常见问题的解决方案。接着介绍了程序集这一.NET框架的基本单位,包括其创建、引用及高级特性如强名称和延迟加载等。通过具体示例,展示了如何创建和使用自定义程序集,并提出了针对版本不匹配和性能问题的有效策略。理解并善用这些概念,能显著提升开发效率和代码质量。
19 4
|
12天前
|
Linux C# 开发者
Uno Platform 驱动的跨平台应用开发:从零开始的全方位资源指南与定制化学习路径规划,助您轻松上手并精通 C# 与 XAML 编程技巧,打造高效多端一致用户体验的移动与桌面应用程序
【9月更文挑战第8天】Uno Platform 的社区资源与学习路径推荐旨在为初学者和开发者提供全面指南,涵盖官方文档、GitHub 仓库及社区支持,助您掌握使用 C# 和 XAML 创建跨平台原生 UI 的技能。从官网入门教程到进阶技巧,再到活跃社区如 Discord,本指南带领您逐步深入了解 Uno Platform,并提供实用示例代码,帮助您在 Windows、iOS、Android、macOS、Linux 和 WebAssembly 等平台上高效开发。建议先熟悉 C# 和 XAML 基础,然后实践官方教程,研究 GitHub 示例项目,并积极参与社区讨论,不断提升技能。
30 2
|
1月前
|
存储 网络协议 安全
|
21天前
|
存储 算法 Java
Java中的集合框架深度解析云上守护:云计算与网络安全的协同进化
【8月更文挑战第29天】在Java的世界中,集合框架是数据结构的代言人。它不仅让数据存储变得优雅而高效,还为程序员提供了一套丰富的工具箱。本文将带你深入理解集合框架的设计哲学,探索其背后的原理,并分享一些实用的使用技巧。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇通往高效编程的大门。
|
1月前
|
数据采集 存储 中间件
Python进行网络爬虫:Scrapy框架的实践
【8月更文挑战第17天】网络爬虫是自动化程序,用于从互联网收集信息。Python凭借其丰富的库和框架成为构建爬虫的首选语言。Scrapy作为一款流行的开源框架,简化了爬虫开发过程。本文介绍如何使用Python和Scrapy构建简单爬虫:首先安装Scrapy,接着创建新项目并定义爬虫,指定起始URL和解析逻辑。运行爬虫可将数据保存为JSON文件或存储到数据库。此外,Scrapy支持高级功能如中间件定制、分布式爬取、动态页面渲染等。在实践中需遵循最佳规范,如尊重robots.txt协议、合理设置爬取速度等。通过本文,读者将掌握Scrapy基础并了解如何高效地进行网络数据采集。
123 6
|
1月前
|
机器学习/深度学习 人工智能 PyTorch
AI智能体研发之路-模型篇(五):pytorch vs tensorflow框架DNN网络结构源码级对比
AI智能体研发之路-模型篇(五):pytorch vs tensorflow框架DNN网络结构源码级对比
57 1
|
19天前
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
34 0
|
1月前
|
Java 应用服务中间件 Linux
(九)Java网络编程无冕之王-这回把大名鼎鼎的Netty框架一网打尽!
现如今的开发环境中,分布式/微服务架构大行其道,而分布式/微服务的根基在于网络编程,而Netty恰恰是Java网络编程领域的无冕之王。Netty这个框架相信大家定然听说过,其在Java网络编程中的地位,好比JavaEE中的Spring。