2023年2月

很久没有写C#进行Visio二次开发的文章了,这次温习一下Visio二次开发的相关知识,全面总结一下Visio 二次开发的方方面面。一方面让对Visio的开发不太了解人员有一个全局的认识,对已经看过我前面文章的人来一个回顾总结。

本次主要根据我在Visio二次开发过程中,获得的一些实际系统开发经验以及学习历程,分三个方面对下面内容进行介绍:

1)介绍C#的Visio二次开发管理系统的架构设计思路

2)介绍C#进行Visio开发的准备工作

3)Visio的编程对象模型

1)Viso二次开发管理系统的架构设计思路

为了,有效的模仿Office界面,我们使用了很多相似工具条图标,图标有一个透明的颜色,可以使得图标展示更加完美,不留底图背景的痕迹。

当然,整个布局使用了很有名的
WeifenLuo
.WinFormsUI.DockContent, 来使得界面更加完美,另外,有一点值得提及的是,为了使得工具条可以移动,分段等操作,这里还使用了框架本身提供的ToolStripPannel面板。

下面对系统的各个界面区域做一个总体的说明,首先我们看看下面的图形,我分了几个部分。

其中红色部分是Visio控件本身的内容,左边的形状(也称模具)窗口,是通过调用打开形状文件而呈现出来的。我们做Visio的二次开发,多数是和这个控件打交道了。其他部分,设计好界面后(主要涉及布局界面的设计使用了),只需要调用相关的API接口就可以实现相关的功能了。

Viso二次开发架构设计图

整个系统的架构设计如下图所示,其中绿色部分为外部控件,其他部分为自己编写的代码,边界也划分的比较清晰,界面层只是和业务逻辑层交互,不会直接操作数据库或者Visio文件,这两个部分交给下面数据访问层(DAL) 和 Visio对象访问层(VOL)进行封装调用。数据访问层对数据库的访问,是通过微软的企业库Enterprise Library库进行调用的,这样可以保证更少的代码,更高的代码质量。其中实体层和通用层是各个模块的共用的内容。

Visio的二次开发,除了需要操作Visio的文件(包含多个模具文件,Viso文档)还有就是也需要和数据库打交道。

为了较好区分和协调他们的访问,我设定了一个访问边界:访问数据库的层不会访问Visio文件对象,访问Visio文件对象的不会去访问数据库,它们统一由业务层(Business)调配,各层之间分享Entity层的信息即可。

数据库的底层访问通过利用Enterprise Library的模块完成,因此DAL层只需要做较少的工作即可完成对数据库的访问了。

Visio的二次开发图纸审批流程

图纸只有通过了编辑后,同时校对、和审核才能够发布,已发布的图方可供Web 端查看。

保存图纸的时候,如果是制图员,将在图纸“制图”栏目中填写用户名,并擦除复核、校对、签发的用户名,图纸状态恢复“编辑”状态;如果是校对或者复核人员,将在“校对”或者“复核”中填写用户名,并擦除“签发”的用户名,图纸恢复“已校对”或者“发布中”的状态;如果是签发人员,系统询问“是否发布”,选择是图纸状态变为“已发布”,否则变为“发布中”,并擦除签发的用户名。

整个发布的流程,是通过属性值进行判断,没有涉及太多的流程内容,因此对图纸的发布操作也相对比较简单。

系统模具对象关系

整个系统设计很多类型的模具对象,所有的设备都有一个字段用来表示其属于那种设备,设备基本上分下面为几类:一类是纯粹的符号,不涉及统计等信息,如河流、道路等;一类是线路设备,包括母线、电缆、架空线;一类是开关,包括负荷开关、刀闸、继保开关等;一类是变压器,包括公用变压器、专用变压器等。

模具的设计比较讲究,由于Visio在图纸分析的时候,如果是组合的模具图标,会认为是两个模具图标,对于拓扑分析非常不利,因此所有的模具均是通过ShapeSheet中的Geometry形状进行绘制,这样就保证整个模具是一个整体,模具绘制是个非常精细复杂的工作,还需要考虑文本、开关闭合等事件的界面处理效果,这个如果需要了解和掌握,需要开几节课程才能讲的清楚。
下图大致绘制出了系统中设备的层次关系图,注意仅是概念图,真实的系统中,设备直接没有继承关系。

Visio二次开发的设备状态跟踪

对于一个使用
Visio
进行二次开发的程序来说,背后你需要知道用户增加了那些设备,删除了那些设备,修改了那些设备(移动或者更改了文字、属性等操作),这样你才能对整个系统的数据进行有效的控制。
如果需要知道这些,那么你对设备的状态跟踪就显得非常重要,特别是一个删除设备的操作,设计到需要删除相关的内容的时候,这项工作就特别的重要了。

由于设备的状态更新频繁,为了系统的稳定及效率,整个系统是在图纸保存或者修改的时候,并不保存相关的设备信息;当图纸发布的时候,清空原来的数据库设备信息表,重新遍历图纸的设备信息,把它一次性写到数据库中,这样保证了发布图纸设备信息的权威性,同时提高了系统的性能。


Visio对象状态跟踪的事件侦听

