2023年2月

好久没写博客了,除了是工作较忙的原因外,其实是也一直在想如何整合我所有的开发经验及技术积累,开发过很多Winform共享软件、Asp.net的WebForm项目,发现很多东西是相互关联很紧密的,但往往我们太忙太懒,要好好整理,并整理出棒棒的一般比较难,但我们没有停步,梦想总会慢慢接近并实现。在做了很多项目之后,发现人的惰性或者惯性很大,因此有机会得好好整理下开发的成功,优化再优化,用的时候就越来越顺手了。

在所有开发过的项目过程,很多如权限管理、字典数据管理模块,都是非常常用的模块,本文主要想介绍下提炼出来,各个项目均可通用的字典数据管理系统(或者叫做模块更为适合),在介绍之前,我想介绍下我的整合路线及一些想法,如下所示:

其中框架中所有介绍的内容均为现有开发框架中有的东西及特性,如果要了解Winform框架的多维特点,可以现在最新的共享软件《仓库管理系统》,具体可以参考文章《
从开发的软件《备件仓库管理系统》总结的一些经验
》进行了解,该共享软件除了整合众多优秀的功能外,一个特点就是数据管理模块也得到了升华。

在Winform框架中,其中权限管理系统、字典管理系统,都是可以做成独立的程序来使用,而且应该可以在程序中引用来查询或者获取相关的字典数据,如找某个键值的字典列表作为下拉列表,而且由于实际项目总,有点是SqlServer、有的是Access数据库的,所以支持多数据库是最好的选择。

在字典数据数据管理工程项目中,我们看到有两个不同的数据访问层,工厂模式通过不同的配置,调用不同的数据访问层,从而实现SqlServer、Access等数据库的支持,当然可以扩展更多的数据库支持,我们先来看看工程项目的视图如下所示:

配置文件如下所示

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="dataConfiguration"
type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
</configSections>
<connectionStrings>
<add name="DataAccess" providerName="System.Data.OleDb" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=E:\我的应用程序\数据字典\SqlDictionary\WHC.Dictionary.UI\bin\Debug\OrderWater.mdb;User ID=Admin;Jet OLEDB:Database Password=;" />
<add name="DataAccess2" providerName="System.Data.SqlClient"
connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=Warehouse;User ID=sa;Password=123456"/>
</connectionStrings>
<dataConfiguration defaultDatabase="DataAccess"/>

<appSettings>
<!--软件名称-->
<add key="ApplicationName" value="深田之星仓库管理系统"/>
<!--开发商名称-->
<add key="Manufacturer" value="广州爱启迪技术有限公司"/>

<!--数据字典的数据库类型:access、sqlserver等-->
<add key="ComponentDbType" value="access"/>
</appSettings>

</configuration>

我们通过DictionaryDbType来切换不同的数据库,不用修改代码实现多数据库支持,当然,不同的数据库,需要创建不同的数据库文件,不过数据库结构基本上是一致的。

我们看看该字典管理模块的最终效果,如下所示:

字典数据模块做成独立的程序后,一个可以独立运行,也可以在宿主程序中通过DLL方式调用类库来获取字典数据,如下所示:


private

void
InitDictItem()
{

this
.txtManufacture.Items.Clear();

this
.txtManufacture.Items.AddRange(DictItemUtil.GetDictByDictType(
"
供货商
"
));


this
.txtBigType.Items.Clear();

this
.txtBigType.Items.AddRange(DictItemUtil.GetDictByDictType(
"
备件属类
"
));


this
.txtItemType.Items.Clear();

this
.txtItemType.Items.AddRange(DictItemUtil.GetDictByDictType(
"
备件类别
"
));


this
.txtSource.Items.Clear();

this
.txtSource.Items.AddRange(DictItemUtil.GetDictByDictType(
"
来源
"
));



this
.txtWareHouse.Items.Clear();

this
.txtWareHouse.Items.AddRange(DictItemUtil.GetAllWareHouse().ToArray());


this
.txtDept.Items.Clear();

this
.txtDept.Items.AddRange(DictItemUtil.GetDictByDictType(
"
部门
"
));
}


