2023年2月

写C#进行Visio二次开发的系列文章有很多篇了,都是写一些经验总结和技术知识的分享,本文继续来探讨这方面开发的一些心得,主要介绍下WinForm界面的设计和Visio软件的架构设计。
先看看我花了N个月的业余时间完成的软件概貌,然后大家一起讨论下相关的技术知识。
Visio_MainForm.jpg

软件界面主要有菜单区、工具条区、图纸内容区、右边的管理区、状态条区等等,主要注意的是采用了ToolStripPanel来进行布局,然后并使用了有名的WeifenLuo.WinFormsUI.Docking.dll控件对图纸内容区、右边的管理区进行控制。
其他就是细致的修改了:如为按钮、菜单添加图片,调整样式等。

Visio的SDK提供了很多基本的操作功能,如下面所示的这些功能,主要都是通过调用其内部函数实现的。
Visio_File.jpg
Visio_View.jpg

其中文件中的另存为和导出AutoCad的操作是通过下面代码完成的。


SaveFileDialog dlg
=

new
SaveFileDialog();

dlg.FileName

=
Portal.gc.gVisioImage.VisioName;

dlg.Filter

=

"
Visio文件(*.vsd)|*.vsd|所有文件(*.*)|*.*
"
;

dlg.FilterIndex

=

1
;

if
(dlg.ShowDialog()
==
DialogResult.OK)



{

if
(dlg.FileName.Trim()
!=

string
.Empty)



{

if
(File.Exists(dlg.FileName))



{

File.Copy(Portal.gc.gVisioTemplateFilePath, dlg.FileName,

true
);

}



else




{

File.Copy(Portal.gc.gVisioTemplateFilePath, dlg.FileName);

}



}



}


SaveFileDialog dlg
=

new
SaveFileDialog();

dlg.FileName

=

""
;

dlg.Filter

=

"
AutoCAD 绘图 (*.dwg)|*.dwg|所有文件(*.*)|*.*
"
;

dlg.FilterIndex

=

1
;

if
(dlg.ShowDialog()
==
DialogResult.OK)



{

if
(dlg.FileName.Trim()
!=

string
.Empty)



{

VisApplication.ActivePage.Export(dlg.FileName);

}



}


其中的视图中有很多Visio的视图窗口需要控制,如形状窗口、自定义属性窗口、扫视缩放窗口、大小位置窗口、其他还有如标尺、网格等显示设置需要控制。这些都是通过调用Application.DoCmd((short)commandID);函数实现的,只要传入了相应的命令ID,那么就达到了控制显示的功能。如自定义属性窗口为VisUICmds.visCmdCustProp,形状窗口为VisUICmds.visCmdShapesWindow,标尺为VisUICmds.visCmdViewRulers,网格为VisUICmds.visCmdViewGrid,这些属性都是可以在SDK中查找到的。

缩放图纸操作,请参考我的一篇文章:
C#进行Visio二次开发之图纸缩放操作的实现
里面有具体的实现过程。

Visio_Other.jpg
Visio_MarkerMenu.jpg
以上的格式和形状菜单项中的操作也是通过Application.DoCmd((short)commandID);函数实现的,只要查询到相应的命令ID即可进行调用,你花点时间看看VisUICmds枚举下都有那些命令ID可以调用就可以了。
以上图片中关于右键菜单的操作,请参考我前面写的一篇文章
C#进行Visio二次开发之自定义右键菜单

再来说说软件的架构设计吧。
整个UI层主要应用了前面介绍过的WeifenLuo.WinFormsUI.Docking.dll的控件(即图中的DockingControl组件),还有就是Visio Drawing Control的OCX控件了(图中的VisioOCX组件)。
两边是各个层中用到的通用类(Commons)和实体类(Entity)。
Visio_Architecture.jpg

Visio的二次开发,除了需要操作Visio的文件(包含多个模具文件,Viso文档)还有就是也需要和数据库打交道(没有数据库的程序会逊色很多的)。
为了较好区分和协调他们的访问,我设定了一个访问边界:访问数据库的层不会访问Visio文件对象,访问Visio文件对象的不会去访问数据库,它们统一由业务层(Business)调配,各层之间分享Entity层的信息即可。
数据库的底层访问通过利用Enterprise Library的模块完成,因此DAL层只需要做较少的工作即可完成对数据库的访问了。