在C#的Viso开发例子中,都建议采用事件侦听的方式进行处理相关的内容,这是一个很好的突破(相对VB中的开发例子而已),不过处理也有一些麻烦,主要是观念的转变以及细节的考虑吧。在这里,你可以侦听到任何你关注的事件,然后通过自定义的函数,实现自己的业务处理,这种方式实现起来确实比较简洁,思路非常清晰。

Visio对象数据库对象及关系

为了保持Visio的相关设备信息,你需要在数据库中建立相关的表,来存储设备属性信息以及图纸信息,方便信息的统计查询,图纸更可以通过Web进行查看等。

其中的Device1是一个根据SystableField表自动生成出来的设备表,它的表名会自动在SysDeviceTable中注册,方便寻找对应设备类型是哪个表。SystableField是差不多是根据Visio对象里面的属性定义的一个拷贝,里面记录了字段名称、字段类型、是否可见、排序、格式、默认值等这些Visio属性定义里面有的(你打开ShapeSheet中就看到每个属性对应一行的定义信息,就是这里面的内容的存储了)。

系统里面有一个VisioImage和VisioImageRelease的表,一个是存放增加或者修改的图纸内容(二进制存储),一个是存放发布后的图纸(二进制存储),我们系统打开图纸的侯,就是写在这个表的二进制文件,还原成Visio文件,进行打开的。

2)C#进行Visio开发的准备工作


安装Visio2007、VisioSDK2003和2007版

Visio2007是推荐的开发版本,因为目前基本上Visio2007应用比较多了,而且2003估计也买不到了,另外Visio的SDK,建议两个版本的都要安装,互补下相信息的不足。如VisioSDK2003中 有对象模型图,2007中没有。

熟悉Visio Drawing Control控件使用

这个控件的熟悉使用时非常重要的,因此在开发之初,最好能够多用这个控件做一些简单的例子,了解里面的各种属性、函数,以及事件的处理等。这样对于开发一个复杂的系统,是非常有帮助的。

Visio开发帮助文档

VisSDK.chm

Visio Code Samples Library.chm

这两个帮助文档基本上涵盖了Visio开发的方方面面,其中有很多代码可以用来做参考,里面对于一些概念或者对象的说明及分析,也是非常独到和有帮助的,虽然是英文的内容,看起来比较费劲,但是开发文档的英文都是比较简单的,应该多看看。

Visio2007文档操作

查看ShapeSheet属性及帮助内容

这个是无可替代的开发熟悉内容,里面的对于各个ShapeSheet的属性描述,更是进阶高级Visio开发的必经之路。

Visio宏的录制

要熟练掌握Office的宏录制和查看功能,对于Visio开发来说就是熟悉和了解宏代码了,里面如何操作Visio对象,是很有参考意义的。

设置Visio文档的开发人员模式

切换Visio的ShapeSheet视图

ShapeSheet视图

3)Visio的编程对象模型

Visio二次开发中,对于其对象模型的了解,就写我们吃饭用筷子一样重要,否则不熟悉筷子的使用场景和方法,是吃不到东西的。

其中主要的是下面几个对象:

Application

Window (Application.ActiveWindow)

Document (Application.ActiveDocument)

Master、Shape、Cell

一个程序中,就只有一个Application对象,类似进程的概念;打开的Visio文件有很多窗口,有一个ActiveWindow的主窗口,选区窗口、形状窗口等;模具文件打开后也是一个Document、打开的Visio文件也是一个Document、ActvieDocument是指当前Visio窗口对应的文档,每个Visio的Document有一个或者多个Page,如系统中有两个Page、一个为绘图Page,一个为背景Page。

模具文件里面有很多Master,Master类似于模板的概念,一个Master代表一个设备类型、Shape是定义一个图形的信息,模具里面的Master有且只有一个Shape,每个Shape又有很多Cell,代表一个ShapeSheet里面的一个格子,每个格子都有一个唯一的引用名称的。

Visio文档里面,和模具文件不同,里面一个大千世界,有很多Master,也很多Shape和Cell,但是由于我们看到图纸可能一个模具的设备会有拷贝多个,因此它们公用一个Master,也就是一个Master有多个Shape引用,我们分析文件,就知道里面有一个MasterID,有点像数据库里面的外键一样。

下面是VisioSDK2003中的对象模型图,概括了各个对象之间的关系。

关于拓扑关系

Visio自己的拓扑关系信息很少,除了有一个Connection知道设备的两个连接关系外,信息比较少,如果需要做设备的拓扑图形分析,需要保存他们自己的关系到数据库中,指定一个开始的设备,然后对图形进行分析,如系统中的停电分析、线损分析等,都是在数据库中进行分析,实现的效果还不错,就是会比较麻烦一些。

Visio XML格式文件分析

Master格式

Pages/Shapes格式

Visio的XML文件之Master部分

图纸的
XML
文档中,
Master
后面的
Shapes
集合中只有一个
Shape
对象

图纸的
Shapes
集合有多个对象,每个对象的
NameU

Name
值可能不一样,一般使用
NameU

Visio的XML文件之Pages、Shapes

Visio中很多属性都有一个同名+U的属性名称,一般情况下最好使用这个名称如NameU,因此这个是一个唯一的名字,有时候你会发现Name相同,但他们就是不一样,因为他们的NameU名称不一样的。

模具文件操作

本文是综合了所有讲过和未讲过的Visio开发知识,有些地方时前面介绍很少或者带过的,在此做了深层次的分析和介绍,对于Visio开发和探索,国内资料相对比较少,很多是探索性和尝试性的研究,希望本文能够为大家做一定的指引作用。

