2023年2月

在我前面很多篇关于框架设计和介绍的文章里面,大多数都是利用框架提供的基础性API进行各种的操作,包括增删改查、分页等各种实现和其衍生的实现,而这些实现绝大多数是基于SQL的标准操作实现的,由于框架的底层是利用了微软企业库Enterprise Library,因此框架也是很好的支持存储过程的各种调用,不过由于整体性和数据库迁移方面的考虑,建议一般使用标准的SQL操作而已,这样能够很大程度上保证数据库可以很平滑过渡到其他数据库,如Access、SQLite等单机版数据库。但是,有时候我们提供对存储过程的支持也是十分必要的,有些业务可能就只是固定在某种特定的数据库上跑,如SQLServer、Oracle等这些支持存储过程的关系型数据库,有些业务可能还真的需要存储过程的整体性的封装;基于这个原因,我撰写了这篇文章,力求从较为全面的角度上阐述存储过程的编写、实现和演化提炼方面做一个介绍。

1、SQLServer存储过程的编写

虽然存储过程一般用于处理一些复杂的逻辑关系或者报表内容,不过为了介绍方便,我们从几个较为基础的操作进行介绍。

我们以一个客户表来进行对应的存储过程来介绍,先介绍客户表T_Customer的表定义。

它的SQLServer脚本如下所示

create tabledbo.T_Customer (
ID
nvarchar(50) not null,
Name
nvarchar(50) null,
Age
int null,
Creator
nvarchar(50) null,
CreateTime
datetime null,constraint PK_T_CUSTOMER primary key(ID)
)

为了介绍存储过程的编写,我们以这个表的相关操作的存储过程来进行介绍,存储过程一般可以分为下面几种情况。

1)提供执行处理,可对执行结果进行反馈

这种情况常常可以见到,如可以对插入、更新、删除等操作进行处理,并获得执行的结果,下面是这两种存储过程的代码。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:插入数据到表中------------------------------------
 CREATE PROCEDUREdbo.T_Customer_Insert 
(
@ID varchar(50),@Name varchar(50) ,@Age int)AS begin tran Insert into dbo.T_Customer( ID,Name,Age ) Values( @ID,@Name,@Age)if @@error!=0 begin rollback end else begin commit end go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,修改表中的数据------------------------------------ CREATE PROCEDUREdbo.T_Customer_UpdateByID
(
@ID varchar(50),@Name varchar(50) ,@Age int)AS begin tran Update dbo.T_Customer Set Name=@Name,Age=@Age Where ID= @ID if @@error!=0 begin rollback end else begin commit end go
--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,删除表的记录------------------------------------
 CREATE PROCEDUREdbo.T_Customer_DeleteByID 
(
@ID varchar(50)
)
AS begin tran Delete From dbo.T_Customer where ID=@ID if @@error!=0 begin rollback end else begin commit end go

2)提供执行处理,获得一个或者多个返回性参数,并可对执行结果进行反馈。

基于上面的处理方式,我们可能还有一种情况,就是需要执行存储过程个,并返回对应的返回参数,我们可以在程序里面利用代码获取这些返回参数的数值,从而用作其他用途。

因此,这种操作,如要是获取返回性参数的情况,如下所示是判断记录是否存在,以及获取客户最大年龄的两个存储过程。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录------------------------------------
 CREATE PROCEDUREdbo.T_Customer_ExistByID 
(
@Exist intoutput ,@ID varchar(50)
)
AS Select @Exist = Case When Exists (Select 1 From dbo.T_Customer Where ID=@ID) Then 1 Else 0 End go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:获取客户最大年龄------------------------------------ CREATE PROCEDUREdbo.T_Customer_MaxAge
(
@MaxAge intoutput )AS Select @MaxAge=Case When Max(Age) is NULL Then 0 Else Max(Age) End Fromdbo.T_Customergo

3)提供查询处理,并返回实体对象

这小节后面介绍的内容,都是存储过程的返回值,这些或者是一条记录,或者是多条记录的查询结果,这个在SQLServer里面很容易实现,而在Oracle里面需要通过游标进行处理。

下面存储过程脚本,是基于返回单条记录的存储过程。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检索表中的数据------------------------------------
 CREATE PROCEDUREdbo.T_Customer_SelectByID 
(
@ID varchar(50)
)
AS Select * from dbo.T_Customer Where ID= @ID go

4)提供查询处理,并返回多条记录集合;包括实体列表集合或
DataTable集合对象

对于返回多条集合的对象,在存储过程里面体现都一样的,我们可能在C#处理的时候,把它转换为不同的对象即可,返回多个集合,在SQLServer里面,它们的存储过程代码如下所示。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:检索表中所有的数据------------------------------------
 CREATE PROCEDUREdbo.T_Customer_SelectAllAS 
 Select * fromdbo.T_Customergo 

2、Oracle存储过程的编写

对应客户表T_Customer,Oracle的创建脚本如下所示。

CREATE TABLET_CUSTOMER ( 
ID
VARCHAR2(100),
NAME
VARCHAR2(50) NOT NULL,
AGE
INTEGER NOT NULL,
CREATOR
VARCHAR2(50) NULL,
CREATETIME DATE
DEFAULTSYSDATE,
);
ALTER TABLE T_CUSTOMER ADD CONSTRAINT PK_T_CUSTOMER PRIMARY KEY (ID);

对应SQLServer的存储过程,Oracle的存储过程也提供了对应的版本,下面是几种情况下的Oracle存储过程的编写。

1)提供执行处理,可对执行结果进行反馈

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:插入数据到表中------------------------------------
 Create Or Replace ProcedureT_Customer_Insert 
(
p_ID
IN T_CUSTOMER.ID%TYPE,
p_Name
IN T_CUSTOMER.NAME%TYPE,
p_Age
IN T_CUSTOMER.AGE%TYPE
)
AS Begin Insert into T_CUSTOMER( ID,NAME,AGE ) Values( p_ID,p_Name,p_Age ) ;Commit;
Exception
When Others Then Rollback;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,修改表中的数据------------------------------------ Create Or Replace ProcedureT_Customer_UpdateByID
(
p_ID
IN T_CUSTOMER.ID%TYPE,
p_Name
IN T_CUSTOMER.NAME%TYPE,
p_Age
IN T_CUSTOMER.AGE%TYPE
)
AS Begin Update T_CUSTOMER Set NAME=p_Name,AGE=p_Age Where ID=p_ID ;Commit;
Exception
When Others Then Rollback;End;/
--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,删除表的记录------------------------------------
 Create Or Replace ProcedureT_Customer_DeleteByID 
(
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin Delete From T_CUSTOMER where ID=p_ID ;Commit;
Exception
When Others Then Rollback;End;/

其中上面的代码涉及几个地方,T_CUSTOMER.ID%TYPE是表示根据字段动态决定参数的类型,避免应硬编码或者反复修改参数类型。

Oracle的参数一般使用p_的前缀开始,方便区分。

2)提供执行处理,获得一个或者多个返回性参数,并可对执行结果进行反馈。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录------------------------------------
 Create Or Replace ProcedureT_Customer_ExistByID 
(
p_Exist OUT
Number,
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin --V9.i以下使用的语句 Select Case When (Count(1)>0) Then 1 Else 0 End Into p_Exist From T_CUSTOMER Where ID=p_ID ;--也可以使用的语句 --Select Decode(Count(1),0,0,1) Into p_Exist From T_CUSTOMER Where ID=p_ID ; End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:获取表用来标识字段的最大ID值,在标识ID非自增字段时可用于数据插入时调用------------------------------------ Create Or Replace ProcedureT_Customer_MaxAge
(
p_MaxAge OUT
Number)AS Begin Select Decode(Max(Age) ,NULL,0,Max(Age)) Into p_MaxAge FromT_CUSTOMER;End;/

上面的代码,都有一个输出的参数,虽然他们执行没有影响记录函数,但是这个主要是通过输出参数的值进行处理了。

3)提供查询处理,并返回实体对象

提供查询处理,不管返回一条记录,还是多条记录,在Oracle里面,一般都是通过游标进行处理的,因此我们需要先定义一个游标类型,供我们返回记录使用的。