字典组件模块调用例子Demo程序下载地址也一并提供下载,下载地址如下:

https://files.cnblogs.com/wuhuacong/DictionaryDemo.rar

在前一篇随笔《
Winform开发框架之字典数据管理
》中介绍过我Winform开发框架的总体情况,其中最重要的思路就是把常用的模块封装成控件进行重复使用,一则可以避免重复开发,提高开发效率;二则可以使用成熟的控件及技术,使得用户体验更好,更加专业。在前面介绍过的Winfrom开发框架总体思路如下所示。

在Winform框架中,其中权限管理系统、字典管理系统,都是可以做成独立的程序来使用,而且应该可以在程序中引用来查询或者获取相关的字典数据,如找某个键值的字典列表作为下拉列表,而且由于实际项目中,有的是SqlServer、有的是Access数据库的或者其他数据库,所以支持多数据库是最好的选择。

本篇主要介绍经过优化完善的权限管理系统, 其既独立又具整合性的权限管理系统,既相互独立,有相互整合,方便重用,又不需重新开发,非常方便、更提高效率。 由于权限系统精简而又能满足日常绝大多数的需要,不会复杂的难于管理,而且也是基于角色的授权访问机制(RBAC),最重要是非常适合软件的整合使用。

权限管理系统作为一个独立的模块,其主要由登陆界面、权限管理主界面(管理用户、角色、机构、功能,以及控制角色的权限等操作功能,部分界面如下所示。


给用户分配权限,首先是给相应角色授权,角色授权功能如下所示,可以编辑角色使用的功能,也可以管理该角色包含的用户,从而实现间接实现给用户分配权限的功能(这也是基于业界通用的基于角色的访问控制(RBAC)的控制法则)。

经过优化后的权限管理系统,界面及业务逻辑封装到一个类库中,我们开发的业务管理系统中集成就很简单了,主要的项目工程界面如下所示:

我编写了一个测试工程,用来模拟业务系统中调用权限管理模块的例子,界面如下所示:

相关的代码很简单,这也是利用权限管理系统后简化很多代码的根本所在:


private

void
Form1_Load(
object
sender, EventArgs e)

{

//
获取所有权限管理系统的用户,并在下拉列表中展示


List
<
UserInfo
>
userList
=
BLLFactory
<
User
>
.Instance.GetAll();

this
.txtLogin.Items.Clear();

foreach
(UserInfo info
in
userList)
{

this
.txtLogin.Items.Add(info.Name);
}
}


private

void
btnSecurity_Click(
object
sender, EventArgs e)
{

//
独立启动权限管理系统,只需一行代码即可


WHC.Security.UI.Portal.StartLogin();
}

当然,如果要判断用户的登录及角色等,可以通过下面代码来实现。


//
判断用户是否登录成功



string
identity
=
BLLFactory
<
User
>
.Instance.VerifyUser(
this
.txtLogin.Text,
this
.txtPassword.Text, Guid.NewGuid().ToString());

if
(
!
string
.IsNullOrEmpty(identity))
{

//
进一步判断用户角色



if
(BLLFactory
<
User
>
.Instance.UserInRole(
this
.txtLogin.Text,
"
管理员
"
))
{
MessageUtil.ShowTips(

string
.Format(
"
用户【{0}】身份验证正确
"
,
this
.txtLogin.Text));
}

else

{
MessageUtil.ShowWarning(

"
该用户没有管理员权限
"
);

return
;
}
}

else

{
MessageUtil.ShowWarning(

"
用户名或密码错误
"
);

return
;
}

权限控制的精髓就是,用户登录后,通过把用户拥有的权限获取出来,放到一个功能列表中,然后在每一个窗体中,根据用户的功能列表,显示或者屏蔽对应的功能即可,获取功能列表代码如下所示:

UserInfo info
=
userBLL.GetUserByName(loginName);


#region
获取用户的功能列表


Function functionBLL

=

new
Function();
List

<
FunctionInfo
>
list
=
functionBLL.GetFunctionsByUser(info.ID,
"
WareMis
"
);

if
(list
!=

null

&&
list.Count
>

0
)
{

foreach
(FunctionInfo functionInfo
in
list)
{

if
(
!
Portal.gc.FunctionDict.ContainsKey(functionInfo.ControlID))
{
Portal.gc.FunctionDict.Add(functionInfo.ControlID, functionInfo);
}
}
}

#endregion

判断的时候,放在一个函数,判断用户访问的功能是否在列表中即可,代码如下所示


///

<summary>


///
看用户是否具有某个功能

///

</summary>


///

<param name="controlID"></param>


///

<returns></returns>



public

bool
HasFunction(
string
controlID)
{

bool
result
=

false
;

if
(FunctionDict.ContainsKey(controlID))
{
result

=

true
;
}


return
result;
}

那么控制用户显示的界面代码如下所示:


#region
KTV包间管理


if
(Portal.gc.HasFunction(
"
KTV
"
))
{
OutlookBarBand myBasicBand

=

new
OutlookBarBand(
"
KTV包间管理
"
);
myBasicBand.SmallImageList

=

this
.imageList;
myBasicBand.LargeImageList

=

this
.imageList;

if
(Portal.gc.HasFunction(
"
KTV/View
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV包间状态视图
"
,
0
));
}

if
(Portal.gc.HasFunction(
"
KTV/Setting
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV包间设置
"
,
1
));
}