深田之星酒店管理系统2009

鼠标单击可查看大图

文件大小

5,000 KB

更新时间

2009-11-29
下载地址
文件大小:5MB

在线帮助:


产品说明如下:

★软件功能
深田之星酒店管理系统2009,是一个集客房管理、茶室管理、KTV管理三大业务管理功能于一体的酒店业务综合管理系统,系统界面优美大方,操作直观简单。软件覆盖整个酒店业务管理的方方面面,并具有丰富、强大的业务报表功能模块;软件操作具有严格的权限分配,操作数据更加放心。

软件主要功能有顾客开单、团体开单、宾客结账、增加消费、宾客预定、更改房间、修改房态、宾客管理、服务生管理、商品管理、供应商管理、库存调拨、采购管理、库存管理查询、成本分析、经营情况分析、商品设置、客房设置、参数设置、数据字典管理、其他款项登记、以及众多汇总分析报表打印等实用有效的的功能。该软件支持各种报表的查询及打印,报表功能在本软件中体现的淋漓尽致,满足客户各种报表打印的需求。
软件包含客房管理、茶室管理、KTV管理三大业务管理,每个部门能看到各自部门的消费等营运信息,又能看到总的汇总信息,茶室和KTV的商品消费可以转到宾客所在的客房账单上,每个业务模块可以通过权限分配,只管理自身的部门业务。
每个部门有非常丰富的报表,能让您及时了解各种营业情况,报表主要有:来宾信息查询、在店离店宾客消费查询、消费退单明细查询、结账单状态查询、收银明细和查询、结账明细查询、日月营业查询、营业统计排行、消费分类统计、库存查询、库存调拨查询、销售查询、成本分析、部门经营情况分析等众多完善的报表,助您更好了解总体的营业情况,更好地对整体进行有效的决策。
详细请查看在线帮助文档:
http://www.iqidi.com/HelpFile/Hotel/Default.htm

★系统需求

深田之星酒店管理系统2009 使用C#语言开发 适运行在 Microsoft WindowsNT/2000/XP/2003 等平台,但必须安装有.Net2.0平台和SqlServer数据库.
该软件利用了微软.NET Framework2.0 优秀的框架和微软SQLServer数据库高性能的数据处理能力,因此在安装软件前,您需要花费一点时间来安装下面的组件(请您按照顺序安装即可):

(1)MicroSoft .NET Framework 2.0 官方下载地址:
http://www.microsoft.com/downloads/info.aspx?na=90&p=&SrcDisplayLang=zh-cn&SrcCategoryId=&SrcFamilyId=0856eacb-4362-4b0d-8edd-aab15c5e04f5&u=http://download.microsoft.com/download/5/6/7/567758a3-759e-473e-bf8f-52154438565a/dotnetfx.exe

(2)如果您的机器上没有安装MS SQLServer数据库,您可以选择下载微软MSDE组件进行安装,该安装包是微软发布的软件,网上随处可以找到,下载后默认进行安装即可,
注意:MSDE安装后,必须重启机器,才能继续下面的安装
。下面提供一个参考下载地址:
http://download.microsoft.com/download/4/5/1/451d5d5c-69d4-40d5-b85d-f1d756cf46db/CHS_MSDE2000A.exe
如果安装MSDE出现“
为了安全起见,要求使用强SA密码。请使用SAPWD开关提供同一密码。
”的提示,请找到msde安装目录下的setup.ini,打开修改成下面这个样子
[Options]
SECURITYMODE=SQL
SAPWD=123456

其中SAPWD后的"123456"是你的sa的密码。(你也可以改成你自己的)。

(3)最后下载 深田之星酒店管理系统2009,进行安装即完成整个软件的安装。安装地址为:
http://www.iqidi.com/Download/HotelSetup.rar


“深田之星酒店管理系统2009”
在线帮助文档


具体界面截图请打开链接地址:
http://www.iqidi.com/Hotel_Picture.htm

星移斗转,时光似箭,不知不觉中,酒店管理系统的开发从开始到现在的结束,已经2个月了,2个月的业余时间,2个月的生活情趣,都寄托在这个软件当中,经历了各种艰苦和困惑,终于得以修成正果---深田之星酒店管理系统的顺利发布。

技术的历程是一个开拓进取、攻克难题的历程,其中有困惑也有兴奋,有苦涩也有甜蜜, 在这个过程中,再一次检阅了我的Database2Sharp代码自动生成的开发工具的,再一次从“深田之星送水管理系统”进行升华,技术从来没有尽头,只有不断完善,以及不断的超越和创新。在这个过程中,总会产生一系列的Q&A,碰到了一个难题,如何寻找相应的解决方法,就是非常有趣的问题了。

写这个随笔的初衷主要不是宣传我做的软件,而是有感而发,感随物现,介绍在其中历程的一些思考和解决方法,介绍做这个酒店管理系统的一些界面和非界面,代码和非代码的东西,和大家做一个交流,希望大家能我从言之无物、略表空洞的文章中捡趣拾遗,略受启发。言毕,晒上所做东西,在继续.......