该软件很早就开始支持Enterprise Library的数据访问操作了,不过之前的没有生成一个完整的解决方案工程,只是生成了数据访问类和实体类,改善后的Enterprise Library代码生成,生成整个项目工程框架,
包含实体类、数据访问类、业务类,利用泛型及缓存机制,良好的架构极大简化代码,强大完善的基类使你甚至不用编写一行代码
1、界面操作如下
Database2Sharp_Enterprise.jpg

2、点击几下鼠标后,生成的解决方案如下,其中标为红色部分为表Equipment生成的相关类,其他是辅助类,BaseDAL和BaseBLL封装了对数据访问的绝大多数函数,数据访问底层采用的是Enterprise Library数据访问库(微软的数据访问层)。

EnterpriseLib.jpg

3、另外实体类还增加了WCF实体类的生成功能,方便你编写WCF代码
Database2Sharp_WCFEntity.jpg

一般来说,对系统的分层,一般都需要下面几个层:实体层(Entity)、数据访问层(DAL)、业务逻辑层(BLL)、界面层(UI);而数据访问层,一般也会加入一个接口层(IDAL)。
在其中的实体层,一般是根据数据库进行映射外加入注释等,技术含量不大,在此一笔带过;数据库访问层和业务逻辑层,是关键之所在,因为这里好的设计,会利用很多基类的操作,减少很多代码和重复劳动;界面层,不管是WebForm还是WinForm,都是尽可能少的逻辑代码或者SQL语句在其中,好的项目可能会利用一些优秀的控件进去,提高体验,减少代码。另外,由于一些创建操作费时费资源,一般还需要把可重复利用的资源缓存起来,提高性能。
先给大家预览下项目的框架,再一层层分析讨论:
EnterpriseLib.jpg

1、 实体层(定义一个空的基类,其他实体类继承之,主要是为了利用泛型操作,用途下面细说)



public

class
BaseEntity



{

}




public

class
EquipmentInfo : BaseEntity



{


Field Members





Property Members




}

2、 数据库访问层,数据访问层的关键是数据访问基类的设计,基类实现大多数数据库的日常操作,如下:





///

<summary>


///
数据访问层的基类

///

</summary>



public

abstract

class
BaseDAL
<
T
>
: IBaseDAL
<
T
>

where
T : BaseEntity,
new
()



{

}

BaseEntity就是实体类的基类,IBaseDAL是定义的数据访问基类接口,包含各种常用的操作定义;因此BaseDAL就是要对各种操作的进行实现,实现接口越多,将来继承类的重用程度就越高。
以上通过泛型
<
T
>
,我们就可以知道实例化那个具体访问类的信息了,可以实现强类型的函数定义。





///

<summary>


///
一些基本的,作为辅助函数的接口

///

</summary>



public

interface
IBaseDAL
<
T
>

where
T : BaseEntity



{



///

<summary>


///
查询数据库,检查是否存在指定键值的对象

///

</summary>


///

<param name="recordTable">
Hashtable:键[key]为字段名;值[value]为字段对应的值
</param>


///

<returns>
存在则返回
<c>
true
</c>
,否则为
<c>
false
</c>

</returns>



bool
IsExistKey(Hashtable recordTable);




///

<summary>


///
查询数据库,检查是否存在指定键值的对象

///

</summary>


///

<param name="fieldName">
指定的属性名
</param>


///

<param name="key">
指定的值
</param>


///

<returns>
存在则返回
<c>
true
</c>
,否则为
<c>
false
</c>

</returns>



bool
IsExistKey(
string
fieldName,
object
key);




///

<summary>


///
获取数据库中该对象的最大ID值

///

</summary>


///

<returns>
最大ID值
</returns>



int
GetMaxID();




///

<summary>


///
根据指定对象的ID,从数据库中删除指定对象

///

</summary>


///

<param name="key">
指定对象的ID
</param>


///

<returns>
执行成功返回
<c>
true
</c>
,否则为
<c>
false
</c>

</returns>



bool
DeleteByKey(
string
key);




///

<summary>


///
根据条件,从数据库中删除指定对象

///

</summary>


///

<param name="condition">
删除记录的条件语句
</param>


///

<returns>
执行成功返回
<c>
true
</c>
,否则为
<c>
false
</c>

</returns>



bool
DeleteByCondition(
string
condition);





///

<summary>


///
插入指定对象到数据库中

///

</summary>


///

<param name="obj">
指定的对象
</param>


///

<returns>
执行成功返回True
</returns>



bool
Insert(T obj);




///

<summary>


///
更新对象属性到数据库中

///

</summary>


///

<param name="obj">
指定的对象
</param>


///

<returns>
执行成功返回
<c>
true
</c>
,否则为
<c>
false
</c>

</returns>



bool
Update(T obj,
string
primaryKeyValue);




///

<summary>


///
查询数据库,检查是否存在指定ID的对象(用于整型主键)

///

</summary>


///

<param name="key">
对象的ID值
</param>


///

<returns>
存在则返回指定的对象,否则返回Null
</returns>


T FindByID(
int
key);




///

<summary>


///
查询数据库,检查是否存在指定ID的对象(用于字符型主键)

///

</summary>


///

<param name="key">
对象的ID值
</param>


///

<returns>
存在则返回指定的对象,否则返回Null
</returns>


T FindByID(
string
key);



返回集合的接口



}