if
(Portal.gc.HasFunction(
"
KTV/Goods
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV商品设置
"
,
3
));
}

if
(Portal.gc.HasFunction(
"
KTV/Waiter
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV服务生管理
"
,
4
));
}

if
(Portal.gc.HasFunction(
"
KTV/BookIn
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV预订管理
"
,
5
));
}

if
(Portal.gc.HasFunction(
"
KTV/OtherIncome
"
))
{
myBasicBand.Items.Add(

new
OutlookBarItem(
"
KTV其他款项登记
"
,
6
));
}
myBasicBand.Background

=
SystemColors.AppWorkspace;
myBasicBand.TextColor

=
Color.White;
outlookBar1.Bands.Add(myBasicBand);
}

#endregion


判断菜单或者功能按钮,也只需要判断某功能点是否在已有集合中即可,如下所示:



///
<summary>


///
根据权限屏蔽功能

///
</summary>



private
void
InitAuthorizedUI()
{

this
.tool_Report.Enabled
=
Portal.gc.HasFunction(
"
Report
"
);

this
.tool_Dict.Enabled
=
Portal.gc.HasFunction(
"
Dictionary
"
);

this
.tool_ItemDetail.Enabled
=
Portal.gc.HasFunction(
"
ItemDetail
"
);

this
.tool_Purchase.Enabled
=
Portal.gc.HasFunction(
"
Purchase
"
);

this
.tool_StockSearch.Enabled
=
Portal.gc.HasFunction(
"
StockSearch
"
);

this
.tool_TakeOut.Enabled
=
Portal.gc.HasFunction(
"
TakeOut
"
);


this
.menu_WareHouse.Enabled
=
Portal.gc.HasFunction(
"
WareHouse
"
);

this
.menu_Dictionary.Enabled
=
Portal.gc.HasFunction(
"
Dictionary
"
);

this
.menu_run_systemLog.Enabled
=
Portal.gc.HasFunction(
"
LoginLog
"
);

this
.menu_Parameters.Enabled
=
Portal.gc.HasFunction(
"
Parameters
"
);

this
.menu_MonthlyStatistic.Enabled
=
Portal.gc.HasFunction(
"
MonthlyStatistic
"
);

this
.menu_AnnualStatistic.Enabled
=
Portal.gc.HasFunction(
"
AnnualStatistic
"
);

this
.menu_ClearAll.Enabled
=
Portal.gc.HasFunction(
"
ClearAllData
"
);

this
.menu_ImportItemDetail.Enabled
=
Portal.gc.HasFunction(
"
ImportItemDetail
"
);
}


至此,权限管理模块介绍已经完毕,下面给出一个调用例子Demo程序给大家参考学习,下载地址如下:

https://files.cnblogs.com/wuhuacong/SecurityDemo.rar