整个系统的界面布局还是沿用我的“送水管理系统网络版”的界面样式,采用了OutlookBar + Wenfenluo停靠控件,客房状态视图、KTV状态视图、茶室状态视图等都是动态展示相关的房间信息的,因此需要做成控件,整个控件结合了菜单操作,以及公布一些接口给界面调用显示的,封装这块总的还是花费了不少功夫,因为很多时间花费在寻找合适的控件上,寻找是否有人家造好的轮子,以免重复制造轮子。不过再好的轮子,要想用的好,都是需要修改和调整的。由于没有找到很合适的,基本上这个界面都是自己封装控件来实现的。下面几篇文章我会详细介绍一些这方面的知识,为读者,也为自己在技术方面做一个到此一游的标记,N月之后,回头看看,希望仍觉得有用,呵呵。

下面介绍一下另外一个部分,就是下图左边部分的显示,它是一个很好的开源控件,给我进行了适当的封装,里面的显示内容,可以随意定制,因此在客服、KTV、茶室中公用一个状态显示窗口,但是显示的内容不同,界面效果还是不错的。左边的状态那块用的是一个ExploreBar的控件,另外一个比普通按钮好看的是一个不错的按钮类,功能比较强大方便,可以设置 很多种效果,包括各种图片的设置还是很方便的,我这里只是用了它的最原始效果。

下面这个是报表模块中的一部分了,整个系统很多报表,报表都脱不了打印啊、导出啊的功能了,开始想利用ActiveReport做为报表打印的,可是发现为每个不同的报表设计一个报表窗口,实在是消受不起,而且这些内容又是重复再重复的了,因此利用我原先封装好的分页GridView控件就可以了,由于很多报表不需要分页功能,因此再封装一个不用分页,但是有导出、打印功能的GridView控件就可以了。封装后的控件,既能解析类似List<EntityInfo>的格式数据源,也可以解析DataTable的数据格式,还可以对字段的显示名称随意设置,感觉省了很多麻烦。



另外一个就是小票打印了,很多基本上采用了GP5860这种POS打印机进行小票打印了,这种如果是串口的打印,那么很方便,我原来的送水系统中就实现了,而且网上也有POS打印的C#代码,可是如果我偏偏碰到了USB口的小票打印机,那么采用那个就不行了,而且那个没有预览功能,另外USB口的小票打印机和普通的打印机很容易弄错乱,不知道是否他们的打印原理差不多?因此必须解决小票打印机和普通打印机的打印问题,即多个并存,互不影响。这个问题可能是做进销存问题,如果碰到打印机冲突,需要解决的问题之一吧。





主要碰到的问题,基本上就是上面这些,其他的很多事苦力活,界面的设计需要耐心细致,功能的开发调试,更需要一份清晰的开发思路。

在开发这个系统的过程中,越来越感觉积累是很重要的东西(前面开发的软件经验和代码积累),开发的辅助工具(如我的Database2Sharp代码生成工具)也是必不可少,每次能够在已有资源上有所创新,有所超越,是一个非常有趣的心理体验。

在上篇《
WinForm界面开发之酒店管理系统--开篇
》中介绍了一些界面的东西,本篇开始抽丝剥茧,细致分析里面的控件组成,并公布相关的控件资源,以飨读者。

1、按钮控件

首先介绍一个按钮控件,这个是一个Vista样式的控件,其代码是在Codeproject上有的:
http://www.codeproject.com/KB/buttons/VistaButton.aspx

Screenshot - Screenshot.jpg

通过改变其颜色,就可以实现不同的效果,而且鼠标靠近或者离开都有特殊的效果,比较酷。例如我加上颜色图片后,得到的效果如下所示:

2、Tab控件

在使用Tab控件做那个房间状态视图的时候,由于内置的Tab控件样式感觉不是很满意,我参考过很多不一样的控件,我觉得比较好的一个是Codeproject上的一个中国人在日本发表的一篇控件文章:
http://www.codeproject.com/KB/tabs/CustomizedTabcontrol.aspx
,控件的界面大致如下。

虽然我因为样式冲突的问题,最终没有使用上她的控件,不过我觉得是很不错的,由于她的控件在Fill状态下有点问题,特意请教了她,并得到了她的最新控件代码,我上传到博客上,给大家下载参考吧:
https://files.cnblogs.com/wuhuacong/CustomTabcontrol.rar

我做的控件大致的思路是先设计一个窗口框架,里面的Tabpage可是通过代码增加的,由于客房的房间类型是动态变化,而不是固定的,如下图所示。我们每次New出一个TabPage的时候,把有图标的用户控件加载(下一个图)进去就可以了。

下面这个是Winform的用户控件,它的职责就是获取数据库的房间信息,根据不同的状态显示不同的图标,然后动态创建,每种房间类型有多少个房间,就动态创建多少个。如下图所示。

另外我们还需要它绑定相关的业务菜单,根据不同的状态,禁用或者显示特定的菜单,如下图所示。

这样我们在最终的界面上就少管很多事情,这样层层下去,各管各的事情,互不干扰。

这个控件会公布一些事件,让外部进行相关的操作,如下代码所示:



代码


public

delegate

void
ShowStatusHandler(RoomInfo roomInfo);

public
ShowStatusHandler OnShowStatus
=

null
;

另外,它也会公布一些接口,给Ower对他进行相关的管理,主要是改变视图类型(大图标、小图标、列表显示),改变房间状态(空闲、占用、预定等),以及强制刷新操作。如下代码所示。