细看上面代码,会发现由一个PagerInfo 的类,这个类是用来做分页参数传递作用的,根据这个参数,你可以知道具体返回那些关心的记录信息,这些记录又转换为强类型的List
<
T
>
集合。
再看看数据库访问基类的具体实现代码吧:




///

<summary>


///
数据访问层的基类

///

</summary>



public

abstract

class
BaseDAL
<
T
>
: IBaseDAL
<
T
>

where
T : BaseEntity,
new
()



{


构造函数





通用操作方法





对象添加、修改、查询接口





返回集合的接口





子类必须实现的函数(用于更新或者插入)





IBaseDAL接口



}

3、具体的数据访问类
基类完成所有的操作了,对于具体的类将是一大福音,说明它的工作减少很多了,下面看看具体的实现过程。定义一个数据访问类接口,然后实现接口和继承基类即可。



public

interface
IEquipment : IBaseDAL
<
EquipmentInfo
>




{

}


public

class
Equipment : BaseDAL
<
EquipmentInfo
>
, IEquipment



{


对象实例及构造函数



}

其实这样就完成了,我们为了提高效率,重载两个函数的实现,避免基类的属性反射带来的性能损失,这两个函数看似很复杂,其实通过代码生成工具,生成起来也是毫不费功夫的。。


protected

override
EquipmentInfo DataReaderToEntity(IDataReader dataReader)


protected

override
Hashtable GetHashByEntity(EquipmentInfo obj)

因此最后的代码就变为下面



public

class
Equipment : BaseDAL
<
EquipmentInfo
>
, IEquipment