前一篇文件的字典组件模块调用例子Demo程序下载地址也一并提供下载,下载地址如下:

https://files.cnblogs.com/wuhuacong/DictionaryDemo.rar

在前面两篇随笔,都围绕着我的Winform开发框架介绍一些重要的组件,如字典模块封装《
Winform开发框架之字典数据管理
》和权限管理系统模块封装《
Winform开发框架之权限管理系统
》。本框架主要的功能如下面的图示所介绍的内容:包括紧密结合Database2Sharp强大的一键解决方案代码生成工具、高度抽象继承及使用泛型支持多数据库的开发框架、集成权限管理系统模块、集成字典数据管理模块、集成强大的分页控件、集成OutLookBar界面控件、集成多文档界面Weifengluo布局控件、集成美观实用的登陆界面、闪屏展示界面、托盘缩小提示功能、全局运行一次实例限制模块代码、集成多年积累公共组件简化常用功能操作、集成Apose.Cell基于模板自定义报表快速生成复杂报表模块、众多美观使用的界面控件整合等等。

该框架是集多年Winform开发经验及总结之大成,及众多博客随笔精华于一身,不断完善,不断改进,当然,框架不能形而上学而存在,因此通过本人最近一次完善的仓库项目并经过多次锤炼的成果进行展示。

本框架目标是确保开发者能够拿过来就用,拿过来只需要添加自己额外的业务界面功能即可,不需要重复开发基础功能模块和相关界面处理,尽可能的节省开发时间,并迅速给阁下的客户展示美观、完善、强大的界面及功能,快速完成一个复杂的项目为目标。





软件框架界面采用了众多功能的集成,因此对于有意学习Winform开发的人来说,虽然不是一本Winform开发百科全书,但也是一部不可多得的代码宝库,在众多地方都可以找到相应的解决方法,对于想快速、高效开发规范、稳定的项目的人来说,在这个不错的框架上搭建业务应用,更是事半功倍。

该开发框架项目演示程序可以随意下载研究,但源码有偿提供不免费赠送,需要者可以和我联系,或者可以一起沟通讨论相关的内容。

演示程序下载地址如下:
https://files.cnblogs.com/wuhuacong/DeviceMis.rar

虽然目前.NET的主流的开发基本上是基于Web方式(传统的Web方式和Silvelight方式)、基于Winform方式(传统的Winform模式和WPF方式等)、基于服务应用方式(传统的WebService和WCF服务方式)等主要几种开发,另外,还有一种就是基于Socket协议的开发方式,不同于高级服务层的WebService和WCF服务,基于Socket协议开发是较为底层的开发方式,它往往具有更加灵活,可控性更高的优点,不过相对来说,开发难度也会大一些。

我由于工作需要,需要开发一个数据采集客户端,监控指定目录的文件变化,登陆服务器端并传送数据内容,本人将其重构优化,逐步提炼把它升级为一种Socket框架的方式来对待,其中涉及一些有意思的技术应用,在此分享讨论下。首先我们看来看数据采集客户端的主界面。

一般的Socket客户端需要涉及到服务启动、服务停止、状态自动检查、用户登录、用户注销、日志记录、协议数据组装拆包、协议数据收发、多线程数据处理、数据展示通知、参数配置等等功能。本文命名为Socket开发框架之数据采集客户端,就是想站在一个较为通用、完好封装的角度上介绍这些功能点的实现。

1、 服务启动及服务停止

根据不同的业务需要,我创建几个业务管理类,分别是CommonManager、DispatchManager、FileWatcherManger、LoginManager、ReqAnsManager。

其中CommonManager是其他业务管理类的总管,并且包含和Socket服务器连接的控制管理, DispatchManager则是负责解析收到的数据并对不同数据进行分发处理,FileWatcherManger是实现对指定目录的文件进行监控并对其数据进行管理,LoginManager主要是实现登录控制管理、ReqAnsManager是对发送数据包需要后续验证处理的管理业务类。他们的关系如图所示:

其中重要的总管CommonManager类负责管理各类的协调,其中启动、停止及退出实现如下所示:


#region
启动和退出



bool
_isStart
=

false
;