代码


///

<summary>


///
修改ListView的视图

///

</summary>


///

<param name="viewType"></param>



public

void
ChangeViewType(View viewType)
{

this
.listView1.View
=
viewType;
BindData();

this
.Refresh();
}


///

<summary>


///
修改房间的状态显示

///

</summary>


///

<param name="roomStatus"></param>



public

void
ChangeRoomStatus(
string
roomStatus)
{

this
.RoomStatus
=
roomStatus;
BindData();

this
.Refresh();
}


public

void
UpdateStatus()
{
BindData();

this
.Refresh();
}

搞定了小的,现在开始搞大的了,就是该用户控件的Owner窗体,它负责很多个这样的用户控件的创建、更新等操作。下面看看代码先。



代码


///

<summary>


///
更新所有房间的状态显示

///

</summary>



public

void
UpdateStatus()
{

foreach
(TabPage page
in

this
.tabControl1.TabPages)
{

foreach
(Control control
in
page.Controls)
{
RoomViewControl lvwControl

=
control
as
RoomViewControl;

if
(lvwControl
!=

null
)
{
lvwControl.UpdateStatus();
}
}
page.Refresh();
}
}


public

void
ChangeViewType(View viewType)
{

foreach
(TabPage page
in

this
.tabControl1.TabPages)
{

foreach
(Control control
in
page.Controls)
{
RoomViewControl lvwControl

=
control
as
RoomViewControl;

if
(lvwControl
!=

null
)
{
lvwControl.ChangeViewType(viewType);
}
}
page.Refresh();
}
}


public

void
ChangeRoomStatus(
string
roomStatus)
{

foreach
(TabPage page
in

this
.tabControl1.TabPages)
{

foreach
(Control control
in
page.Controls)
{
RoomViewControl lvwControl

=
control
as
RoomViewControl;

if
(lvwControl
!=

null
)
{
lvwControl.ChangeRoomStatus(roomStatus);
}
}
page.Refresh();
}
}

上面的代码,其实就是遍历其TabPage中的控件,并判断是否特定的控件,然后进行相关的操作,就是调用每一个控件公布的接口。

由于控件的变化,需要通知状态视图,进行相应的显示,如下图所示。

要实现动态的状态变化,那么就需要注册状态变化的事件了,我们在构建该用户控件的时候,注册它的变化事件相应即可。如下代码所示



代码


private

void
Form1_Load(
object
sender, EventArgs e)
{

#region
根据不同的房间类型创建不同的Tab和房间视图


this
.tabControl1.TabPages.Clear();
List

<
RoomTypeInfo
>
roomTypeList
=
BLLFactory
<
RoomType
>
.Instance.GetAll();

foreach
(RoomTypeInfo info
in
roomTypeList)
{
TabPage page

=

new
TabPage();
page.Text

=
info.Name;
page.Tag

=
info;

RoomViewControl viewControl

=

new
RoomViewControl();
viewControl.RoomType

=
info.Name;
viewControl.Dock

=
DockStyle.Fill;
viewControl.OnShowStatus

=

new
RoomViewControl.ShowStatusHandler(OnShowStatus);

page.Controls.Clear();
page.Controls.Add(viewControl);

this
.tabControl1.TabPages.Add(page);
}

#endregion

}

下面的代码是就是事件响应代码,它的功能就是完成状态的更新显示,以及房价费用的显示。如下图所示。



代码


private

void
OnShowStatus(RoomInfo roomInfo)
{

decimal
allMoney
=

0.0M
;


#region
更新消费记录


if
(roomInfo
!=

null
)
{
List

<
ConsumerListInfo
>
consumerList
=
BLLFactory
<
Room
>
.Instance.GetAllConsumption(roomInfo.RoomNo);


this
.listView1.Items.Clear();

int
i
=

1
;

foreach
(ConsumerListInfo info
in
consumerList)
{
ListViewItem item

=

new
ListViewItem(i.ToString());
item.SubItems.Add(info.RoomNo);
item.SubItems.Add(info.ItemName);
item.SubItems.Add(info.Price.ToString(

"
C2
"
));
item.SubItems.Add(info.Discount.ToString());
item.SubItems.Add(info.DiscountPrice.ToString(

"
C2
"
));
item.SubItems.Add(info.Quantity.ToString());
item.SubItems.Add(info.Amount.ToString(

"
C2
"
));
item.SubItems.Add(info.BeginTime.ToString());
item.SubItems.Add(info.Waiter);
item.SubItems.Add(info.Creator);


if
(info.Quantity
<

0
)
{
item.ForeColor

=
Color.Red;
}


this
.listView1.Items.Add(item);
allMoney

+=
info.Amount;
i

++
;
}
}

#endregion



#region
更新房间信息

FrmStatus dlg

=
Portal.gc.MainDialog.mainStatus;

if
(dlg
!=

null
)
{

if
(roomInfo
!=

null
)
{
InitDisplayItems(dlg.DisplayItems, roomInfo, allMoney);
dlg.UpdateContent();
}

else

{
dlg.InitDisplayItems();
dlg.UpdateContent();
}
}

//
Portal.gc.MainDialog.ShowMainStatusWin();




#endregion



this
.lblAmount.Text
=

string
.Format(
"
消费总金额:{0:C2}
"
, allMoney);
}