{


对象实例及构造函数






///

<summary>


///
将DataReader的属性值转化为实体类的属性值,返回实体类

///

</summary>


///

<param name="dr">
有效的DataReader对象
</param>


///

<returns>
实体类对象
</returns>



protected

override
EquipmentInfo DataReaderToEntity(IDataReader dataReader)



{

EquipmentInfo equipmentInfo

=

new
EquipmentInfo();

SmartDataReader reader

=

new
SmartDataReader(dataReader);


equipmentInfo.ID

=
reader.GetInt32(
"
ID
"
);

equipmentInfo.PartID

=
reader.GetString(
"
PartID
"
);

equipmentInfo.Name

=
reader.GetString(
"
Name
"
);

equipmentInfo.EquipmentType

=
reader.GetString(
"
EquipmentType
"
);

equipmentInfo.Specification

=
reader.GetString(
"
Specification
"
);

equipmentInfo.Manufacturer

=
reader.GetString(
"
Manufacturer
"
);

equipmentInfo.Picture

=
reader.GetBytes(
"
Picture
"
);

equipmentInfo.ApplyEquipment

=
reader.GetString(
"
ApplyEquipment
"
);

equipmentInfo.BuyAmount

=
reader.GetInt32(
"
BuyAmount
"
);

equipmentInfo.BuyDate

=
reader.GetDateTime(
"
BuyDate
"
);

equipmentInfo.Status

=
reader.GetString(
"
Status
"
);

equipmentInfo.UserName

=
reader.GetString(
"
UserName
"
);

equipmentInfo.SafeNumber

=
reader.GetInt32(
"
SafeNumber
"
);

equipmentInfo.Note

=
reader.GetString(
"
Note
"
);


return
equipmentInfo;

}






///

<summary>


///
将实体对象的属性值转化为Hashtable对应的键值

///

</summary>


///

<param name="obj">
有效的实体对象
</param>


///

<returns>
包含键值映射的Hashtable
</returns>



protected

override
Hashtable GetHashByEntity(EquipmentInfo obj)



{

EquipmentInfo info

=
obj
as
EquipmentInfo;

Hashtable hash

=

new
Hashtable();


hash.Add(

"
ID
"
, info.ID);

hash.Add(

"
PartID
"
, info.PartID);

hash.Add(

"
Name
"
, info.Name);

hash.Add(

"
EquipmentType
"
, info.EquipmentType);

hash.Add(

"
Specification
"
, info.Specification);

hash.Add(

"
Manufacturer
"
, info.Manufacturer);

hash.Add(

"
Picture
"
, info.Picture);

hash.Add(

"
ApplyEquipment
"
, info.ApplyEquipment);

hash.Add(

"
BuyAmount
"
, info.BuyAmount);

hash.Add(

"
BuyDate
"
, info.BuyDate);

hash.Add(

"
Status
"
, info.Status);

hash.Add(

"
UserName
"
, info.UserName);

hash.Add(

"
SafeNumber
"
, info.SafeNumber);

hash.Add(

"
Note
"
, info.Note);


return
hash;

}



}



文章太长,下面关于逻辑层、缓存、界面部分的设计在下一篇文章中介绍。

以上所引用的代码是通过代码生成工具Database2Sharp自动生成(
http://www.iqidi.com/Database2Sharp.htm
),选择EnterpriseLibrary架构即可。
Database2Sharp_Enterprise.jpg

接着上一篇关于分层架构的讨论,
一个分层架构设计的例子(1)

上篇介绍了实体类(Entity)、数据库访问类(DAL)、数据访问接口(IDAL)的相关设计,本篇主要讨论下面几个部分内容:业务逻辑层、缓存机制、界面层等方面。
业务逻辑层,主要是业务逻辑基类的设计,由于数据库访问类(DAL)的基类封装了大量的操作实现,因此,业务逻辑层的主要工作是进一步封装对底层访问接口的实现,如下所示。



public

class
BaseBLL
<
T
>

where
T : BaseEntity,
new
()



{


构造函数





对象添加、修改、删除等接口





返回集合的接口



}


业务层基类封装了大量的调用,那么对于业务层的具体操作类,它的工作就很简单了,基本上只需要继承一下基类就可以了,这就是有一个优秀父亲的好处,呵呵



public

class
Equipment : BaseBLL
<
EquipmentInfo
>




{

public
Equipment() :
base
()



{

}



}


基本上,业务层的设计到此应该收尾了,可是我们注意到,很多开发都使用了缓存的机制来进一步提高程序的性能,下面对这方面进行讨论。缓存的机制,一般是把创建过的对象资源放到一个集合中,需要的时候,调出来,如下业务层的工厂类所示。



public

class
BLLFactory
<
T
>

where
T :
class




{

private

static
Hashtable objCache
=

new
Hashtable();

public

static
T Instance



{

get




{

string
CacheKey
=

typeof
(T).FullName;

T bll

=
(T)objCache[CacheKey];
//
从缓存读取



if
(bll
==

null
)



{

bll

=
Reflect
<
T
>
.Create(
typeof
(T).Name,
"
HuaweiSoftware.IPSPBD.BLL
"
);
//
反射创建,并缓存


}



return
bll;

}



}



}


这是一个业务逻辑类工厂创建类,我们在界面层只需要如下调用即可构造一个(利用了缓存)具体的业务类出来


CustomerInfo info
=
BLLFactory
<
Customer
>
.Instance.FindByID(ID);


在上面的BaseBLL和BLLFactory类中,有一个Reflect的操作类,这是反射缓存的具体实现所在,我们探讨一下它的实现。



public

class
Reflect
<
T
>

where
T :
class