下面定义一个游标的包代码如下。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:创建一个包,含有一个游标类型:(一个数据库中只需声明一次)------------------------------------
 CREATE OR REPLACEPACKAGE MyCURSORASTYPE cur_OUTIS REF CURSOR;End;/ 

然后我们就可以在各个返回记录的存储过程里面使用这个游标类型了。

例如在下面的存储过程里面,返回一条指定的数据记录,那么输出参数里面需要有一个游标的定义参数,但是我们在C#里面使用数据访问框架来处理数据的时候,可以忽略他它的存在,就只需要输入p_ID参数就可以了。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检索表中的数据------------------------------------
 Create Or Replace ProcedureT_Customer_SelectByID 
(
cur_OUT OUT MyCURSOR.cur_OUT ,
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin OPEN cur_OUT FOR Select * from T_CUSTOMER Where ID =p_ID ;End;/

4)提供查询处理,并返回多条记录集合;包括实体列表集合或DataTable集合对象

和上面返回单条记录一样,需要返回多条记录的存储过程,也需要使用一个游标的输出参数来获取返回的记录,并可以对游标进行处理。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:检索表中所有的数据------------------------------------
 Create Or Replace ProcedureT_Customer_SelectAll 
( cur_OUT OUT MyCURSOR.cur_OUT )
AS Begin OPEN cur_OUT FOR Select * fromT_CUSTOMER;End;/

最后,我们看看SQLServer和Oracle数据库的脚本完整情况。

SQLServer存储过程代码:


--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:插入数据到表中------------------------------------
 CREATE PROCEDUREdbo.T_Customer_Insert 
(
@ID varchar(50),@Name varchar(50) ,@Age int)AS begin tran Insert into dbo.T_Customer( ID,Name,Age ) Values( @ID,@Name,@Age)if @@error!=0 begin rollback end else begin commit end go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,修改表中的数据------------------------------------ CREATE PROCEDUREdbo.T_Customer_UpdateByID
(
@ID varchar(50),@Name varchar(50) ,@Age int)AS begin tran Update dbo.T_Customer Set Name=@Name,Age=@Age Where ID= @ID if @@error!=0 begin rollback end else begin commit end go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:检索表中所有的数据------------------------------------ CREATE PROCEDUREdbo.T_Customer_SelectAllAS Select * fromdbo.T_Customergo --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检索表中的数据------------------------------------ CREATE PROCEDUREdbo.T_Customer_SelectByID
(
@ID varchar(50)
)
AS Select * from dbo.T_Customer Where ID= @ID go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录------------------------------------ CREATE PROCEDUREdbo.T_Customer_ExistByID
(
@Exist intoutput ,@ID varchar(50)
)
AS Select @Exist = Case When Exists (Select 1 From dbo.T_Customer Where ID=@ID) Then 1 Else 0 End go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,删除表的记录------------------------------------ CREATE PROCEDUREdbo.T_Customer_DeleteByID
(
@ID varchar(50)
)
AS begin tran Delete From dbo.T_Customer where ID=@ID if @@error!=0 begin rollback end else begin commit end go --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:获取客户最大年龄------------------------------------ CREATE PROCEDUREdbo.T_Customer_MaxAge
(
@MaxAge intoutput )AS Select @MaxAge=Case When Max(Age) is NULL Then 0 Else Max(Age) End Fromdbo.T_Customergo

View Code

Oracle存储过程代码:


--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:插入数据到表中------------------------------------
 Create Or Replace ProcedureT_Customer_Insert 
(
p_ID
IN T_CUSTOMER.ID%TYPE,
p_Name
IN T_CUSTOMER.NAME%TYPE,
p_Age
IN T_CUSTOMER.AGE%TYPE
)
AS Begin Insert into T_CUSTOMER( ID,NAME,AGE ) Values( p_ID,p_Name,p_Age ) ;Commit;
Exception
When Others Then Rollback;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,修改表中的数据------------------------------------ Create Or Replace ProcedureT_Customer_UpdateByID
(
p_ID
IN T_CUSTOMER.ID%TYPE,
p_Name
IN T_CUSTOMER.NAME%TYPE,
p_Age
IN T_CUSTOMER.AGE%TYPE
)
AS Begin Update T_CUSTOMER Set NAME=p_Name,AGE=p_Age Where ID=p_ID ;Commit;
Exception
When Others Then Rollback;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:创建一个包,含有一个游标类型:(一个数据库中只需声明一次)------------------------------------ CREATE OR REPLACEPACKAGE MyCURSORASTYPE cur_OUTIS REF CURSOR;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:检索表中所有的数据------------------------------------ Create Or Replace ProcedureT_Customer_SelectAll
( cur_OUT OUT MyCURSOR.cur_OUT )
AS Begin OPEN cur_OUT FOR Select * fromT_CUSTOMER;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检索表中的数据------------------------------------ Create Or Replace ProcedureT_Customer_SelectByID
(
cur_OUT OUT MyCURSOR.cur_OUT ,
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin OPEN cur_OUT FOR Select * from T_CUSTOMER Where ID =p_ID ;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检查表中是否存在符合条件的记录------------------------------------ Create Or Replace ProcedureT_Customer_ExistByID
(
p_Exist OUT
Number,
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin --V9.i以下使用的语句 Select Case When (Count(1)>0) Then 1 Else 0 End Into p_Exist From T_CUSTOMER Where ID=p_ID ;--V8.i及以下使用的语句 --Select Decode(Count(1),0,0,1) Into p_Exist From T_CUSTOMER Where ID=p_ID ; End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,删除表的记录------------------------------------ Create Or Replace ProcedureT_Customer_DeleteByID
(
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin Delete From T_CUSTOMER where ID=p_ID ;Commit;
Exception
When Others Then Rollback;End;/ --------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:获取表用来标识字段的最大ID值,在标识ID非自增字段时可用于数据插入时调用------------------------------------ Create Or Replace ProcedureT_Customer_MaxAge
(
p_MaxAge OUT
Number)AS Begin Select Decode(Max(Age) ,NULL,0,Max(Age)) Into p_MaxAge FromT_CUSTOMER;End;/

View Code

以上就是存储过程编写过程中的处理和对比,下一篇将继续介绍这个主体,并针对性的介绍如何在C#底层数据访问里面,对这些存储过程的使用。

本系列两篇文章,列表如下:

Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(1)

Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(2)

本篇继续上篇《
Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(1)
》来对Winform开发框架之存储过程的支持进行介绍,上篇主要介绍了SQLServer和Oracle两种数据库对常规存储过程的编写和对比,本篇主要介绍如何在C#里面,如何对这些存储过程进行调用,并获取到对应的数据类型,如输出参数,单个数据记录,多个数据记录等情况。最后在完成实现功能的基础上,对这些实现进行演化提炼,并扩展到我的WInform开发框架里面,实现功能重用、代码简化的目的。

1、数据访问接口的定义

我们整个实例是以一个客户表T_Customer为例进行讲解的,整个表的框架支持代码,可以通过代码生成工具进行快速生成,生成后包括了IDAL、Entity、DALSQL、BLL层代码,然后可以利用代码进行测试存储过程是否执行成功等功能。

数据访问层的定义,依照框架的分层模式来处理,后面我们在增加DALOracle对Oracle数据库进行支持即可。

生成后数据访问层接口,他们通过基类接口继承的方式,已经具有了常规的增删改查、分页等系列接口,但是其他业务接口还是需要自己定义的,如数据访问接口成的定义如下所示。

namespaceWHC.TestProject.IDAL
{
/// <summary> ///客户信息/// </summary> public interface ICustomer : IBaseDAL<CustomerInfo>{
}
}

这里面的代码很简单,没有多余的代码行,那么里面究竟发生了什么呢,其中的IBaseDAL又是什么定义呢?

其实,IBaseDAL就是定义了很多我们开发用到的基础接口,如标准的增删改查,以及衍生出来的一些其他接口,如分页查询,条件查询等接口内容。这个ICustomer就是用来定义一些除了标准接口不能实现外的业务接口。

如果我们需要实现基于存储过程的接口,我们可能就需要增加一些接口定义,如下所示。

namespaceWHC.TestProject.IDAL
{
/// <summary> ///客户信息/// </summary> public interface ICustomer : IBaseDAL<CustomerInfo>{#region 使用存储过程 /// <summary> ///使用存储过程插入数据/// </summary> /// <param name="info">实体对象</param> /// <returns></returns> bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null);/// <summary> ///使用存储过程更新数据/// </summary> /// <param name="info">实体对象</param> /// <returns></returns> bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null);/// <summary> ///使用存储过程获取所有数据/// </summary> /// <returns></returns> List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null);/// <summary> ///使用存储过程获取所有数据/// </summary> /// <returns></returns> DataTable StorePorc_GetAllToDataTable(DbTransaction trans = null);/// <summary> ///使用存储过程,根据ID获取对应记录/// </summary> /// <param name="ID"></param> /// <returns></returns> CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null);/// <summary> ///使用存储过程,判断记录ID是否存在/// </summary> /// <param name="ID">记录ID</param> /// <returns></returns> bool StorePorc_ExistByID(string ID, DbTransaction trans = null);/// <summary> ///使用存储过程,根据ID删除对应记录/// </summary> /// <param name="ID">记录ID</param> /// <returns></returns> bool StorePorc_DeleteByID(string ID, DbTransaction trans = null);/// <summary> ///获取客户的最大年龄/// </summary> /// <returns></returns> intStorePorc_GetMaxAge();#endregion}

对于插入、更新和删除这样的操作,我们只需要返回它是否成功就可以了,那么它的接口实现应该是如何的呢?

2、SqlServer存储过程的调用实现

由于我们的Winform开发框架底层是利用微软企业库EnterpriseLibrary来访问数据的,那么对应这个企业库的使用存储过程的方法,也就是我们的实现了。

下面的代码就是它们对应的SqlServer实现了。

        public bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null)
{
string procName = "T_Customer_Insert";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddInParameter(command,
"@ID", DbType.String, info.ID);
db.AddInParameter(command,
"@Name", DbType.String, info.Name);
db.AddInParameter(command,
"@Age", DbType.Int32, info.Age);bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
returnresult;
}
public bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null)
{
string procName = "T_Customer_UpdateByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddInParameter(command,
"@ID", DbType.String, info.ID);
db.AddInParameter(command,
"@Name", DbType.String, info.Name);
db.AddInParameter(command,
"@Age", DbType.Int32, info.Age);bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
returnresult;
}
public bool StorePorc_DeleteByID(string ID, DbTransaction trans = null)
{
string procName = "T_Customer_DeleteByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);
db.AddInParameter(command,
"@ID", DbType.String, ID);bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
returnresult;
}