好了,描述与代码齐上,虽不齐整,但希望抛砖引玉能,给各位读者的思绪及灵感有一个引桥般的铺垫,完毕收工。

Socket开发是属于通信底层的开发,.NET也提供了非常丰富的类来实现Socket的开发工作,本篇不是介绍这些基础类的操作,而是从一个大的架构方面阐述Socket的快速开发工作,本篇以TCP模式进行程序的开发介绍,以期达到抛砖引玉的目的。

要掌握或者了解Socket开发,必须了解下面所述的场景及知识。

1、TCP客户端,连接服务器端,进行数据通信

2、TCP服务器端,负责侦听客户端连接

3、连接客户端的管理,如登陆,注销等,使用独立线程处理

4、数据接收管理,负责数据的接受,并处理队列的分发,使用独立线程处理,简单处理后叫给“数据处理线程”

5、数据处理线程,对特定的数据,采用独立的线程进行数据处理

6、数据的封包和解包,按照一定的协议进行数据的封装和解包

针对以上内容,可以封装以下功能的操作类作为共用基类:

1、BaseSocketClient,客户端基类

2、BaseSocketServer,TCP服务器管理基类

3、BaseClientManager,连接客户端管理类

4、BaseReceiver,数据接收处理类

5、ThreadHandler,数据独立线程处理类

6、PreData、DataTypeKey、Sign分别是定义数据的基础格式、协议标识、分隔符号等,另外我们定义需要发送的实体类信息,发送和接收通过实体类进行数据转换和解析。

以上类是基类,不能直接使用,在服务器端和客户端都要继承相应的类来完成所需要的工作。

BaseSocketClient只要负责客户端的链接、断开、发送、接收等操作,大致的定义如下:



代码


public

class
BaseSocketClient
{

public
BaseSocketClient()
{
_Name

=

this
.GetType().Name;
}


public
BaseSocketClient(Socket socket) :
this
()
{
_socket

=
socket;
IPEndPoint ipAndPort

=
(IPEndPoint)socket.RemoteEndPoint;
_IP

=
ipAndPort.Address.ToString();
_port

=
ipAndPort.Port;
}


///

<summary>


///
断开连接

///

</summary>



public

virtual

void
DisConnect()
{
.........
}


///

<summary>


///
主动连接

///

</summary>



public

virtual

void
Connect(
string
ip,
int
port)
{
........
}


///

<summary>


///
开始异步接收

///

</summary>



public

void
BeginReceive()
{
.........
}


///

<summary>


///
开始同步接收

///

</summary>




public

void
StartReceive()
{
.........
}


///

<summary>


///
异步发送

///

</summary>



public

void
BeginSend(SendStateObject sendState)
{
........
}


///

<summary>


///
同步发送。直接返回成功失败状态

///

</summary>



public

bool
SendTo(
string
data)
{
.........
}

///

<summary>


///
主动检查连接

///

</summary>



public

virtual

void
CheckConnect()
{
.............
}


protected

virtual

void
OnRead(PreData data)
{
}
}

2、BaseSocketServer,TCP服务器管理基类

该类负责在独立的线程中侦听指定的端口,如果有客户端连接进来,则进行相应的处理,重载处理函数可以实现独立的处理。大致的定义如下。



代码


public