public
DateTime StartTime;

public
DateTime StopTime;


public

bool
IsStart
{

get
{
return
_isStart; }

set
{ _isStart
=
value; }
}


public

void
Start()
{

if
(
!
IsStart)
{
StartTime

=
DateTime.Now;
DispatchManager.Instance.Start();
LoginManager.Instance.Login();

//
使用默认账号密码登录




this
.CheckTimer();

TimeSpan ts

=
(TimeSpan)(DateTime.Now
-

this
.StartTime);
Log.WriteInfo(

string
.Format(
"
[{0}] 结束启动服务.  | cost: {1} ms
"
,
this
._Name, ts.TotalMilliseconds));

IsStart

=

true
;
}
}


public

void
Stop()
{

if
(pcClient.Connected)
{
pcClient.DisConnect();
}

this
.StopBaseDataRefresh();
FileWatcherManger.Instance.StopFileWatcher();

this
.StopCheckTimer();
StopTime

=
DateTime.Now;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Offline);
Log.WriteInfo(

string
.Format(
"
操作者主动停止服务
"
));

IsStart

=

false
;
}


public

void
Exit()
{

if
(IsProcess)
{
Application.ExitThread();
System.Diagnostics.Process.GetCurrentProcess().Kill();
}

else

{
Stop();
}
}


#endregion



#region
终端通信


PCDataClient pcClient

=

new
PCDataClient();

public

bool
Connected
{

get
{
return
pcClient.Connected; }
}


///

<summary>


///
连接服务器

///

</summary>



public

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


///

<summary>


///
不指定服务器ip和port时,使用默认值

///

</summary>



public

void
Connect()
{

this
.Connect(PCDataCollector_Config.Default.ServerIP, PCDataCollector_Config.Default.ServerPort);
}


public

bool
Send(
string
send)
{

if
(
!
this
.Connected)
{
Connect();

//
确保连接上


}


return
pcClient.SendTo(send);
}

#endregion

2、 状态自动检查

另外,CommonManager类需要启用一个定时器,来定时检测Socket客户端的连接情况,数据接收线程的正常情况,数据处理分派业务类的正常情况,把它放到一个Timer处理定时检测的实现。


///
<summary>


///
服务检查

///
</summary>



public
void
CheckTimer()
{

if
(_CheckTimer
==
null
)
{
_CheckTimer

=
new
System.Threading.Timer(
new
TimerCallback(Check));
_CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan, PCDataCollector_Config.Default.CheckTimerSpan);
}
Log.WriteInfo(

string
.Format(
"
[{0}] 服务检查线程启动.....
"
,
this
._Name));
}


///
<summary>


///
停止服务器检查

///
</summary>