对于有返回输出参数的值,我们的做法有些不同,不过最主要的还是最终获取它的输出参数值而已,代码如下所示。

        public bool StorePorc_ExistByID(string ID, DbTransaction trans = null)
{
bool result = false;string procName = "T_Customer_ExistByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);
db.AddInParameter(command,
"@ID", DbType.String, ID);
db.AddOutParameter(command,
"@Exist", DbType.Int32, 0);//输出参数 if (trans != null)
{
db.ExecuteNonQuery(command, trans);
}
else{
db.ExecuteNonQuery(command);
}
int iExist = 0;int.TryParse(db.GetParameterValue(command, "@Exist").ToString(), outiExist);

result
= iExist > 0;returnresult;
}
public intStorePorc_GetMaxAge()
{
string procName = "T_Customer_MaxAge";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddOutParameter(command,
"@MaxAge", DbType.Int32, 0);//输出参数 db.ExecuteNonQuery(command);int maxAge = 0;int.TryParse(db.GetParameterValue(command, "@MaxAge").ToString(), outmaxAge);returnmaxAge;
}

上面的代码,主要就是利用了AddOutParameter对输出参数的信息进行设置,输出参数的数据类型要和脚本里面的类型定义对应,它的AddOutParameter的size参数值,可以为0。

最后我们通过db.GetParameterValue(command, "@MaxAge")的方式获取它的输出参数的值,并返回即可。

最后一个例子是介绍如何通过代码调用,获得它的实体对象或者实体对象列表,以及DataTable集合对象的例子了,这个也相对不是很麻烦,参照框架里面的做法即可。

获取实体对象信息的代码如下所示。

        public CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null)
{
string procName = "T_Customer_SelectByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);
db.AddInParameter(command,
"@ID", DbType.String, ID);

CustomerInfo entity
= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
if(dr.Read())
{
entity
=DataReaderToEntity(dr);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
if(dr.Read())
{
entity
=DataReaderToEntity(dr);
}
}
}
returnentity;
}

获取集合的代码如下所示。

        public List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null)
{
string procName = "T_Customer_SelectAll";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

List
<CustomerInfo> list = new List<CustomerInfo>();
CustomerInfo entity
= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
list.Add(entity);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
list.Add(entity);
}
}
}
returnlist;
}
public DataTable StorePorc_GetAllToDataTable(DbTransaction trans = null)
{
string procName = "T_Customer_SelectAll";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

DataTable dt
= null;if (trans != null)
{
dt
= db.ExecuteDataSet(command, trans).Tables[0];
}
else{
dt
= db.ExecuteDataSet(command).Tables[0];
}
returndt;
}

3、Oracle存储过程的调用实现

上面是基于SqlServer存储过程的调用,前面的一篇文章我们介绍了存储过程的Oracle定义,是增加了一个游标来进行记录行数据的处理的,不管对于单行记录,还是多行记录,都是用了游标的输出参数的,那么在客户端里面,使用EnterpriseLibrary,应该如何调用,并且不需要传入这个输出参数的呢,做法其实很类似,只是有一点差异而已。

我们先从最简单的Oracle存储过程调用案例开始,介绍如何调用插入、更新和删除操作的Oracle存储过程的调用。这里和SqlServer的类似,不同的是我们使用了p_前缀来定义参数(基于Oracle的通用脚本参数定义规则)。

        public bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null)
{
string procName = "T_Customer_Insert";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddInParameter(command,
"p_ID", DbType.String, info.ID);
db.AddInParameter(command,
"p_Name", DbType.String, info.Name);
db.AddInParameter(command,
"p_Age", DbType.Int32, info.Age);bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
returnresult;
}
public bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null)
{
string procName = "T_Customer_UpdateByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddInParameter(command,
"p_ID", DbType.String, info.ID);
db.AddInParameter(command,
"p_Name", DbType.String, info.Name);
db.AddInParameter(command,
"p_Age", DbType.Int32, info.Age);bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
returnresult;
}

Oracle输出外部参数的做法也和sqlServer类似,具体调用代码如下所示。

        public intStorePorc_GetMaxAge()
{
string procName
="T_Customer_MaxAge";Database db =CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

db.AddOutParameter(command, "p_MaxAge", DbType.Int32,
0);//输出参数

db.ExecuteNonQuery(command);
int maxAge = 0;int.TryParse(db.GetParameterValue(command, "p_MaxAge").ToString(), out maxAge);returnmaxAge;
}

其他的也就很类似,就不再一一赘述了,基本上和SqlServer的一致,我们节省篇幅,用来看看如何调用返回记录的查询接口。下面是对应的Oracle存储过程的调用代码

        public CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null)
{
string procName = "T_Customer_SelectByID";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);
db.AddInParameter(command,
"p_ID", DbType.String, ID);

CustomerInfo entity
= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
if(dr.Read())
{
entity
=DataReaderToEntity(dr);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
if(dr.Read())
{
entity
=DataReaderToEntity(dr);
}
}
}
returnentity;
}

返回多条记录的操作代码如下所示。

        public List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null)
{
string procName = "T_Customer_SelectAll";

Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(procName);

List
<CustomerInfo> list = new List<CustomerInfo>();
CustomerInfo entity
= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
list.Add(entity);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
list.Add(entity);
}
}
}
returnlist;
}

看完上面两个对记录处理的接口,我们看到,还是对我们在Oracle存储过程里面定义的输出游标参数忽略处理,我们不需要对它进行传值,它好像是透明的,呵呵。