class
BaseSocketServer
{

public
BaseSocketServer()
{

this
._SocketName
=

this
.GetType().Name;
}


///

<summary>


///
启动监听线程

///

</summary>



public

void
StartListen(
string
ip,
int
port)
{
_IP

=
ip;
_port

=
port;

if
(_listenThread
==

null
)
{
_listenThread

=

new
Thread(Listen);
_listenThread.IsBackground

=

true
;
_listenThread.Start();
}
}


///

<summary>


///
检查监听线程

///

</summary>



public

void
CheckListen()
{

if
(_listenThread
==

null

||
(
!
_listenThread.IsAlive))
{
_listenThread

=

new
Thread(Listen);
_listenThread.IsBackground

=

true
;
_listenThread.Start();
}
}


///

<summary>


///
监听线程

///

</summary>



protected

virtual

void
Listen()
{
IPEndPoint ipAndPort

=

new
IPEndPoint(IPAddress.Parse(IP), Port);
TcpListener tcpListener

=

new
TcpListener(ipAndPort);
tcpListener.Start(

50
);
//
配置




while
(
true
)
{
Socket socket

=
tcpListener.AcceptSocket();
AcceptClient(socket);
}
}


///

<summary>


///
接收一个Client

///

</summary>



protected

virtual

void
AcceptClient(Socket socket)
{
}

3、BaseClientManager,连接客户端管理类

由于考虑性能的影响,客户端对象的管理交给一个独立的线程进行处理,一则处理思路清晰,二则充分利用线程的性能。该类主要负责客户端登录超时处理,连接上来的客户端维护,经过登陆验证的客户端维护,客户端登陆验证接口,客户端发送数据处理等功能。



代码


public

class
BaseClientManager
<
T
>

where
T : BaseSocketClient
{

#region
登陆管理



protected

string
_Name
=

"
BaseClientManager
"
;

private

int
_SessionId
=

0
;

private

object
_LockSession
=

new

object
();


private
System.Threading.Timer _CheckInvalidClientTimer
=

null
;
//
检查客户端连接timer



private
System.Threading.Timer _SendTimer
=

null
;
//
发送数据调用timer




///

<summary>


///
已经注册的客户端 关键字userid

///

</summary>



protected
SortedList
<
string
, T
>
_loginClientList
=

new
SortedList
<
string
, T
>
();

///

<summary>


///
连上来的客户端 未注册 关键字Session

///

</summary>



protected
SortedList
<
string
, T
>
_tempClientList
=

new
SortedList
<
string
, T
>
();


///

<summary>


///
构造函数

///

</summary>



public
BaseClientManager()
{

this
._Name
=

this
.GetType().Name;
}


///

<summary>


///
已经注册的客户端 关键字userid

///

</summary>



public
SortedList
<
string
, T
>
LoginClientList
{

get
{
return
_loginClientList; }

set
{ _loginClientList
=
value; }
}


///

<summary>


///
增加一个连上来(未注册)的客户端

///

</summary>


///

<param name="client"></param>



public

void
AddClient(T client)
{
......
}


///

<summary>


///
增加一个已登录的客户端

///

</summary>



public

void
AddLoginClient(T client)
{
......
}


///

<summary>


///
当客户端登陆,加入列表后的操作

///

</summary>


///

<param name="client"></param>



protected

virtual

void
OnAfterClientSignIn(T client)
{
}


///

<summary>


///
验证登录

///

</summary>



public

virtual

bool
CheckClientLogin(
string
userId,
string
psw,
ref

string
memo)
{

return

false
;
}


///

<summary>


///
电召客户端登出

///

</summary>


///

<param name="userId"></param>



public

void
ClientLogout(
string
userId)
{

if
(_loginClientList.ContainsKey(userId))
{
RadioCallClientLogout(_loginClientList[userId]);
}
}


///

<summary>


///
电召客户端登出

///

</summary>


///

<param name="client"></param>



private

void
RadioCallClientLogout(T client)
{
client.DisConnect();
}


///

<summary>


///
移除注册的客户端

///

</summary>


///

<param name="client"></param>



private

void
RemoveLoginClient(T client)
{
......
}


///

<summary>


///
移除客户端后的操作

///

</summary>


///

<param name="client"></param>



protected

virtual

void
OnAfterClientLogout(T client)
{
}


///

<summary>


///
在连接的列表中移除客户端对象

///

</summary>


///

<param name="client"></param>



public

virtual

void
RemoveClient(T client)
{
RemoveLoginClient(client);
RemoveTempClient(client);
}


#endregion



///

<summary>


///
开始客户端连接处理

///

</summary>



public

void
Start()
{
StartSendTimer();
StartCheckInvalidClientTimer();
}


///

<summary>


///
启动客户端发送数据线程

///

</summary>



public

void
StartSendTimer()
{
......
}


///

<summary>


///
启动检查客户端连接timer

///

</summary>



public

void
StartCheckInvalidClientTimer()
{
......
}


///

<summary>


///
检查客户端连接

///

</summary>


///

<param name="stateInfo"></param>



private

void
CheckInvalidClient(Object stateInfo)
{
......
}


public

virtual

void
RemoveInvalidClient()
{
......
}


///

<summary>


///
增加一条客户端发送数据

///

</summary>



public

void
AddSend(
string
userid,
string
send,
bool
isFirst)
{
......
}
}

4、BaseReceiver,数据接收处理类

该基类是所有接受数据的处理类,负责维护数据的队列关系,并进一步进行处理。



代码


public

class
BaseReceiver
{

protected

string
_Name
=

"
BaseReceiver
"
;

protected
Thread _PreDataHandlehread
=

null
;
//
处理数据线程



protected
Fifo
<
PreData
>
_preDataFifo
=

new
Fifo
<
PreData
>
(
50000
);


public
BaseReceiver()
{
_Name

=

this
.GetType().Name;
}


///

<summary>


///
接收处理数据

///

</summary>



public

void
AppendPreData(PreData data)
{
_preDataFifo.Append(data);
}


///

<summary>


///
数据处理

///

</summary>



protected

virtual

void
PreDataHandle()
{
......
}


///

<summary>


///
数据处理

///

</summary>


///

<param name="data"></param>



public

virtual

void
PreDataHandle(PreData data)
{
}


///

<summary>


///
开始数据处理线程

///

</summary>



public

virtual

void
Start()
{

if
(_PreDataHandlehread
==

null
)
{
_PreDataHandlehread

=

new
Thread(
new
ThreadStart(PreDataHandle));
_PreDataHandlehread.IsBackground

=

true
;
_PreDataHandlehread.Start();
}
}
}

5、ThreadHandler,数据独立线程处理类

对每个不同类型的数据(不同的协议类型),可以用独立的线程进行处理,这里封装了一个基类,用于进行数据独立线程的处理。



代码


public

class
ThreadHandler
<
T
>

{
Thread _Handlehread

=

null
;
//
处理数据线程



private

string
_ThreadName
=

""
;

private
Fifo
<
T
>
_DataFifo
=

new
Fifo
<
T
>
();


///

<summary>


///
接收处理数据

///

</summary>



public

virtual

void
AppendData(T data)
{

if
(data
!=

null
)
_DataFifo.Append(data);
}


///

<summary>


///
数据处理

///

</summary>



protected

virtual

void
DataThreadHandle()
{

while
(
true
)
{
T data

=
_DataFifo.Pop();
DataHandle(data);
}
}


///

<summary>


///
数据处理

///

</summary>


///

<param name="data"></param>



public

virtual

void
DataHandle(T data)
{
}


///

<summary>


///
检查数据处理线程

///

</summary>



public

virtual

void
Check()
{
......
}


///

<summary>


///
开始数据处理线程

///

</summary>



public

virtual

void
StartHandleThread()
{
......
}
}

6、PreData、DataTypeKey、Sign

PreData是定义了一个标准的协议数据格式,包含了协议关键字、协议内容、用户标识的内容,代码如下。



代码


///

<summary>


///
预处理的数据

///

</summary>



public

class
PreData
{

private

string
_key;

private

string
_content;

private

string
_userId;


public
PreData()
{
}


public
PreData(
string
key,
string
data)
{
_key

=
key;
_content

=
data;
}


///

<summary>


///
协议关键字

///

</summary>



public

string
Key
{

get
{
return
_key; }

set
{ _key
=
value; }
}


///

<summary>


///
数据内容

///

</summary>



public

string
Content
{

get
{
return
_content; }

set
{ _content
=
value; }
}


///

<summary>


///
客户端过来为用户帐号,或者指定的名称

///

</summary>



public

string
UserId
{

get
{
return
_userId; }

set
{ _userId
=
value; }
}
}

其中的DataTypeKey和Sign定义了一系列的协议头关键字和数据分隔符等信息。



代码


public

class
DataTypeKey
{

///

<summary>


///
认证请求 AUTHR C->S

///

</summary>



public

const

string
AuthenticationRequest
=

"
AUTHR
"
;

///

<summary>


///
认证请求应答AUTHA S->C

///

</summary>



public

const

string
AuthenticationAnswer
=

"
AUTHA
"
;


///

<summary>


///
测试数据TESTR C->S

///

</summary>



public

const

string
TestDataRequest
=

"
TESTR
"
;

///

<summary>


///
测试数据TESTA S->C

///

</summary>



public

const

string
TestDataAnswer
=

"
TESTA
"
;

.........

}

下面是数据分割符号,定义了数据包的开始符号、结束符号,分隔符号和数据分隔符等。



代码


public

class
Sign
{

///

<summary>


///
开始符

///

</summary>



public

const

string
Start
=

"
~
"
;

///

<summary>


///
开始符比特

///

</summary>



public

const

byte
StartByte
=

0x7E
;

///

<summary>


///
结束符

///

</summary>



public

const

string
End
=

"
#
"
;

///

<summary>


///
结束符比特

///

</summary>



public

const

byte
EndByte
=

0x23
;

///

<summary>


///
分隔符

///

</summary>



public

const

string
Separator
=

"
&
"
;

///

<summary>


///
分隔符比特

///

</summary>



public

const

byte
SeparatorByte
=

0x26
;

///

<summary>


///
数据分隔符

///

</summary>



public

const

string
DataSeparator
=

"
|
"
;

///

<summary>


///
数据分隔符比特

///

</summary>



public

const

byte
DataSeparatorByte
=

0x7C
;
}

另外,前面说了,我们数据是通过实体类作为载体的,我们知道,收到的Socket数据经过粗略的解析后,就是PreData类型的数据,这个是通用的数据格式,我们需要进一步处理才能转化为所能认识的数据对象(实体类对象),同样,我们发送数据的时候,内容部分肯定是按照一定协议规则串联起来的数据,那么我们就需要把实体转化为发送的数据格式。综上所述,我们通过实体类,必须实现数据的发送和读取的转换。



代码


///

<summary>


///
测试数据的实体类信息

///

</summary>



public

class
TestDataRequest
{

#region
MyRegion



///

<summary>


///
请求序列

///

</summary>



public

string
seq;

///

<summary>


///
用户帐号

///

</summary>



public

string
userid;

///

<summary>


///
用户密码

///

</summary>



public

string
psw;


#endregion



public
TestDataRequest(
string
seq,
string
userid,
string
psw)
{

this
.seq
=
seq;

this
.userid
=
userid;

this
.psw
=
psw;
}

public
TestDataRequest()
{
}


///

<summary>


///
转换Socket接收到的信息为对象信息

///

</summary>


///

<param name="data">
Socket接收到的信息
</param>



public
TestDataRequest(
string
data)
{

string
[] dataArray
=

null
;
dataArray

=
NetStringUtil.UnPack(data);

if
(dataArray
!=

null

&&
dataArray.Length
>

0
)
{
TestDataRequest newAnswerData

=

new
TestDataRequest();

int
i
=

0
;

this
.seq
=
dataArray[i
++
];

this
.userid
=
dataArray[i
++
];

this
.psw
=
dataArray[i
++
];
}
}


///

<summary>


///
转换对象为Socket发送格式的字符串

///

</summary>


///

<returns></returns>



public

override

string
ToString()
{

string
data
=

""
;
data

=

this
.seq
+

"
|
"

+

this
.userid
+

"
|
"

+

this
.psw.ToString();
data

=
NetStringUtil.PackSend(DataTypeKey.TestDataRequest, data);

return
data;
}

在接下来的工作中,就需要继承以上的基类,完成相关的对象和数据的处理了。

本人是实际中,编写了一个测试的例子,大致的基类使用情况如下所示。