public
void
StopCheckTimer()
{

if
(_CheckTimer
!=
null
)
{
_CheckTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
}


///
<summary>


///
检查

///
</summary>


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



private
void
Check(Object stateInfo)
{

if
(_CheckTimer
!=
null
)
{
_CheckTimer.Change(Timeout.Infinite, Timeout.Infinite);
}

Check();


if
(_CheckTimer
!=
null
)
{
_CheckTimer.Change(PCDataCollector_Config.Default.CheckTimerSpan, PCDataCollector_Config.Default.CheckTimerSpan);
}
}


public
void
Check()
{
pcClient.CheckConnect();
ReceiverForServer.Instance.Check();
DispatchManager.Instance.Check();
PostServerInfo();

}

这样终端就能定时的检测和通讯服务器之间的连接以及数据处理线程的正常运行。

3、 用户登录及用户注销

用户登录,首先通过发送登录协议指令并在内存中记录发送的协议包,等待服务器的响应,当收到服务器的登录响应后 ,执行相关的操作,如启动文件目录监控,为发送数据做好准备。


public

void
Login()
{
Login(PCDataCollector_Config.Default.UserName, PCDataCollector_Config.Default.Password);
}


///

<summary>


///
使用账号密码登录

///

</summary>



public

void
Login(
string
userNo,
string
password)
{

if
(
string
.IsNullOrEmpty(userNo)
&&

string
.IsNullOrEmpty(password))
{

throw

new
Exception(
"
用户名或密码不能为空!
"
);
}

this
.userNo
=
userNo;

this
.password
=
password;


SendLogin();
}


///

<summary>


///
重新登录

///

</summary>



public

void
ReLogin()
{
SendLogin();
}


private

void
SendLogin()
{

string
seqNo
=
DateTime.Now.ToString(
"
yyyyMMdd
"
)
+

new
Random().Next(
99999
).ToString().PadLeft(
5
,
'
0
'
);
//
发送请求


AuthenticationRequest requestData
=

new
AuthenticationRequest(seqNo, userNo, password);
CommonManager.Instance.Send(requestData.ToString());


//
记录请求


ReqAnsManager.Instance.Add(
new
RequestRecord(DataTypeKey.AuthenticationRequest, seqNo, DateTime.Now.AddSeconds(
10
)));

Log.WriteInfo(

string
.Format(
"
正在登录。。。。{0}
"
, tryCount));
Interlocked.Increment(

ref
tryCount);
//
计数


}


///

<summary>


///
服务器响应处理

///

</summary>



public

void
HandleLoginResult(AuthenticationAnswerData data)
{

try

{
RequestRecord record

=
ReqAnsManager.Instance.Find(data.SeqNo, DataTypeKey.AuthenticationRequest);

if
(record
!=

null
)
{
ReqAnsManager.Instance.Remove(record);

if
(data.ValidateResult
==

0
)
{
tryCount

=

0
;
//
重置失败次数为0


lastLoginTime
=
DateTime.Now;
isLogined

=

true
;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Online);

Log.WriteInfo(

"
登录成功!
"
);


//
ThreadPool.QueueUserWorkItem(DataAccess.Instance.LoadBaseData);

//
CommonManager.Instance.StarBaseDataRefresh();


//
登录成功后,对指定文件夹进行监控,自动发送数据


FileWatcherManger.Instance.StartFileWatcher();
}

else

{
lastLoginTime

=
DateTime.Now;
isLogined

=

false
;
Portal.gc.MainDialog.SetNotifyStatus(NotifyIconHelper.Status.Offline);

Log.WriteError(

"
登录失败:
"

+
data.Message);


if
(tryCount
<
PCDataCollector_Config.Default.TryLoginCount)
{
Thread.Sleep(

100
);
LoginManager.Instance.ReLogin();
}

else

{
Log.WriteInfo(

string
.Format(
"
尝试登录失败超过【{0}】次,等待【{1}】秒后再进行连接!
"
,
PCDataCollector_Config.Default.TryLoginCount, PCDataCollector_Config.Default.ReConnectSecconds));

tryCount

=

0
;
//
重置失败次数为0


Thread.Sleep(PCDataCollector_Config.Default.ReConnectSecconds
*

1000
);
LoginManager.Instance.ReLogin();
}
}
}
}

catch
(Exception ex)
{
Log.WriteError(

"
初始化异常:
"

+
ex.Message);
CommonManager.Instance.Exit();
}
}

用户的注销及断开,是通过客户端连接类来处理,该类命名为PCDataClient,其继承自Socket客户端处理类BaseSocketClient类,基类封装了对一般Socket类的连接、接收、发送等操作。该类只需要把解析好的数据传送给
ReceiverForServer类进行处理,而该类会把
数据分派给处理类DispatchManager类进行进一步处理。


public

class
PCDataClient : BaseSocketClient
{

protected

override

void
OnRead(PreData data)
{
ReceiverForServer.Instance.AppendPreData(data);
ReceiverForServer.Instance.Check();
}


///

<summary>


///
断开连接

///

</summary>



public

override

void
DisConnect()
{

base
.DisConnect();

LoginManager.Instance.isLogined

=

false
;
LoginManager.Instance.tryCount

=

0
;
//
重置失败次数为0


//
客户端和服务器连接中断


Log.WriteError(
string
.Format(
"
客户端和服务器连接中断
"
));
}


///

<summary>


///
由于基类只是在调用CheckConnect()函数时确保连接,并没有重新登录,

///
因此需要重载基类,如果断网了,连接后重新执行登录,否则收发数据不成功

///

</summary>



public

override

void
CheckConnect()
{

if
(
!
this
.Connected)
{
Connect(IP, Port);
LoginManager.Instance.Login();
}
Log.WriteInfo(

string
.Format(
"
[{0}] 服务连接 | IP:{1} | Port:{2} | receive:{3} | send:{4}
"
,
_Name, IP, Port, ReceivePackCount, SendPackCount));
}
}