这样它的做法就和SqlServer个各个接口实现也都差不多的了。

下面的脚本是我们之前定义的Oracle存储过程脚本,方便对比参照一下调用的函数代码。

--------------------------------------作者:伍华聪 http://wuhuacong.cnblogs.com--创建时间:2014年11月27日--功能描述:以字段ID为关键字,检索表中的数据------------------------------------
 Create Or Replace ProcedureT_Customer_SelectByID 
(
cur_OUT OUT MyCURSOR.cur_OUT ,
p_ID
IN T_CUSTOMER.ID%TYPE
)
AS Begin OPEN cur_OUT FOR Select * from T_CUSTOMER Where ID =p_ID ;End;/

4、业务逻辑层的实现

上面我们定义了数据访问接口,以及两种数据实现层,在框架里面会根据不同的数据库类型配置,然后从不同的数据库访问层构建对象的,业务逻辑层主要就是对他们的接口进行调用了,具体代码如下所示。

    /// <summary>
    ///客户信息/// </summary>
    public class Customer : BaseBLL<CustomerInfo>{public Customer() : base()
{
base.Init(this.GetType().FullName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
}
/// <summary> ///根据客户名称获取客户列表/// </summary> /// <param name="name">客户名称</param> /// <returns></returns> public List<CustomerInfo> FindByName(stringname)
{
string condition = string.Format("Name like '%{0}%'", name);returnbaseDal.Find(condition);
}
#region 使用存储过程 public bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_Insert(info, trans);
}
public bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_Update(info, trans);
}
public List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_GetAll(trans);
}
public DataTable StorePorc_GetAllToDataTable(DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_GetAllToDataTable(trans);
}
public CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_FindByID(ID, trans);
}
public bool StorePorc_ExistByID(string ID, DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_ExistByID(ID, trans);
}
public bool StorePorc_DeleteByID(string ID, DbTransaction trans = null)
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_DeleteByID(ID, trans);
}
public intStorePorc_GetMaxAge()
{
ICustomer dal
= baseDal asICustomer;returndal.StorePorc_GetMaxAge();
}
#endregion}

为了验证我们的实现是否能够正常处理,并顺利获取对应的对象或者集合,我们需要编写一些代码,用来对它进行测试。

测试的代码如下所示。

        /// <summary>
        ///测试存储过程的插入、修改、返回实体类、返回实体类集合、返回DataTable对象、输出参数等接口/// </summary>
        private void btnTestStoreProc_Click(objectsender, EventArgs e)
{
//定义一个实体类的数据 CustomerInfo info = newCustomerInfo();
info.Name
= "测试名称";
info.Age
= 20;//调用存储过程插入数据,并判断是否成功 bool inserted = BLLFactory<Customer>.Instance.StorePorc_Insert(info);
Debug.Assert(inserted);
//调用存储过程,获取输出参数,获得最大年龄值 int maxAge = BLLFactory<Customer>.Instance.StorePorc_GetMaxAge();
Debug.Assert(maxAge
> 0);//调用存储过程,修改客户名称 info.Name = "修改名称";bool updated = BLLFactory<Customer>.Instance.StorePorc_Update(info);//调用存储过程,获取最新的实体类对象,并对比是否修改成功 CustomerInfo newInfo = BLLFactory<Customer>.Instance.StorePorc_FindByID(info.ID);
Debug.Assert(newInfo
!= null);
Debug.Assert(newInfo.Name
==info.Name);//调用存储过程,获取输出参数,判断指定ID记录是否存在 bool exist = BLLFactory<Customer>.Instance.StorePorc_ExistByID(info.ID);
Debug.Assert(exist);
//调用存储过程,获取全部实体列表集合,判断实体类列表是否正确 List<CustomerInfo> list = BLLFactory<Customer>.Instance.StorePorc_GetAll();
Debug.Assert(list.Count
> 0);//调用存储过程,获取DataTable对象,判断集合不为空 DataTable dt = BLLFactory<Customer>.Instance.StorePorc_GetAllToDataTable();
Debug.Assert(dt.Rows.Count
> 0);//调用存储过程,执行删除操作,并判断是否成功了 bool deleted = BLLFactory<Customer>.Instance.StorePorc_DeleteByID(info.ID);
Debug.Assert(deleted);
string result = "全部操作完成";
Console.WriteLine(result);
MessageUtil.ShowTips(result);
}

5、具体测试和验证

为了对他们进行测试,我们需要分别对SqlServer和Oracle进行测试,然后才能确认我们的实现是正确的。

分别在SQLServer和Oracle上运行存储过程脚本,创建对应的数据库脚本,如下所示。

测试Winform小程序,会得到成功的标志,标识所有的断言全部通过。

6、框架基类的演化提炼

本来写到上面小节,应该就可以告一段落了,因为功能也已经完成了,而且还是支持了两种不同的数据库,说明我们的实现和原先的想法都是正确的。

但是,我从来不喜欢臃肿的代码,我们留心回头看看前面的代码,两种不同数据库的实现很多是相似的,即使对于同一个数据库(如SQLServer)的存储过程接口实现,他们还是有很多优化的地方,代码依旧不够精简和优化,本小节就是专门针对这些进行提炼和优化的。

前面的框架介绍文章,我们可以了解到,数据访问接口实现层和接口定义层一样,都有一个基类,如基于SqlServer实现的基类为BaseDALSQL,这个基于SqlServer的数据访问基类,它也是继承自一个超级基类(大多数的实现在这里)AbstractBaseDAL。他们之间的继承关系如下所示,最终我们把提炼好的内容,放到这个AbstractBaseDAL就可以了,这样各个子类都可以进行调用,实现存储过程的处理。

对于存储过程的实现,我们分析一下各个接口,可以看到,输入参数是可选的,因为有些接口不需要输出参数;输出参数也是可选的,有些接口也不需要输出参数,返回的记录类型主要有bool类型,实体类型,实体集合类型,DataTable类型这几种,当然虽然有年龄接口的整形,但是这个是通过输出参数来获得的。

我们于是可以定义一个类似这样的通用接口参数集合,用来处理需要返回是否成功获取带有输出参数的,事务对象的接口,如下所示。

        /// <summary>
        ///执行存储过程,如果影响记录数,返回True,否则为False,修改并输出外部参数outParameters(如果有)。/// </summary>
        /// <param name="storeProcName">存储过程名称</param>
        /// <param name="inParameters">输入参数,可为空</param>
        /// <param name="outParameters">输出参数,可为空</param>
        /// <param name="trans">事务对象,可为空</param>
        /// <returns>如果影响记录数,返回True,否则为False</returns>
        public bool StorePorcExecute(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)

它的实现基本上就是分为了几部分,第一部分是传入参数值(包括输入参数、输出参数的值),第二部是执行存储过程,三部分是获得输出参数并修改值即可。

具体的实现代码如下所示。

        /// <summary>
        ///执行存储过程,如果影响记录数,返回True,否则为False,修改并输出外部参数outParameters(如果有)。/// </summary>
        /// <param name="storeProcName">存储过程名称</param>
        /// <param name="inParameters">输入参数,可为空</param>
        /// <param name="outParameters">输出参数,可为空</param>
        /// <param name="trans">事务对象,可为空</param>
        /// <returns>如果影响记录数,返回True,否则为False</returns>
        public bool StorePorcExecute(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)
{
Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(storeProcName);//参数传入 SetStoreParameters(db, command, inParameters, outParameters);//获取执行结果 bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
//获取输出参数的值 EditOutParameters(db, command, outParameters);returnresult;
}

上面两部分红色哪里,因为他们在很多其他函数里面也通用,所以我就抽离作为一个私有函数了,就是传入参数,和传出结果的两部分。