{

private

static
Hashtable m_objCache
=

null
;

public

static
Hashtable ObjCache



{

get




{

if
(m_objCache
==

null
)



{

m_objCache

=

new
Hashtable();

}



return
m_objCache;

}



}




public

static
T Create(
string
sName,
string
sFilePath)



{

return
Create(sName, sFilePath,
true
);

}



public

static
T Create(
string
sName,
string
sFilePath,
bool
bCache)



{

string
CacheKey
=
sFilePath
+

"
.
"

+
sName;

T objType

=

null
;

if
(bCache)



{

objType

=
(T)ObjCache[CacheKey];
//
从缓存读取



if
(
!
ObjCache.ContainsKey(CacheKey))



{

Assembly assObj

=
CreateAssembly(sFilePath);

object
obj
=
assObj.CreateInstance(CacheKey);

objType

=
(T)obj;


ObjCache.Add(CacheKey, objType);

//
写入缓存 将DAL内某个对象装入缓存


}



}



else




{

objType

=
(T)CreateAssembly(sFilePath).CreateInstance(CacheKey);
//
反射创建


}




return
objType;

}




public

static
Assembly CreateAssembly(
string
sFilePath)



{

Assembly assObj

=
(Assembly)ObjCache[sFilePath];

if
(assObj
==

null
)



{

assObj

=
Assembly.Load(sFilePath);

ObjCache.Add(sFilePath, assObj);

//
将整个DLL装入缓存


}



return
assObj;

}



}



另外,如果你在业务层需要实现更加复杂的功能,而数据库访问基类BaseDAL提供的函数不能满足你的需要,可以扩展数据访问层的接口和实现,如下所示。



public

interface
ICustomer : IBaseDAL
<
CustomerInfo
>




{

List

<
string
>
GetAllCustomerNumber();


CustomerInfo GetByCustomerNumber(

string
number);

}





public

class
Customer : BaseDAL
<
CustomerInfo
>
, ICustomer



{


对象实例及构造函数













ICustomer 成员



}



那么在业务层的类修改如下



public

class
Customer : BaseBLL
<
CustomerInfo
>




{

public
Customer() :
base
()



{

}




public
List
<
string
>
GetAllCustomerNumber()



{

ICustomer customerDAL

=
baseDal
as
ICustomer;

return
customerDAL.GetAllCustomerNumber();

}




public
CustomerInfo GetByCustomerNumber(
string
number)



{

ICustomer customerDAL

=
baseDal
as
ICustomer;

return
customerDAL.GetByCustomerNumber(number);

}



}


最后,界面方面的设计是见仁见智,但根本一条是利用一些控件,可以统一风格,减少劳动,给出几个界面的设计截图供大家参考
WinForm方面的(颜色标明的是使用了特定的界面控件,其中红色部分为和整个架构整合起来的分页控件,集成了一些基本的右键菜单操作,包括打印功能、数据导出功能等):
WinForm_UI1.jpg

Winform分页控件设计视图
GridViewPager.jpg

可以选择列进行打印
GridViewPager_PrintOption.jpg

在实际运用过程中的界面效果
GridViewPager_Product.jpg


WebForm方面的(可以使用之前文章介绍的查询控件、分页控件、内容编辑控件):

下图是查询控件和分页控件的一起运用:

WebForm_UI1.jpg

修改内容时候的编辑控件
WebForm_UI2.jpg


查看内容时候的编辑控件
WebForm_UI3.jpg


以上所引用的代码是通过代码生成工具Database2Sharp自动生成(
http://www.iqidi.com/Database2Sharp.htm
),选择EnterpriseLibrary架构即可。
Database2Sharp_Enterprise.jpg

—

界面应统一风格(菜单、工具条、状态条)


—

控件的图片、图片透明颜色


—

控件的命名统一


—

菜单项目

menu_

—

工具条按钮

tsb_

—

文本
txt_
、下拉列表
cmb_



—

控件布局:


—

基类
BaseForm
,实现统一出现位置


—

窗体的大小尽量一样


—

Tab
顺序、控件长度高度、控件停靠、自动伸缩


—

菜单、按钮快捷键


—

注重你代码及知识的储备


—

每做一个项目,储备几个辅助类


—

看到好的控件或代码,收集


—

利用一切可以使用的轮子


—


codeproject
网站中找相关内容


—

比较并找出较好、较合适的进行修改


—

组装更好的轮子