对接收到的数据进行统一处理,只需要继承基类
BaseReceiver即可,相关基类的实现可以参考随笔《
Socket开发探秘--基类及公共类的定义
》。


public

class
ReceiverForServer : BaseReceiver
{

#region
单件



///

<summary>


///
Receiver实例

///

</summary>



private

static
ReceiverForServer _manager;

///

<summary>


///
锁定实例

///

</summary>



private

static

object
oClassLock
=

typeof
(ReceiverForServer);


///

<summary>


///
得到该实例

///

</summary>


///

<returns></returns>



public

static
ReceiverForServer Instance
{

get

{

lock
(oClassLock)
//
加锁只生成一个实例


{

if
(_manager
==

null
)
{
_manager

=

new
ReceiverForServer();
}
}

return
_manager;
}
}


///

<summary>


///
私有的构造函数,防止从外部实例化

///

</summary>



private
ReceiverForServer()
{
}


#endregion



public

override

void
PreDataHandle(PreData data)
{
DispatchManager.Instance.Dispatch(data);
}

}

4、 协议数据组装拆包

数据的组包和拆包,在较早的随笔《
Socket开发探秘--数据封包和拆包
》有详细的介绍,数据的组装和拆包主要通过反射原理,把字符串数据转换为对应的实体类,或者把实体类组装成字符串。

5、 数据分派处理


public

sealed

class
DispatchManager
{

#region
单例



private

static
DispatchManager _manager
=

new
DispatchManager();


public

static
DispatchManager Instance
{

get

{

return
_manager;
}
}


private
DispatchManager() { }


#endregion



#region
委托、事件



//
认证应答事件



public

delegate

void
AuthenticationAnswerDelegate(AuthenticationAnswerData data);

public

event
AuthenticationAnswerDelegate SignalAuthenticationAnswer;


//
空车位上传数据应答事件



public

delegate

void
PCParkingDataAnswerDelegate(PCParkingInfoAnswer data);

public

event
PCParkingDataAnswerDelegate parkingDataAnswer;


//
明细信息上传的应答事件



public

delegate

void
PCTicketDataAnswerDelegate(PCTicketDataAnswer data);

public

event
PCTicketDataAnswerDelegate ticketDataAnswer;


#endregion



///

<summary>


///
启动,关联所有socket接收过来分发的事件

///

</summary>



public

void
Start()
{

//
注册登录结果处理方法


SignalAuthenticationAnswer
+=

new
DispatchManager.AuthenticationAnswerDelegate(LoginManager.Instance.HandleLoginResult);
parkingDataAnswer

+=

new
PCParkingDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
ticketDataAnswer

+=

new
PCTicketDataAnswerDelegate(FileWatcherManger.Instance.HandleAnswerResult);
}


#region
方法


///

<summary>


///
分发应答数据

///
对于部分类型的应答,需要进行内部广播

///

</summary>


///

<param name="predata"></param>



public

void
Dispatch(PreData predata)
{

if
(predata
==

null
)

return
;


switch
(predata.Key)
{

//
身份认证应答



case
DataTypeKey.AuthenticationAnswer:
AuthenticationAnswerData authData

=

new
AuthenticationAnswerData(predata.Content);
SignalAuthenticationAnswer(authData);

break
;


case
DataTypeKey.PCParkingInfoAnswer:
PCParkingInfoAnswer spaceData

=

new
PCParkingInfoAnswer(predata.Content);
parkingDataAnswer(spaceData);

break
;


case
DataTypeKey.PCTicketDataAnswer:
PCTicketDataAnswer pickBaseData

=

new
PCTicketDataAnswer(predata.Content);
ticketDataAnswer(pickBaseData);

break
;


default
:

break
;
}
}


///

<summary>


///
系统检查

///

</summary>



public

void
Check()
{
}


#endregion

}