由于输入输出参数都是可选的,因为我们不确定它是否存在值,所以我们分别对它进行了一定的处理,具体两个函数的代码如下所示。

        /// <summary>
        ///传入输入参数和输出参数到Database和DbCommand对象。/// </summary>
        /// <param name="db">Database对象</param>
        /// <param name="command">DbCommand对象</param>
        /// <param name="inParameters">输入参数的哈希表</param>
        /// <param name="outParameters">输出参数的哈希表</param>
        private void SetStoreParameters(Database db, DbCommand command, Hashtable inParameters = null, Hashtable outParameters = null)
{
#region 参数传入 //传入输入参数 if (inParameters != null)
{
foreach (string param ininParameters.Keys)
{
object value =inParameters[param];
db.AddInParameter(command, param, TypeToDbType(value.GetType()), value);
}
}
//传入输出参数 if (outParameters != null)
{
foreach (string param inoutParameters.Keys)
{
object value =outParameters[param];
db.AddOutParameter(command, param, TypeToDbType(value.GetType()),
0);//size统一设置为0 }
}
#endregion}/// <summary> ///执行存储过程后,获取需要输出的参数值,修改存储在哈希表里/// </summary> /// <param name="db">Database对象</param> /// <param name="command">DbCommand对象</param> /// <param name="outParameters">输出参数的哈希表</param> private void EditOutParameters(Database db, DbCommand command, Hashtable outParameters = null)
{
#region 获取输出参数的值 if (outParameters != null)
{
ArrayList keys
= new ArrayList(outParameters.Keys);//使用临时集合对象,避免迭代错误 foreach (string param inkeys)
{
object retValue =db.GetParameterValue(command, param);object value =outParameters[param];
outParameters[param]
=Convert.ChangeType(retValue, value.GetType());
}
}
#endregion}

这样我们就完成了一个普通存储过程该接口的通用处理了,但是我们知道,还有返回列表对象,列表集合,DataTable对象的几种不同方式,我们也应该要对他们进行一定的封装处理,已达到在子类能够很好使用的目的。

下面我把整个对这几部分封装的代码进行公布,它们的封装的代码如下所示(记得是放在超级抽象类上AbstractBaseDAL即可。

        #region 存储过程执行通用方法

        /// <summary>
        ///执行存储过程,如果影响记录数,返回True,否则为False,修改并输出外部参数outParameters(如果有)。/// </summary>
        /// <param name="storeProcName">存储过程名称</param>
        /// <param name="inParameters">输入参数,可为空</param>
        /// <param name="outParameters">输出参数,可为空</param>
        /// <param name="trans">事务对象,可为空</param>
        /// <returns>如果影响记录数,返回True,否则为False</returns>
        public bool StorePorcExecute(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)
{
Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(storeProcName);//参数传入 SetStoreParameters(db, command, inParameters, outParameters);//获取执行结果 bool result = false;if (trans != null)
{
result
= db.ExecuteNonQuery(command, trans) > 0;
}
else{
result
= db.ExecuteNonQuery(command) > 0;
}
//获取输出参数的值 EditOutParameters(db, command, outParameters);returnresult;
}
/// <summary> ///执行存储过程,返回实体列表集合,修改并输出外部参数outParameters(如果有)。/// </summary> /// <param name="storeProcName">存储过程名称</param> /// <param name="inParameters">输入参数,可为空</param> /// <param name="outParameters">输出参数,可为空</param> /// <param name="trans">事务对象,可为空</param> /// <returns>返回实体列表集合</returns> public List<T> StorePorcToList(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)
{
Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(storeProcName);//参数传入 SetStoreParameters(db, command, inParameters, outParameters);#region 获取执行结果List<T> result = new List<T>();
T entity
= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
result.Add(entity);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
while(dr.Read())
{
entity
=DataReaderToEntity(dr);
result.Add(entity);
}
}
}
#endregion //获取输出参数的值 EditOutParameters(db, command, outParameters);returnresult;
}
/// <summary> ///执行存储过程,返回DataTable集合,修改并输出外部参数outParameters(如果有)。/// </summary> /// <param name="storeProcName">存储过程名称</param> /// <param name="inParameters">输入参数,可为空</param> /// <param name="outParameters">输出参数,可为空</param> /// <param name="trans">事务对象,可为空</param> /// <returns>返回DataTable集合</returns> public DataTable StorePorcToDataTable(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)
{
Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(storeProcName);//参数传入 SetStoreParameters(db, command, inParameters, outParameters);#region 获取执行结果DataTable result= null;if (trans != null)
{
result
= db.ExecuteDataSet(command, trans).Tables[0];
}
else{
result
= db.ExecuteDataSet(command).Tables[0];
}
if (result != null)
{
result.TableName
= "tableName";//增加一个表名称,防止WCF方式因为TableName为空出错 }#endregion //获取输出参数的值 EditOutParameters(db, command, outParameters);returnresult;
}
/// <summary> ///执行存储过程,返回实体对象,修改并输出外部参数outParameters(如果有)。/// </summary> /// <param name="storeProcName">存储过程名称</param> /// <param name="inParameters">输入参数,可为空</param> /// <param name="outParameters">输出参数,可为空</param> /// <param name="trans">事务对象,可为空</param> /// <returns>返回实体对象</returns> public T StorePorcToEntity(string storeProcName, Hashtable inParameters = null, Hashtable outParameters = null, DbTransaction trans = null)
{
Database db
=CreateDatabase();
DbCommand command
=db.GetStoredProcCommand(storeProcName);//参数传入 SetStoreParameters(db, command, inParameters, outParameters);#region 获取执行结果T result= null;if (trans != null)
{
using (IDataReader dr =db.ExecuteReader(command, trans))
{
if(dr.Read())
{
result
=DataReaderToEntity(dr);
}
}
}
else{using (IDataReader dr =db.ExecuteReader(command))
{
if(dr.Read())
{
result
=DataReaderToEntity(dr);
}
}
}
#endregion //获取输出参数的值 EditOutParameters(db, command, outParameters);returnresult;
}
/// <summary> ///传入输入参数和输出参数到Database和DbCommand对象。/// </summary> /// <param name="db">Database对象</param> /// <param name="command">DbCommand对象</param> /// <param name="inParameters">输入参数的哈希表</param> /// <param name="outParameters">输出参数的哈希表</param> private void SetStoreParameters(Database db, DbCommand command, Hashtable inParameters = null, Hashtable outParameters = null)
{
#region 参数传入 //传入输入参数 if (inParameters != null)
{
foreach (string param ininParameters.Keys)
{
object value =inParameters[param];
db.AddInParameter(command, param, TypeToDbType(value.GetType()), value);
}
}
//传入输出参数 if (outParameters != null)
{
foreach (string param inoutParameters.Keys)
{
object value =outParameters[param];
db.AddOutParameter(command, param, TypeToDbType(value.GetType()),
0);//size统一设置为0 }
}
#endregion}/// <summary> ///执行存储过程后,获取需要输出的参数值,修改存储在哈希表里/// </summary> /// <param name="db">Database对象</param> /// <param name="command">DbCommand对象</param> /// <param name="outParameters">输出参数的哈希表</param> private void EditOutParameters(Database db, DbCommand command, Hashtable outParameters = null)
{
#region 获取输出参数的值 if (outParameters != null)
{
ArrayList keys
= new ArrayList(outParameters.Keys);//使用临时集合对象,避免迭代错误 foreach (string param inkeys)
{
object retValue =db.GetParameterValue(command, param);object value =outParameters[param];
outParameters[param]
=Convert.ChangeType(retValue, value.GetType());
}
}
#endregion}#endregion

封装好这些超级基类后,我们在数据访问层里面,就可以很好地简化对存储过程的调用了,而且他们的做法都很类似,我们可以对比一下,它们调用存储过程的实现真正简化了很多。

例如对于SqlServer数据访问层,使用超级基类的接口,我们简化代码如下所示。

        public bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"ID", info.ID);
inParameters.Add(
"Name", info.Name);
inParameters.Add(
"Age", info.Age);return StorePorcExecute("T_Customer_Insert", inParameters, null, trans);
}
public bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"ID", info.ID);
inParameters.Add(
"Name", info.Name);
inParameters.Add(
"Age", info.Age);return StorePorcExecute("T_Customer_UpdateByID", inParameters, null, trans);
}
public List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null)
{
return StorePorcToList("T_Customer_SelectAll", null, null, trans);
}
public DataTable StorePorc_GetAllToDataTable(DbTransaction trans = null)
{
return StorePorcToDataTable("T_Customer_SelectAll", null, null, trans);
}
public CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"ID", ID);return StorePorcToEntity("T_Customer_SelectByID", inParameters, null, trans);
}
public bool StorePorc_ExistByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"ID", ID);

Hashtable outParameters
= newHashtable();
outParameters.Add(
"Exist", 0);

StorePorcExecute(
"T_Customer_ExistByID", inParameters, outParameters, trans);int exist = (int)outParameters["Exist"];return exist > 0;
}
public bool StorePorc_DeleteByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"ID", ID);return StorePorcExecute("T_Customer_DeleteByID", inParameters, null, trans);
}
public intStorePorc_GetMaxAge()
{
Hashtable outParameters
= newHashtable();
outParameters.Add(
"MaxAge", 0);

StorePorcExecute(
"T_Customer_MaxAge", null, outParameters, null);int MaxAge = (int)outParameters["MaxAge"];returnMaxAge;
}

对于Oracle数据访问层的实现来说,它的接口实现一样简单,只是参数命名有所不同而已。

        public bool StorePorc_Insert(CustomerInfo info, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"p_ID", info.ID);
inParameters.Add(
"p_Name", info.Name);
inParameters.Add(
"p_Age", info.Age);return StorePorcExecute("T_Customer_Insert", inParameters, null, trans);
}
public bool StorePorc_Update(CustomerInfo info, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"p_ID", info.ID);
inParameters.Add(
"p_Name", info.Name);
inParameters.Add(
"p_Age", info.Age);return StorePorcExecute("T_Customer_UpdateByID", inParameters, null, trans);
}
public List<CustomerInfo> StorePorc_GetAll(DbTransaction trans = null)
{
return StorePorcToList("T_Customer_SelectAll", null, null, trans);
}
public DataTable StorePorc_GetAllToDataTable(DbTransaction trans = null)
{
return StorePorcToDataTable("T_Customer_SelectAll", null, null, trans);
}
public CustomerInfo StorePorc_FindByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"p_ID", ID);return StorePorcToEntity("T_Customer_SelectByID", inParameters, null, trans);
}
public bool StorePorc_ExistByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"p_ID", ID);

Hashtable outParameters
= newHashtable();
outParameters.Add(
"p_Exist", 0);

StorePorcExecute(
"T_Customer_ExistByID", inParameters, outParameters, trans);int exist = (int)outParameters["p_Exist"];return exist > 0;
}
public bool StorePorc_DeleteByID(string ID, DbTransaction trans = null)
{
Hashtable inParameters
= newHashtable();
inParameters.Add(
"p_ID", ID);return StorePorcExecute("T_Customer_DeleteByID", inParameters, null, trans);
}
public intStorePorc_GetMaxAge()
{
Hashtable outParameters
= newHashtable();
outParameters.Add(
"p_MaxAge", 0);

StorePorcExecute(
"T_Customer_MaxAge", null, outParameters, null);int MaxAge = (int)outParameters["p_MaxAge"];returnMaxAge;
}

以上就是我针对《Winform开发框架之存储过程的支持--存储过程的实现和演化提炼》这个主题进行的介绍和分析,希望对大家有所帮助,也希望结合我的框架,迅速开发各种不同的项目。

文章内容有点长,感谢您的耐心阅读和支持。

Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(1)

Winform开发框架之存储过程的支持--存储过程的实现和演化提炼(2)

在一般的管理系统模块里面,越来越多的设计到一些常用文档的上传保存操作,其中如PDF、Word、Excel等文档,有时候是通过分布式的WCF技术实现数据的显示和处理,因此希望直接预览而不需要下载文件,这样能够给我们提供很多的方便。在DevExpress里面,提供了相应的控件来显示和处理这些文档,本文主要介绍如何利用DevExpress的控件实现对PDF、Word、Excel文档的预览和操作处理。

1、PDF的预览和操作

在较早的DevExpress的控件里面,已经提供了对应的PDF文档的显示控件,不过由于其对PDF格式支持不是很好,有些文档是Office导出的,也不是很正常阅读,因此很少使用,本文介绍的DevExpress的PDF查看控件是基于14.1的,测试过很多文档,好像都能正常打开,因此也想在系统中广泛使用了。

为了演示这些控件的处理,我单独编写了一个例子,用来实现对PDF、Word、Excel等文档的处理。

为了显示PDF文档,我们需要在界面里面添加一个XtraPdfViewer.PdfViewer的控件,这个主要是用来显示PDF的,它有很多属性方法,用来实现对PDF的处理操作,测试界面设计好如下所示。

对PDF,我们一般主要是用来打开文件,另存为,或者预览就可以了。相关的操作代码如下所示。

    /// <summary>
    ///PDF测试显示窗体/// </summary>
    public partial classPDFViewer : Form
{
//记录窗体的名称 readonly stringmainFormText;publicPDFViewer()
{
InitializeComponent();
//记录窗体的名称,并实现文档变化事件的处理,方便显示新的文件名称 mainFormText = this.Text;
pdfViewer1.DocumentChanged
+= newDevExpress.XtraPdfViewer.PdfDocumentChangedEventHandler(pdfViewer1_DocumentChanged);
}
/// <summary> ///PDF文档变化后,实现对新文件名称的显示/// </summary> void pdfViewer1_DocumentChanged(objectsender, DevExpress.XtraPdfViewer.PdfDocumentChangedEventArgs e)
{
string fileName =Path.GetFileName(e.DocumentFilePath);if(String.IsNullOrEmpty(fileName))
{
Text
=mainFormText;
}
else{
Text
= fileName + "-" +mainFormText;
}
}
/// <summary> ///打开PDF文件/// </summary> private void btnOpenFile_Click(objectsender, EventArgs e)
{
string filePath =FileDialogHelper.OpenPdf();if (!string.IsNullOrEmpty(filePath))
{
this.pdfViewer1.LoadDocument(filePath);
}
}
/// <summary> ///另存为PDF文件/// </summary> private void btnSaveAs_Click(objectsender, EventArgs e)
{
string dir =System.Environment.CurrentDirectory;string filePath = FileDialogHelper.SavePdf("", dir);if (!string.IsNullOrEmpty(filePath))
{
try{this.pdfViewer1.SaveDocument(filePath);
MessageUtil.ShowTips(
"保存成功");
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageUtil.ShowError(ex.Message);
}
}
}
/// <summary> ///PDF文件打印/// </summary> private void btnPreview_Click(objectsender, EventArgs e)
{
this.pdfViewer1.Print();
}
}

从上面的代码,我们可以看到,对于PDF,我们操作起来很方便,主要就是在界面里面加载文件后,就可以对PDFViewer对象实现相关的操作了。

2、WORD文档的预览和操作

类似于PDF文档,我们对WORD文档,也是通过使用RichEditControl实现文档的显示,不过和PDFViewer不同,这个控件可以实现对文档的修改和保存操作,这种对于我们提供用户对文档进行编辑很方便。

测试例子的界面如下所示。