由于涉及内容较多,篇幅太长,这些相关的内容将逐步介绍,期间可能会逐步对代码进行优化和提炼,以求达到通用、良好封装的目的,能促进其他项目的使用及处理。

希望本文对于从事Socket开发处理的读者有一个好的引导和启发,大家一起交流,互通有无,各取所长。

在前面几篇文章介绍我的Winform框架随笔文章,包括有《
Winform开发框架之字典数据管理
》、《
Winform开发框架之权限管理系统
》、《
Winform开发框架之终极应用
》,其中Winform开发框架之终极应用是集众多功能与一身,提供综合一站式、整体性的传统应用系统的开发框架,在此基础上开发新的业务系统,开发工作则是事半功倍,而且提供了高效、统一的界面布局以及支持多种数据库的数据访问层支持,提供了基于大量数据的数据分页解决方案,提供了传统Excel报表以及自定义模板的高级Excel报表的解决方案支持。我们先回顾一下框架的整体介绍图片。

基于上面的内容,我在之前《》中开发了一套基于传统界面的框架,如下所示:

这种界面是较为传统的界面框架,统一性及美观性都是比较标准的Winform界面,由于本人之前研究开发过基于DotNetBar框架的Office Ribbon样式的Winform界面,详细可以参考文章《
如何使用dotnetbar控件来构造多文档界面
》 ,因此本人想把这些界面样式整合到我的Winform开发框架中,作为另外一种界面样式的Winform开发框架。经过周末不少时间的整理改造,成功改造原有传统的Winform开发框架界面,界面效果如下所示:


另外编辑界面也适当调整为:


以上界面样式和传统的界面开发框架有不少的差别,通过利用Office2007 Ribbon样式,可以放置更多的功能按钮,并像Office2007那样,可以折叠起来更有效利用界面空间,而且也能够给用户较为绚丽的界面,使得你的应用程序在客户中获得较高的印象分数。

整个界面工程如下所示:

虽然使用了基于DotNetBar控件改造了Winform界面,但是和之前的开发框架,界面之间的继承关系并没有变化,都是遵循下面的原则:

1、停靠在主窗体中的界面继承自基类BaseDock;

2、弹出的普通窗体,继承自基类BaseForm;

3、弹出的数据编辑窗体,继承自BaseEditForm,而BaseEditForm继承自BaseForm。

通过不同的继承基类,我封装了不同的处理函数,节省了子类的重复代码,并增强了Winform界面的统一性、良好封装性、良好可操作性。


下面总结几个界面改造过程中注意的事项:

1、如果是从已有Winform窗体中转换的话,需要把原有的TextBox控件、Label控件、ComboBox控件调整为TextBoxX、LableX、ComboBoxX控件外,还需要修改TextBox的Border.Class为TextBoxBorder(默认添加的TextboxX控件有该样式),否则样式没有边框,不好看;ComboBoxX控件的DrawMode修改为OwnerDrawFixed,否则同样没有边框,不好看。另外,如果Label或者文本控件的背景色存在白色,通过选定控件,拉动一下即可解决该问题。

2、虽然窗体集成自Office2007Form,界面已经有一些Office的界面效果了,不过为了好看,最好在窗体界面中添加一个PanelEx这样的控件,界面尽可能不要GroupBox控件,而是用PanelEx这样的控件来代替,这样效果好一些。


3、布局使用DotNetBar控件中的Bar控件或者Tab控件来替代原有的Weifenluo控件,这样整体性会好一些。

4、封装Bar控件,使之在创建窗体的时候,更加方便,而不需要更多的代码,如在主窗体中增加一个窗体,代码如下即可:

SetMdiForm(
new
FrmItemDetail(),
"
备件信息
"
);

5、利用DataGridViewEx来替换默认的 DataGridView控件,整体效果也会更加好一些,用法是一样的。

基于Dotnetbar的Office2007最新Winform框架程序,

可以从网站地址
http://www.iqidi.com/download/warehouse/Device_DotNetBar.rar
下载