相关的操作代码,也和PDF的操作类似,不同的是,它在文档变化后,不能很容易从参数里面获取到对应的文档的路径,需要特殊的处理才能得到。

    /// <summary>
    ///WORD控件的测试例子/// </summary>
    public partial classWordViewer : Form
{
//记录窗体的名称 readonly stringmainFormText;publicWordViewer()
{
InitializeComponent();
//记录窗体的名称,并实现文档变化事件的处理,方便显示新的文件名称 mainFormText = this.Text;this.richEditControl1.DocumentLoaded += newEventHandler(richEditControl1_DocumentLoaded);
}
/// <summary> ///WORD文档变化后,实现对新文件名称的显示/// </summary> void richEditControl1_DocumentLoaded(objectsender, EventArgs e)
{
string fileName = Path.GetFileName(this.richEditControl1.Options.DocumentSaveOptions.CurrentFileName);if(String.IsNullOrEmpty(fileName))
{
Text
=mainFormText;
}
else{
Text
= fileName + "-" +mainFormText;
}
//修改默认字体 DocumentRange range =richEditControl1.Document.Range;
CharacterProperties cp
= this.richEditControl1.Document.BeginUpdateCharacters(range);
cp.FontName
= "新宋体";//cp.FontSize = 12; this.richEditControl1.Document.EndUpdateCharacters(cp);
}
/// <summary> ///打开WORD文件/// </summary> private void btnOpenFile_Click(objectsender, EventArgs e)
{
string filePath =FileDialogHelper.OpenWord();if (!string.IsNullOrEmpty(filePath))
{
richEditControl1.LoadDocument(filePath);
//, DocumentFormat.Doc); }
}
/// <summary> ///保存WORD文件/// </summary> private void btnSaveFile_Click(objectsender, EventArgs e)
{
this.richEditControl1.SaveDocument();
}
/// <summary> ///另存为WORD文件/// </summary> private void btnSaveAs_Click(objectsender, EventArgs e)
{
try{
richEditControl1.SaveDocumentAs();
MessageUtil.ShowTips(
"保存成功");
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageUtil.ShowError(ex.Message);
}
}
/// <summary> ///WORD文件打印/// </summary> private void btnPreview_Click(objectsender, EventArgs e)
{
this.richEditControl1.ShowPrintPreview();
}
}

加载文档后,界面显示内容如下所示:

文档控件很容易支持打印预览功能,打印预览的界面如下所示

不过话说回来,这个RichEditControl虽然能够较好显示Word文档,不过也有一些字体显示的不是很好,格式和微软的Word软件显示的有些不太一样,格式有所损失。

因此如果对于格式要求比较严谨的,建议还是只是做显示为佳,不要保存原有的文档。如果对格式不是特别严格,倒是可以作为一个文档服务器实现文档的新建、保存处理。

3、Excel文档的预览和操作

对于Excel文档的预览和操作,DevExpress控件在最近版本中增加的XtraSpreadsheet.SpreadsheetControl控件就可以实现Excel的显示和处理操作,这个控件很强大,可以处理很复杂格式的Excel文档,虽然我原来使用了另外一个FarPoint Spread控件组,不过这个XtraSpreadsheet控件组,如果集成在DevExpress也就很方便了。

这个DevExpress的控件,可以在其中进行Excel的新建、保存、打印预览等操作,当然也可以打开我们已有的Excel文件了。

打开文件后,界面效果如下所示。

界面的相关功能操作代码如下所示。

    /// <summary>
    ///Excel控件的测试例子/// </summary>
    public partial classExcelViewer : Form
{
//记录窗体的名称 readonly stringmainFormText;publicExcelViewer()
{
InitializeComponent();
//记录窗体的名称,并实现文档变化事件的处理,方便显示新的文件名称 mainFormText = this.Text;this.spreadsheetControl1.DocumentLoaded += newEventHandler(spreadsheetControl1_DocumentLoaded);
}
/// <summary> ///文档变化后,实现对新文件名称的显示/// </summary> void spreadsheetControl1_DocumentLoaded(objectsender, EventArgs e)
{
string fileName = Path.GetFileName(this.spreadsheetControl1.Document.Path);if(String.IsNullOrEmpty(fileName))
{
Text
=mainFormText;
}
else{
Text
= fileName + "-" +mainFormText;
}
}
/// <summary> ///打开Excel文件/// </summary> private void btnOpenFile_Click(objectsender, EventArgs e)
{
string filePath =FileDialogHelper.OpenExcel();if (!string.IsNullOrEmpty(filePath))
{
IWorkbook workbook
=spreadsheetControl1.Document;
workbook.LoadDocument(filePath);
}
}
/// <summary> ///保存Excel文件/// </summary> private void btnSaveFile_Click(objectsender, EventArgs e)
{
spreadsheetControl1.SaveDocument();
}
/// <summary> ///另存为Excel文件/// </summary> private void btnSaveAs_Click(objectsender, EventArgs e)
{
string dir =System.Environment.CurrentDirectory;string filePath = FileDialogHelper.SaveExcel("", dir);if (!string.IsNullOrEmpty(filePath))
{
try{
IWorkbook workbook
=spreadsheetControl1.Document;
workbook.SaveDocument(filePath);

MessageUtil.ShowTips(
"保存成功");
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageUtil.ShowError(ex.Message);
}
}
}
/// <summary> ///Excel文件打印/// </summary> private void btnPreview_Click(objectsender, EventArgs e)
{
this.spreadsheetControl1.ShowPrintPreview();
}
}

预览也很方便,和Word的预览操作类似。

以上就是几个常用文档的显示和操作案例,有了这些我们很容易整合到我们的附件管理里面了。

如我在我的《Winform开发框架》、《混合式开发框架里面》使用的通用附件管理模块,就是基于这些特点,实现图片、Excel文档、Word文档和PDF等文档的在线预览和管理操作,界面截图如下所示。

在较早期的Dev开发中,基本上都是在使用一个DLL包的汉化文件,如基于13.1的汉化包文件Dxper.LocalizationCHS.Win.v13.1.5.dll,这个汉化包也比较方便,大多数时候复制一个文件就搞定了。不过这样的汉化包一般同步更新比较慢,最近使用了DevExpress当前较新版本14.*后,希望集成使用官方的汉化文件,本文介绍在使用官方汉化文件的一些历程和心得,希望对后来者有帮助。

我们知道,如果使用有其他方处理汉化包文件(类似汉化包Dxper.LocalizationCHS.Win.v13.1.5.dll),这样的方式就是复制文件比较方便,如果使用官方汉化包,复制的文件比较多,比较零散。但是使用官方汉化包也有好处,就调用的时候,一句话就可以把界面线程的语言进行切换了,而且打包的时候,也不会有太多的不同。

1、如何在程序中使用官方汉化文件

Dev的官方汉化文件,是一系列的独立资源类,它对应着不同的界面汉化内容,如下界面截图所示。

以上这些文件是全部界面方面的汉化处理,一般情况下,我们不需要全部的内容,只需要我们使用到对应模块的汉化资源就可以了,所以即使是复制,也是复制一部分进行使用。

一般情况下,我们根据需要复制一部分即可。

准备好上面的文件后,我们在程序代码里面,需要增加一句话,指定去对应目录获取汉化资源文件即可。

这样如果我们是直接复制文件去机器上运行,记得把zh-CN目录下的资源文件一并复制过去,运行起来就可以看到Dev控件的中文化菜单或者提示了。

2、如何在安装包里面使用汉化文件

对于这么多汉化文件,看起来虽然比较多,但是,打包的时候,你最终也只是把这些零散的内容整合到安装包里面了,所以没有太多的不一样的地方。

一般情况下,我们在安装包里面,集成汉化文件的时候,就在文件视图里面,把对应的zh-CN目录整个拖动到VS的安装包设计器里面,就可以了,这样所有相关的文件都会加入进来,也是非常方便的。

做好这一步,其他的内容,就是按照常规的方式进行添加或者处理就可以了,对于安装包,有时候通过刷新引用关系,并检查某些文件是否遗漏即可。

以上是基于VS2010的安装包处理,对于VS2012、VS2013以上,他们的做法上有所不同,不过总体的步骤是一致的。

3、DevExpress14的版本使用总结

对于引入Dev14.*的版本,由于它们增加了对PDF的改善支持,因此我们可以直接在附件里面预览PDF文档而没有出现格式出错的情况了(13.*处理PDF的格式会有问题),另外还增强了XtraSpreadSheet的功能,因此也可以使用这个模块来处理Excel的相关内容了,Word照常可以使用RichEdit控件进行预览和处理,不过格式显示依旧有一些失真,有些地方字体显示还是不太好。

不过基于PDF、RichEdit和SpreadSheet的功能,可以使用它们进行很好的应用了。如我在我的《Winform开发框架》、《混合式开发框架里面》使用的通用附件管理模块,就是基于这些特点,实现图片、Excel文档、Word文档和PDF等文档的在线预览和管理操作,界面截图如下所示。

由于引进了一些新的对象,如Range等,这些如果你在Aspose.Cell里面已经使用它来处理报表方面的时候,那么就需要特别声明了,因为它会和DevExpress里面的SpreadSheet里面的对象类产生冲突,一般情况下处理下就可以了。

除了这些,基本上没有其他的冲突问题,DevExpress版本的更新,依旧保持比较的向后兼容的情况。

在做企业电子商务方面,微信小店虽然较淘宝天猫等起步较晚,但是作为一个电商平台,这个影响力不容忽视,结合微信的特点和便利,微信小店具有很好的粘合性和广泛的用户基础,因此花费一定的时间,在这方面做深入的研究和应用,也是我一个感兴趣的领域,本文基于前面微信系列文章的基础上,再对微信小店的内容进行一个系列化的介绍,希望对大家理解和使用上有所帮助,同时也把我自己的微信系列文章推向纵深的领域和方向。

1、微信小店的申请和搭建

微信小店的资质是必须为认证的公众号,并且需要认证后进行独立的申请,提交相关的企业信息和财务相关信息,手续和盖章的文件相对有点繁琐,不过为了好好研究和应用微信小店的功能,这些都没什么了,就按照他们的要求一步步提供相应的材料即可。

通过后,在自己的接口功能列表里面,可以看到对应的接口已经获取到了。

有了这些功能模块,第一步,我们就可以在微信公众号的管理平台上添加对应的商品信息,然后搭建好自己的微信店铺了。

我们进入微信小店,可以看到微信小店的响应功能操作界面。

我们第一步是需要添加对应的商品信息,从分类里面选择适合自己的分类,然后添加对应的商品信息和图片。

最后,我们添加完成了自己的商品列表(包括商品信息和商品分组的处理),完成后,类似的界面如下所示。

为了把商品合理的展示出来,微信小店引入了一个货架的概念,就是把商品分门别类的很好展示给客户,货架就是类似一个布局良好的展柜,我们可以定义不同的货架,然后公布不同的URL进行体验。

构建好货架信息后,我们就可以把货架的URL放到微信的菜单里面去了,这样我们就可以查看到自己的微信小店了。

当然店铺可以下单并进行处理了,客户下了订单,我们可以为微信小店的管理界面上进行订单的发货管理等操作。

在我们发货后,系统会有一个信息提示给对应购买者的微信号,如下所示。

如果需要了解我的微信小店的功能,可以扫描下面二维码进行关注我们的认证公众号:
广州爱奇迪

2、微信小店的对象模型

通过前面第1小节的学习,我们可能已经了解到了大概的微信小店的相关对象模型了,基本上就是包括了,常规的商品、商品分组、货架、库存、订单这些,还有商品分类,商品分类属性、商品分类SKU、快递邮寄模板、图片管理等功能。

为了更有效了解整个对象模型,我绘制了一些图形以帮助了解这些对象模型。

上面的图形,很好地阐述了这些对象的关系,基本上就是,我们第一接触的就是货架管理,通过货架入口,绑定了显示的商品分组和数量,我们就可以看到对应的商品信息,而商品通过图片、分类、属性、SKU等信息构建一个完整的商品对象,另外在货架的入口里面,我们可以对商品进行下订单,因此设计到订单和库存的管理,订单的发货需要有运费模板进行关联,从而实现完整的一个微信店铺的流程处理。

当然其中每个模型都有其对应的API接口,为了更加方便了解微信小店提供的功能接口,我通过图形列出上面涉及到的对象的功能接口,如下所示。

除了商品的对象模型接口,还有下面的一些接口。

3、微信小店API的使用

上面通过图示的方式,介绍了微信小店的相关对象和接口功能,这些我只是根据微信API提供的内容进行了汇总而已。

要了解更加详细的接口说明,我们还是需要参考微信的官方接口说明。

但是通过上面图示的接口功能图,我们可以整理出对应的微信小店的API接口和实现了。

下面的接口类和接口实现类,就是根据上面的分析和微信店铺的文档说明进行整理的C#开发封装处理。

由于接口涉及的内容比较多,因此我通过几个接口进行一定的见解,其他的如此类推。

例如对于微店商品的管理,我定义了下列的接口代码。

    /// <summary>
    ///微小店管理的商品API接口/// </summary>
    public interfaceIMerchantApi
{
#region 商品信息 /// <summary> ///创建商品/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="merchantJson">商品对象</param> AddMerchantResult AddMerchant(stringaccessToken, MerchantJson merchantJson);/// <summary> ///删除商品/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="productId">商品ID</param> CommonResult DeleteMerchant(string accessToken, stringproductId);/// <summary> ///修改商品///product_id表示要更新的商品的ID,其他字段说明请参考增加商品接口。///从未上架的商品所有信息均可修改,否则商品的名称(name)、商品分类(category)、商品属性(property)这三个字段不可修改。/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="merchantJson">修改商品的信息</param> CommonResult UpdateMerchant(stringaccessToken, MerchantJson merchantJson);/// <summary> ///根据ID查询商品信息,如果成功返回MerchantJson信息,否则返回null/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="productId">商品的Id</param> MerchantJson GetMerchant(string accessToken, string productId);

通过有效的封装处理,他们的一些实现代码也很简单,如下所示。

        /// <summary>
        ///创建商品/// </summary>
        /// <param name="accessToken">调用接口凭证</param>
        /// <param name="merchantJson">商品对象</param>
        /// <returns></returns>
        public AddMerchantResult AddMerchant(stringaccessToken, MerchantJson merchantJson)
{
var url = string.Format("https://api.weixin.qq.com/merchant/create?access_token={0}", accessToken);string postData =merchantJson.ToJson();return JsonHelper<AddMerchantResult>.ConvertJson(url, postData);
}
/// <summary> ///删除商品/// </summary> /// <param name="accessToken">调用接口凭证</param> /// <param name="productId">商品ID</param> /// <returns></returns> public CommonResult DeleteMerchant(string accessToken, stringproductId)
{
var url = string.Format("https://api.weixin.qq.com/merchant/del?access_token={0}", accessToken);var data = new{
product_id
=productId
};
string postData =data.ToJson();returnHelper.GetExecuteResult(url, postData);
}

基于文章篇幅的考虑,下面系列文章再进行单独的模型介绍和讲解。

如果对这个《C#开发微信门户及应用》系列感兴趣,可以关注我的其他文章,系列随笔如下所示:

C#开发微信门户及应用(25)-微信企业号的客户端管理功能

C#开发微信门户及应用(24)-微信小店货架信息管理

C#开发微信门户及应用(23)-微信小店商品管理接口的封装和测试

C#开发微信门户及应用(22)-微信小店的开发和使用

C#开发微信门户及应用(21)-微信企业号的消息和事件的接收处理及解密

C#开发微信门户及应用(20)-微信企业号的菜单管理

C#开发微信门户及应用(19)-微信企业号的消息发送(文本、图片、文件、语音、视频、图文消息等)

C#开发微信门户及应用(18)-微信企业号的通讯录管理开发之成员管理

C#开发微信门户及应用(17)-微信企业号的通讯录管理开发之部门管理

C#开发微信门户及应用(16)-微信企业号的配置和使用

C#开发微信门户及应用(15)-微信菜单增加扫一扫、发图片、发地理位置功能

C#开发微信门户及应用(14)-在微信菜单中采用重定向获取用户数据

C#开发微信门户及应用(13)-使用地理位置扩展相关应用

C#开发微信门户及应用(12)-使用语音处理

C#开发微信门户及应用(11)--微信菜单的多种表现方式介绍

C#开发微信门户及应用(10)--在管理系统中同步微信用户分组信息

C#开发微信门户及应用(9)-微信门户菜单管理及提交到微信服务器

C#开发微信门户及应用(8)-微信门户应用管理系统功能介绍

C#开发微信门户及应用(7)-微信多客服功能及开发集成

C#开发微信门户及应用(6)--微信门户菜单的管理操作

C#开发微信门户及应用(5)--用户分组信息管理

C#开发微信门户及应用(4)--关注用户列表及详细信息管理

C#开发微信门户及应用(3)--文本消息和图文消息的应答


C#开发微信门户及应用(2)--微信消息的处理和应答


C#开发微信门户及应用(1)--开始使用微信接口