wenmo8 发布的文章

关于Dapper的介绍,我想很多人都对它有一定的了解,这个类似一个轻型的ORM框架是目前应用非常火的一个东西,据说各方面的性能都不错,而且可以支持多种数据库,在开始介绍这个文章之前,我花了不少功夫来学习了Dapper 的相关使用。Dapper.Contrib是对Dapper的进一步封装,使对象的基本增删改查等操作进一步简化,我做了一个案例使用Dapper.Contrib 开发.net core程序,测试它对多种数据库的处理。

1、Dapper.Contrib的使用

前面介绍过,Dapper.Contrib是对Dapper的进一步封装,使对象的基本增删改查等操作进一步简化。

它主要是通过特性映射的方式实现自定义类和数据库之间的关系处理,如下是实体类的定义信息。

[Table("T_Customer")]public classCustomerInfo
{
[ExplicitKey]
//非自增长的用此标识 public virtual string ID { get; set; }public virtual string Name { get; set; }public virtual int Age { get; set; }public virtual string Creator { get; set; }public virtual DateTime CreateTime { get; set; }

}

Dapper.Contrib的所有实体配置选项

  • Table:指定实体对应地数据库表名,如果类名和数据库表名不同,需要设置(如案例所示)
  • Key:指定此列为自动增长主键
  • ExplicitKey:指定此列为非自动增长主键(例如guid,字符串列)
  • Computed:计算属性,此列不作为更新
  • Write:指定列是否可写

通过定义好实体类和数据库表的映射关系,就可以通过强类型处理相关的接口了,如下所示。

T Get<T>(id);
IEnumerable
<T> GetAll<T>();int Insert<T>(T obj);int Insert<T>(Enumerable<T>list);bool Update<T>(T obj);bool Update<T>(Enumerable<T>list);bool Delete<T>(T obj);bool Delete<T>(Enumerable<T>list);bool DeleteAll<T>();

这样通过映射指定表名或者字段信息后,就可以知道类和表之间的关系,可以封装对应的强类型处理接口了。

2、Dapper.Contrib 开发.net core程序

我们创建一个空白的.net core程序框架后,就在它的基础上做一些Dapper的数据库测试。

首先为了考虑多数据库的处理,我们需要创建一个配置文件,并可以动态配置不同的数据库,配置文件appSettings.json如下所示。

上面我配置了多种数据库的连接字符串,并且通过动态指定节点名称和数据库类型,来实现对项目指向不同数据库的访问。

例如我们准备需要让Dapper支持我们常见的数据库类型,如下定义数据库类型。

    /// <summary>
    ///数据库类型定义/// </summary>
    public enumDatabaseType
{
SqlServer,
//SQLServer数据库 MySql, //Mysql数据库 Npgsql, //PostgreSQL数据库 Oracle, //Oracle数据库 Sqlite, //SQLite数据库 DB2 //IBM DB2数据库 }

对于不同的数据库信息,我们需要根据不同的配置连接字符串,并创建对应的数据库连接对象供Dapper使用,如对于SQLServer的数据库,那么创建的是SqlConnection对象,对于Mysql,创建的是MySqlConnection连接对象,对于PostgreSQL对应的是NpgsqlConnection,以此类推。而Dapper则通过对连接对象的扩展实现了多种数据请求。

对于多数据库的支持,我们需要统一解析配置内容appSetting.json的内容,并返回不同数据库的连接对象,如下是连接工厂的统一处理方式,通过 CreateConnection() 返回配置的连接对象。

    /// <summary>
    ///数据库连接辅助类/// </summary>
    public classConnectionFactory
{
/// <summary> ///转换数据库类型/// </summary> /// <param name="databaseType">数据库类型</param> /// <returns></returns> private static DatabaseType GetDataBaseType(stringdatabaseType)
{
DatabaseType returnValue
=DatabaseType.SqlServer;foreach (DatabaseType dbType in Enum.GetValues(typeof(DatabaseType)))
{
if(dbType.ToString().Equals(databaseType, StringComparison.OrdinalIgnoreCase))
{
returnValue
=dbType;break;
}
}
returnreturnValue;
}
/// <summary> ///获取数据库连接/// </summary> /// <returns></returns> public staticIDbConnection CreateConnection()
{
IDbConnection connection
= null;//获取配置进行转换 var type = AppConfig.GetConfig("ComponentDbType");var dbType =GetDataBaseType(type);//DefaultDatabase 根据这个配置项获取对应连接字符串 var database = AppConfig.GetConfig("DefaultDatabase");if (string.IsNullOrEmpty(database))
{
database
= "sqlserver";//默认配置 }var strConn =AppConfig.Configuration.GetConnectionString(database);switch(dbType)
{
caseDatabaseType.SqlServer:
connection
= newSystem.Data.SqlClient.SqlConnection(strConn);break;caseDatabaseType.MySql:
connection
= newMySql.Data.MySqlClient.MySqlConnection(strConn);break;caseDatabaseType.Npgsql:
connection
= newNpgsql.NpgsqlConnection(strConn);break;caseDatabaseType.Sqlite:
connection
= newSQLiteConnection(strConn);break;caseDatabaseType.Oracle:
connection
= newOracle.ManagedDataAccess.Client.OracleConnection(strConn);//connection = new System.Data.OracleClient.OracleConnection(strConn); break;caseDatabaseType.DB2://connection = new System.Data.OleDb.OleDbConnection(strConn); break;
}
returnconnection;
}
}

有了数据库对象工厂,我们的配置就可以动态化了。

下面我们来看看,获得这些连接对象后,如何通过Dapper.Contrib来获取对应的对象了,下面的类是常规的对数据库信息的处理,包括常规的增删改查等基础接口。

    /// <summary>
    ///常规的数据访问层/// </summary>
    public classCustomer
{
publicIDbConnection Connection
{
get{var connection =ConnectionFactory.CreateConnection();
connection.Open();
returnconnection;
}
}
public IEnumerable<CustomerInfo>GetAll()
{
using (IDbConnection dbConnection =Connection)
{
return dbConnection.GetAll<CustomerInfo>();//return dbConnection.Query<CustomerInfo>("SELECT * FROM T_Customer"); }
}
public CustomerInfo FindByID(stringid)
{
using (IDbConnection dbConnection =Connection)
{
return dbConnection.Get<CustomerInfo>(id);//string query = "SELECT * FROM T_Customer WHERE ID = @Id";//return dbConnection.Query<CustomerInfo>(query, new { Id = id }).FirstOrDefault(); }
}
public boolInsert(CustomerInfo info)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
= dbConnection.Insert(info) > 0;
result
= true;//string query = "INSERT INTO T_Customer (ID, Name, Age, Creator, CreateTime)"//+ " VALUES(@ID, @Name, @Age, @Creator, @CreateTime)";//result = dbConnection.Execute(query, info) > 0; }returnresult;
}
public boolUpdate(CustomerInfo prod)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
=dbConnection.Update(prod);//string query = "UPDATE T_Customer SET Name = @Name,"//+ " Age = @Age, Creator= @Creator, CreateTime=@CreateTime"//+ " WHERE ID = @ID";//result = dbConnection.Execute(query, prod) > 0; }returnresult;
}
public bool Delete(stringid)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
= dbConnection.Delete(new CustomerInfo { ID =id });//string query = "DELETE FROM T_Customer WHERE ID = @Id";//result = dbConnection.Execute(query, new { ID = id }) > 0; }returnresult;
}
public boolDeleteAll()
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
= dbConnection.DeleteAll<CustomerInfo>();//string query = "DELETE FROM T_Customer WHERE ID = @Id";//result = dbConnection.Execute(query, new { ID = id }) > 0; }returnresult;
}
}

其中的备注部分的代码是等同于上面的执行代码的,是Dapper 的SQL版本的一种处理方式。

我们看到,对于Customer表来说,使用对象的接口处理,我们已经隔离了很多硬编码的SQL处理,不过我们还可以对它进行进一步的优化处理。

我们定义一个通用的BaseDAL来剥离常规的增删改查处理,并且把同步和异步的操作分来两个文件来管理,同步处理的基类如下代码所示。

    /// <summary>
    ///数据库访问基类/// </summary>
    /// <typeparam name="T">实体类类型</typeparam>
    public partial class BaseDAL<T> where T : class{/// <summary>
        ///对象的表名/// </summary>
        public string TableName { get; set; }/// <summary>
        ///主键属性对象/// </summary>
        public PropertyInfo PrimaryKey { get; set; }publicBaseDAL()
{
this.TableName = EntityHelper.GetTableName(typeof(T));this.PrimaryKey = EntityHelper.GetSingleKey<T>();
}
/// <summary> ///数据库连接/// </summary> protectedIDbConnection Connection
{
get{var connection =ConnectionFactory.CreateConnection();
connection.Open();
returnconnection;
}
}
/// <summary> ///返回数据库所有的对象集合/// </summary> /// <returns></returns> public IEnumerable<T>GetAll()
{
using (IDbConnection dbConnection =Connection)
{
return dbConnection.GetAll<T>();
}
}
/// <summary> ///查询数据库,返回指定ID的对象/// </summary> /// <param name="id">主键的值</param> /// <returns></returns> public T FindByID(objectid)
{
using (IDbConnection dbConnection =Connection)
{
return dbConnection.Get<T>(id);
}
}
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public boolInsert(T info)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
dbConnection.Insert(info);
result
= true;
}
returnresult;
}
/// <summary> ///插入指定对象集合到数据库中/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public bool Insert(IEnumerable<T>list)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
= dbConnection.Insert(list) > 0;
}
returnresult;
}
/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public boolUpdate(T info)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
=dbConnection.Update(info);
}
returnresult;
}
/// <summary> ///更新指定对象集合到数据库中/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public bool Update(IEnumerable<T>list)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
=dbConnection.Update(list);
}
returnresult;
}
/// <summary> ///从数据库中删除指定对象/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public boolDelete(T info)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
=dbConnection.Delete(info);
}
returnresult;
}
/// <summary> ///从数据库中删除指定对象集合/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public bool Delete(IEnumerable<T>list)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
=dbConnection.Delete(list);
}
returnresult;
}
/// <summary> ///根据指定对象的ID,从数据库中删除指定对象/// </summary> /// <param name="id">对象的ID</param> /// <returns></returns> public bool Delete(objectid)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
string query = string.Format("DELETE FROM {0} WHERE {1} = @id", TableName, PrimaryKey.Name);var parameters = newDynamicParameters();
parameters.Add(
"@id", id);

result
= dbConnection.Execute(query, parameters) > 0;
}
returnresult;
}
/// <summary> ///从数据库中删除所有对象/// </summary> /// <returns></returns> public boolDeleteAll()
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
result
= dbConnection.DeleteAll<T>();
}
returnresult;
}

}

异步类的代码如下所示。

    /// <summary>
    ///数据库访问基类/// </summary>
    /// <typeparam name="T">实体类类型</typeparam>
    public partial class BaseDAL<T> where T : class{/// <summary>
        ///返回数据库所有的对象集合/// </summary>
        /// <returns></returns>
        public virtual async Task<IEnumerable<T>>GetAllAsync()
{
using (IDbConnection dbConnection =Connection)
{
return await dbConnection.GetAllAsync<T>();
}
}
/// <summary> ///查询数据库,返回指定ID的对象/// </summary> /// <param name="id">主键的值</param> /// <returns></returns> public virtual async Task<T> FindByIDAsync(objectid)
{
using (IDbConnection dbConnection =Connection)
{
return await dbConnection.GetAsync<T>(id);
}
}
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public virtual async Task<bool>InsertAsync(T info)
{
bool result = false;using (IDbConnection dbConnection =Connection)
{
awaitdbConnection.InsertAsync(info);
result
= true;
}
return await Task<bool>.FromResult(result);
}
/// <summary> ///插入指定对象集合到数据库中/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public virtual async Task<bool> InsertAsync(IEnumerable<T>list)
{
using (IDbConnection dbConnection =Connection)
{
return await dbConnection.InsertAsync(list) > 0;
}
}
/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public virtual async Task<bool>UpdateAsync(T info)
{
using (IDbConnection dbConnection =Connection)
{
return awaitdbConnection.UpdateAsync(info);
}
}
/// <summary> ///更新指定对象集合到数据库中/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public virtual async Task<bool> UpdateAsync(IEnumerable<T>list)
{
using (IDbConnection dbConnection =Connection)
{
return awaitdbConnection.UpdateAsync(list);
}
}
/// <summary> ///从数据库中删除指定对象/// </summary> /// <param name="info">指定的对象</param> /// <returns></returns> public virtual async Task<bool>DeleteAsync(T info)
{
using (IDbConnection dbConnection =Connection)
{
return awaitdbConnection.DeleteAsync(info);
}
}
/// <summary> ///从数据库中删除指定对象集合/// </summary> /// <param name="list">指定的对象集合</param> /// <returns></returns> public virtual async Task<bool> DeleteAsync(IEnumerable<T>list)
{
using (IDbConnection dbConnection =Connection)
{
return awaitdbConnection.DeleteAsync(list);
}
}
/// <summary> ///根据指定对象的ID,从数据库中删除指定对象/// </summary> /// <param name="id">对象的ID</param> /// <returns></returns> public virtual async Task<bool> DeleteAsync(objectid)
{
using (IDbConnection dbConnection =Connection)
{
string query = string.Format("DELETE FROM {0} WHERE {1} = @id", TableName, PrimaryKey.Name);var parameters = newDynamicParameters();
parameters.Add(
"@id", id);return await dbConnection.ExecuteAsync(query, parameters) > 0;
}
}
/// <summary> ///从数据库中删除所有对象/// </summary> /// <returns></returns> public virtual async Task<bool>DeleteAllAsync()
{
using (IDbConnection dbConnection =Connection)
{
return await dbConnection.DeleteAllAsync<T>();
}
}
}

这样,我们如果需要增加一个如客户信息表的管理类,就很简单的继承基类就可以了,代码很少,但是增删改查接口一个也少不了。

    /// <summary>
    ///继承基类对象管理/// </summary>
    public class CustomerDAL :BaseDAL<CustomerInfo>{
}

为了测试一下数据访问层的处理接口,我创建了一个.net core的控制台程序进行测试,如下项目视图所示。

主要目的是确认数据处理的效果。

我们在Program.cs类里面增加相关的测试代码,为了简便和处理效果没有用UnitTest处理。

            //创建管理对象,并测试接口
            var customer = newCustomerDAL();var list =customer.GetAll();foreach (var item inlist)
{
Console.WriteLine(item.ToJson());
var info =customer.FindByID(item.ID);
Console.WriteLine(info.ToJson());
Console.WriteLine();
}
//插入记录 var insertInfo = new CustomerInfo() { Name = "test", Age = 30, Creator = "test"};var insertList = new List<CustomerInfo>() { insertInfo };var flag =customer.Insert(insertList);
Console.WriteLine(
"插入操作" + (flag ? "成功" : "失败"));

Console.WriteLine(
"插入的新内容");
insertInfo
=customer.FindByID(insertInfo.ID);
Console.WriteLine(insertInfo.ToJson());

Console.WriteLine(
"更新内容");
insertInfo.Name
= "Test" +DateTime.Now.ToShortDateString();
flag
=customer.Update(insertInfo);
Console.WriteLine(
"更新操作" + (flag ? "成功" : "失败"));

Console.WriteLine(
"更新的新内容");
insertInfo
=customer.FindByID(insertInfo.ID);
Console.WriteLine(insertInfo.ToJson());

Console.WriteLine(
"删除内容");
flag
=customer.Delete(insertInfo.ID);
Console.WriteLine(
"删除操作" + (flag ? "成功" : "失败"));


Console.WriteLine(
"所有内容");
list
=customer.GetAll();foreach (var item inlist)
{
Console.WriteLine(item.ToJson());
Console.WriteLine();
}

Console.ReadLine();

测试Mysql、SQLite数据库同样没有问题

Mysql配置信息如下

处理的Mysql记录信息如下。

SQLite配置信息如下

处理SQLite数据信息如下

而在处理PostgreSQL的信息(配置节点npgsql里面)的时候,查询的主键好像和大小写有关系,导致插入记录出错。

而Oracle我采用的是Oracle.ManagedDataAccess.Core进行访问,由于我本地Oracle数据库侦听处理有点问题,因此没有测试成功,暂不予置评。

而对于数据库的支持问题,导致我重新审核一下是否采用Dapper.Contrib还是其他Dapper方式来构建数据库访问基类的问题,我需要兼容多种数据库的信息,并且能够尽可能的封装常规的增删改查等操作,其中目前的基类还没有加入更加复杂的查询操作,分页操作等功能,在解决这些困惑问题,才会继续考虑把底层支持的接口全部完善。

在开发查看流程表单明细的时候,在Web界面中,我们往往通过使用@RenderPage实现页面内容模块化的隔离,减少复杂度,因此把一些常用的如审批、撤销、会签、阅办等等的流程步骤都放到了通用处理的页面ViewDetail.cshtml里面实现了。那么如果在一些特殊的表单处理过程中,我们在审批完成后需要触发一个事件来修改一些业务表的数据的处理的话,就会变得麻烦起来,通用的处理就代表要损失一部分灵活性,特殊性。那是否可以通过一些处理,能够实现这些特殊更新的操作呢?本篇随笔介绍的就是这个方面,实现流程完成后更新资料状态处理。

1、页面的模块关系

之前我在随笔《
在Bootstrap开发框架的前端视图中使用@RenderPage实现页面内容模块化的隔离,减少复杂度
》里面已经介绍过,对于这种有着共同内容的页面,我们可以把它提取出来,然后在主页面里面使用@RenderPage的方式进行包含进来,这样有助于我们模块化的处理。

上面通用的处理内容,全部在Apply/ViewDetail.cshtml页面里面。

它的页面内容,主要是封装一些常规的界面和处理脚本。

也就是对于一些如资产领用或者资产处置的界面,它都是包含这些基础流程显示的界面和处理逻辑的。

查看具体流程表单明细的界面如下所示。

或者资产领用表单的查看界面如下所示。

2、脚本的模板化处理,实现子类函数的差异性

在查看信息的顶部有对应的流程处理按钮,如果用户是流程参与人,那么可以执行相关的处理。

审批是一个通用的常规处理操作,弹出界面需要录入处理意见,以及下一步流程的处理人员,如下界面所示。

既然我们需要在一些业务表单结束后,触发一些后处理的事件来实现我们所需要数据的更新操作,那么我们可以通过使用Javascript的模板化方法的设计模式来实现我们需要的处理。

首先我们在通用处理的模块页面里面,定义好我们需要的函数,可以保留它不做任何事情,仅仅定义一个空壳函数就可以,不过需要在调用它的地方加上这个函数即可,而差异处理的不同,则让子类页面重载它的函数就可以实现差异性的模板方法模式。

例如我们在Apply/ViewDetail.cshtml页面,这个通用页面是相当于基类界面了,定义它的函数处理如下所示。

    //审批处理后的执行操作
    //该操作一般发生在申请单完成后,需要修改业务表单数据的情况
    //该函数默认为空,具体逻辑给子视图页面ViewDetail.cs里面实现
    functionToCompletedForm() {
console.log(
"ToCompletedForm");//留待子类重写函数 }

而在可能需要进行后续处理的地方,调用这个方法,以完成它的调用逻辑。

以上就是处理流程的时候,增加上这个函数处理方法的调用,其他地方如果是类似的场景,则类似处理即可。

然后,我们不同的子类页面,重写好自己的处理函数就可以覆盖基类的函数ToCompletedForm的处理逻辑了。

以上是资产领用流程的一个处理步骤,其中主要就是在流程结束的时候,完成一些数据更新和状态的处理操作。

以上就是对于表单完成后的一些特殊处理的实现方法,在流程结束的时刻,我们可以做更多的相关后台处理逻辑,都可以这样统一在一个后台方法中处理。

这样通过这种方式实现的逻辑,不影响页面的关系和处理过程,仅仅是我们在子页面里面重写一下处理规则即可,如果不需要,也可以不重写,不影响整个处理过程。

在开发项目的时候,我们为了提高速度和质量,往往不是白手起家,需要基于一定的基础上进行项目的快速开发,这样可以利用整个框架的生态基础模块,以及成熟统一的开发方式,可以极大提高我们开发的效率。本篇随笔就是介绍基于Winform开发框架或混合框架基础上进行项目的快速开发,这个主题在之前的一些随笔有一定的介绍,但是觉得都不是很全面、完整,因此本篇随笔通过一些简单的例子以及一个具体的项目案例来进行全流程的开发出来,希望读者对基于我们开发框架的开发模式有一个更深入的了解。

1、基于框架开发简单界面的过程

由于我们框架提供了很多基础的模块来给我们新业务开发提供便利,如公用类库、分页控件、字典管理、权限管理系统、自动升级、附件管理、人员管理、工作流管理等可以反复重用的模块。

有了这些模块的加持,我们开发项目很多基础的处理就不用所有的东西都从头来过。

对于一个新建的业务表,我们需要开发的需要底层的实现和界面层的展示,这些工作量也是非常巨大的,如果基于控件细粒度的处理,也是非常繁琐的工作,因此基于这些开发过程的考虑,我们引入了提高效率开发的代码生成工具Database2Sharp,专门为我们基于开发框架基础上的框架实现代码开发,和业务界面展示的快速开发。

代码生成工具,不仅能够让它生成我们常规开发的界面层以下的实现代码(包括BLL、DAL、Entity、IDAL等层,以及混合框架的WCF、Web API的实现层和调用封装层),以及界面层的调用代码。

有了这些的处理,我们可极大减轻工作量。

我们开发新业务表的处理没有发生变化,需要设计好自己的业务表,然后利用代码生成工具生成对应代码,最后合并到项目里面并调整界面为合适的展示方式。

我在之前的随笔《
循序渐进开发WinForm项目(3)--Winform界面层的项目设计
》里面简单的介绍了一般简单界面的处理过程。

我们首先需要使用PowerDesigner或者同类工具进行表的建模,如下所示。

然后使用代码生成工具生成框架代码,如Winform框架生成下面项目框架代码。

这个分层,在Web项目或者WInform项目(包括WPF项目)这些分层都是可以重用的,这样我们就不用重复处理界面一下的逻辑,针对性的开发我们需要的界面层即可。

DAL层根据不同的需要,扩展支持不同的数据库类型,每个数据库类型,对应一个数据库访问实现层即可,它们实现IDAL层的接口,称之为数据库访问接口实现层。

然后接着在代码生成工具中单击“Windows界面代码生成”后,生成的窗体界面文件如下所示,分为了两个部分,一个是列表界面,一个是数据编辑界面。

然后把相关的界面内容复制到刚才创建的空项目工程上,最后界面层的项目截图如下所示。

生成的界面打开就具有了常规的列表和数据编辑展示的界面,如下所示。

列表界面包括了查询、新建、导入、导出,以及分页控件的整合处理,分页控件里面整合支持了各种右键操作,包括增加、编辑、删除,以及各种调整设置功能。

搞定列表界面,下面我们再来看看数据的编辑界面部分,数据编辑界面生成的效果如下所示(由于版面的原因,我把它进行了一定的缩小)。

我们看到,里面的字段说明,控件的名称等内容,都已经合理安排好了,基本上设计数据库的时候,指定的字段备注就能正确生成出来了。

这里演示的表字段比较少,因此可能排版布局方面没有很好的演示效果,不过我们知道DevExpress本身的LayoutControl布局很强大,我们可以随意调整里面控件的位置。

对于控件的类型,也可以进行不同类型的转换,如可以吧文本类型,转换为数值类型的输入控件等等。

如果对于复杂的界面,我们需要进行一定的合并拖动处理,以实现我们所需要的界面效果,如下所示。

对于界面层大量的处理操作,我们后台的代码基本上不需要调整,从而减少我们出错和降低效率的问题。

以上就是我们在做一个简单界面的过程中,一些开发思路过程。

2、实际的项目开发过程

以上介绍过程中,基本上设计到了核心的开发流程了,而我们实际的项目开发过程中,可能还需要涉及到更细节的一些过程。

1)基础项目的生成

首先我们可能需要很多字段在一个表里面,需要设计到很多业务表,那么也就是工作量的问题,和前面不会有本质的差异。

不管我们业务中涉及有多少个表,只要我们定义好表和它的备注信息,一切后面处理起来都会是那么的惬意的了。

生成的项目中,我们已经有了对应框架支持的实现层了。

而界面层,我们可以考虑使用框架基于插件模式的动态加载配置菜单的启动模块项目,如下所示。

对于Winform的界面项目,我们一般建议将我们的业务独立一个整体的项目,并包含所需要的引用文件,这个在利用代码生成工具生成代码的时候,就会有一个空白的UI项目可供使用的。

如对于普通的WInform项目,可以看到它的项目效果大概如下所示。

我们这里实际的项目使用混合框架进行处理的,因此界面的内容比这个复杂一点,不过也是一个独立的界面项目。

我们看到整个UI的界面部分是放在了Client目录下面,我在下面进行了分门别类的放置一些目录,区分不同的界面部分,方便寻找和处理的。

其他的Core层则是使用引用现有文件路径的方式进行包含文件,目的是减少DLL的依赖。

对于混合框架部分,我们这里需要多阐述一些项目,包括Web API的项目(或者WCF的项目),以及对Web API的调用封装处理。

Web API项目我们可以从框架案例中把基础的Web API项目拷贝过来使用,然后增加自己业务的Web API控制器就可以了。

对于新增业务表的Web API控制器代码,也可以利用代码生成工具进行生成。

生成后的控制器代码,我们整合到主体的Web API项目中就可以使用了(对于WCF项目,一样的方式,不在赘述)。

控制器里面,基本上就是封装对常规BLL层的调用,不过Web API的接口都需要token参数,或者一些特殊的接口需要签名信息(如登陆的接口等)

关于Web API的封装调用介绍,这里不准备详细进行介绍,有兴趣可以了解下我的其他随笔《
Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解
》、《
Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用
》、《
Web API应用架构设计分析(1)
》、《
Web API应用架构设计分析(2)
》、《
Web API接口设计经验总结
》、《
Winform混合式开发框架访问Web API接口的处理
》、《
Web API项目中使用Area对业务进行分类管理
》,还是蛮多的,呵呵。

原则上,我们有了启动界面项目WHC.Framework.StarterDx2,有了基于插件模块界面项目的WHC.HospitalDeptMis.ClientDx,以及一些框架的支持模块,那么我们就基本上可以跑起来了。

如果编译没问题,顺利的话,我们登陆后可以看到界面如下所示。

我们没有看到任何业务的菜单,那些在顶端的系统管理菜单是固定的菜单项,我们应该如何才能显示我们需要的菜单项呢?

2)菜单配置

我们根据我们实际的需要,创建业务界面的菜单,我们在权限管理系统的菜单管理界面里面,新建我们的菜单信息。

上面的WInform窗体类型,就是用于定位我们的窗体对象类的,前面部分为类的全名,后者为DLL的文件名称,中间用逗号分隔。

一般菜单最好分为三层,便于区分展示,如下界面所示

定义好菜单后,我们需要为对应的角色分配对应的菜单资源,这样用户登录系统后才能看到对应的菜单,如下是角色分配菜单的界面。

登录系统界面,可以看到菜单信息的展示了。

3)权限的配置

前面介绍了菜单的创建和配置,其中角色包含菜单资源也可以看做是菜单权限的配置,不过我们知道,系统还是有很多功能操作需要进行控制的,如新增、编辑、删除,导出、导入等,这些是如何实现权限的控制的呢?

这些依然是在权限系统里面进行控制管理的,首先我们需要定义需要控制的功能点,如下界面所示,可以批量录入某个业务的相关处理操作。

最后我们可以看到展开的功能点

定义好这些功能点后,我们可以为对应的角色分配对应的功能点权限,和前面的菜单权限分配类似。

做完这些,剩下最后一步就是需要在界面中绑定权限控制逻辑,我们来看看列表界面和详细界面如何进行控制的。

列表界面的权限控制处理如下:

编辑详细界面权限控制处理如下所示。

有了界面模块的权限通用处理,以及权限管理系统的信息配置,那么整个系统跑起来就能够实现我们对菜单的控制,以及对系列度的功能点进行控制处理的了。

这个是在实际开发过程中,我们需要注意的处理步骤。

在开发Winform程序界面的时候,我们往往会使用一些较好看的图表,以便能够为我们的程序界面增色,良好的图标设置可以让界面看起来更加美观舒服,而且也比较容易理解,图标我们可以通过一些网站获取各种场景的图标资源,不过本篇随笔主要介绍如何利用DevExpress的内置图标资源来实现界面图标的设置。

1、设计时刻的图标处理

丰富的图标处理,在菜单、工具栏、树列表等地方,以及按钮等地方,都可以使用,而这些我们可以利用DevExpress的内置图标选择来减轻我们寻找合适图标的烦恼。

一些按钮、工具栏等的图标设置,一般是固定的,我们往往可以在设计时刻就指定它,这样我们可以使用本地的图标,也可以使用DevExpress的内置图标。而使用DevExpress内置图标资源的时候,我们可以调出DevExpress的内置图标选择框的。

如下是按钮添加图标方式,操作非常简单,在按钮的右上角小图标上单击一下进入编辑界面,如下所示。

然后选择Image按钮,进入图标选择界面,选择内置的DevExpress图标库即可,基本上,只要是DevExpress的原生控件,那么就可以通过这种内置图标的对话框进行图标选择,非常方便。

2、运行时刻的图标处理

上面的操作是在设计时候,DevExpress设计器给我们提供很多便利选择内置图标,而在界面运行时刻,想动态处理界面按钮图标,或者树形菜单的图标的时候,就没有这个直接的接口来设置图标了,而我们框架的菜单往往都是需用动态增加的,因此图标的设置也是在运行时刻的。如下面的树列表中,图标就是动态指定的。

这些动态的树形菜单,是在权限系统里面动态配置的,菜单的配置界面如下所示。

上面的选择图图标就是我们需要动态设置的图标,由于图标资源我们是以图片形式存储在对应的记录里面的,因此使用起来也是比较方便的,我们在配置的时候,获取到对应的图标资源并存储起来即可。

除了上面可以参考从DevExpress内置图标资源获取图标的方式外

我们还可以选择我们自己喜欢的图标资源,也就是从系统图标文件中选择自己喜欢的,如下界面所示。

因此我考虑在运行时刻整合两种不同选择图标的方式。

我们先来看看我整合后的图表选择界面,如下所示,包含了运行时刻提取DevExpress内置图标的功能和从系统文件中选择图标的功能。

3、运行时刻提取DevExpress内置图标的功能实现

首先我们参考设计时刻的界面展示

来设计一个界面来展示图标信息

参考原版的界面,设计尽可能贴近即可,另外我们自己加入一个从系统选择图标资源的操作。

至于图标选中后我们返回对应的Image对象给调用者,则通过事件进行处理,以便选中后,即使更新显示效果。

如下所示,我们定义一个委托和事件。

    /// <summary>
    ///DevExpress图标和系统图标选择窗体/// </summary>
    public partial classFrmImageGallery : BaseForm
{
/// <summary> ///自定义一个委托处理图标选择/// </summary> public delegate void IconSelectHandlerDelegate(Image image, stringname);/// <summary> ///图标选择的事件/// </summary> public eventIconSelectHandlerDelegate OnIconSelected;private DXImageGalleryLoader loader = null;publicFrmImageGallery()
{
InitializeComponent();

InitDictItem();
//初始化 }/// <summary> ///处理图标选择的事件触发/// </summary> public virtual void ProcessIconSelected(Image image, stringname)
{
if (OnIconSelected != null)
{
OnIconSelected(image, name);
}
}

然后在内置图标显示中,如果触发图标的单击,我们就触发事件,以便让调用者更新界面显示,如下代码所示。

foreach (GalleryItem item initems[key])
{
item.ItemClick
+= (s, e) =>{//选择处理 ProcessIconSelected(item.ImageOptions.Image, item.Description);
};
}

而对于从系统文件加载文件进行显示图标的,类似的触发方式。

        /// <summary>
        ///从系统资源中加载图标文件,然后触发事件进行显示/// </summary>
        private void txtFilePath_Properties_ButtonClick(objectsender, ButtonPressedEventArgs e)
{
string file =GetIconPath();if (!string.IsNullOrEmpty(file))
{
this.txtFilePath.Text = file;//记录文件名 this.txtEmbedIcon.Image = LoadIcon(file);//显示图片 this.txtEmbedIcon.Size = new System.Drawing.Size(64, 64);//返回处理 ProcessIconSelected(this.txtEmbedIcon.Image, file);
}
}

这样我们在菜单的选择图标的时候,就可以触发事件进行获取图表并更新自身了。

        private void btnSelectIcon_Click(objectsender, EventArgs e)
{
FrmImageGallery dlg
= newFrmImageGallery();
dlg.OnIconSelected
+= (image, name) =>{this.txtEmbedIcon.Image =image;
};
dlg.ShowDialog();
}

完成了这些处理,我们再次将焦点放在如何提取并展示DevExpress内置图标的身上。

为了获取图表资源里面的分类及大小等信息,我们需要把图标资源进行一个加载出来,然后读取里面的类别和大小、集合等信息。先定义几个变量来承载这些信息。

        /// <summary>
        ///图标分类/// </summary>
        public List<string> Categories { get; set; }/// <summary>
        ///图标集合/// </summary>
        public List<string> Collection { get; set; }/// <summary>
        ///图标尺寸/// </summary>
        public List<string> Size { get; set; }

我们知道,DevExpress的图标资源在程序集DevExpress.Utils.DxImageAssemblyUtil.ImageAssembly里面,因此我们需要对它进行读取,并依次对各个资源进行处理。

我们来看看具体的处理代码,如下所示。

            using (System.Resources.ResourceReader reader =GetResourceReader(DevExpress.Utils.DxImageAssemblyUtil.ImageAssembly))
{
System.Collections.IDictionaryEnumerator dict
=reader.GetEnumerator();while(dict.MoveNext())
{
string key = (string)dict.Key as string;if (!DevExpress.Utils.DxImageAssemblyUtil.ImageProvider.IsBrowsable(key)) continue;if (key.EndsWith(".png", StringComparison.Ordinal))
{
string reg = @"(?<collection>\S*?)/(?<category>\S*?)/(?<name>\S*)";var collectionItem = CRegex.GetText(key, reg, "collection");var categoryItem = CRegex.GetText(key, reg, "category");string sizeReg = @"_(?<size>\S*)\.";var sizeItem = CRegex.GetText(key, sizeReg, "size");if (!this.Collection.Contains(collectionItem))
{
this.Collection.Add(collectionItem);
}
if (!this.Categories.Contains(categoryItem))
{
this.Categories.Add(categoryItem);
}
if (!this.Size.Contains(sizeItem))
{
this.Size.Add(sizeItem);
}

Image image
=GetImageFromStream((System.IO.Stream)dict.Value);if (image != null)
{
var item = newDevExpress.XtraBars.Ribbon.GalleryItem(image, key, key);if (!ImageCollection.ContainsKey(key))
{
ImageCollection.Add(key, item);
}
}
}
}
}

其中读取资源的操作代码是

GetResourceReader(DevExpress.Utils.DxImageAssemblyUtil.ImageAssembly)

这个代码它就是从资源里面进行获取对应的图表资源。

        privateSystem.Resources.ResourceReader GetResourceReader(System.Reflection.Assembly imagesAssembly)
{
var resources =imagesAssembly.GetManifestResourceNames();var imageResources = Array.FindAll(resources, resourceName => resourceName.EndsWith(".resources"));if (imageResources.Length != 1)
{
throw new Exception("读取异常");
}
return new System.Resources.ResourceReader(imagesAssembly.GetManifestResourceStream(imageResources[0]));
}

另外,我们根据图表的文件名结构,我们通过正则表达式来读取它的对应信息,然后把它的大小、类别、集合信息存储起来。

    string reg = @"(?<collection>\S*?)/(?<category>\S*?)/(?<name>\S*)";var collectionItem = CRegex.GetText(key, reg, "collection");var categoryItem = CRegex.GetText(key, reg, "category");string sizeReg = @"_(?<size>\S*)\.";var sizeItem = CRegex.GetText(key, sizeReg, "size");

图表信息读取了,我们需要解析它然后存储起来,把图标的Image对象放在一个字典类别里面,方便按照组别进行展示。

    Image image =GetImageFromStream((System.IO.Stream)dict.Value);if (image != null)
{
var item = newDevExpress.XtraBars.Ribbon.GalleryItem(image, key, key);if (!ImageCollection.ContainsKey(key))
{
ImageCollection.Add(key, item);
}
}

有了这些资源,我们对它们进行搜索就显得很方便了,我们如果需要根据文件名或者其他条件进行查询集合的数据,提供一个通用的方法即可,如下代码所示。

        /// <summary>
        ///根据条件获取集合/// </summary>
        /// <returns></returns>
        public Dictionary<string, GalleryItemCollection> Search(List<string> collection, List<string>categories, 
List
<string> size, string fileName = "")
{
Dictionary
<string, GalleryItemCollection> dict = new Dictionary<string, GalleryItemCollection>();

GalleryItemCollection list
= newGalleryItemCollection();foreach (var key inImageCollection.Keys)
{
//使用正则表达式获取图标文件名中的集合、类别、大小等信息 string reg = @"(?<collection>\S*?)/(?<category>\S*?)/(?<name>\S*)";var collectionItem = CRegex.GetText(key, reg, "collection");var categoryItem = CRegex.GetText(key, reg, "category");string sizeReg = @"_(?<size>\S*)\.";var sizeItem = CRegex.GetText(key, sizeReg, "size");//如果是查询处理,把记录放到查询结果里面 if (!string.IsNullOrEmpty(fileName))
{
if(key.Contains(fileName))
{
list.Add(ImageCollection[key]);
}
dict[
"查询结果"] =list;
}
else{//如果是集合和列表中包含的,把它们按类别添加到字典里面 if (collection.Contains(collectionItem) &&categories.Contains(categoryItem)&&size.Contains(sizeItem))
{
if (!dict.ContainsKey(categoryItem))
{
GalleryItemCollection cateList
= newGalleryItemCollection();
cateList.Add(ImageCollection[key]);
dict[categoryItem]
=cateList;
}
else{
GalleryItemCollection cateList
=dict[categoryItem];
cateList.Add(ImageCollection[key]);
}
}
}
}
returndict;
}

这次搜索就直接基于已有的集合ImageCollection 进行搜索的了,不用再次读取程序集并依次分析它,速度提供不少的。

由于图表资源的处理是比较耗时的,我们把整个图标加载的类作为一个静态的对象缓存起来,这样下次使用直接从缓存里面拿,对应的资源也不用重新加载,更好的提高我们重用的效果了,体验更好了。

    /// <summary>
    ///图标库加载处理/// </summary>
    public classDXImageGalleryLoader
{
/// <summary> ///图标字典类别集合/// </summary> public Dictionary<string, GalleryItem> ImageCollection { get; set; }/// <summary> ///图标分类/// </summary> public List<string> Categories { get; set; }/// <summary> ///图标集合/// </summary> public List<string> Collection { get; set; }/// <summary> ///图标尺寸/// </summary> public List<string> Size { get; set; }/// <summary> ///使用缓存处理,获得对象实例/// </summary> public staticDXImageGalleryLoader Default
{
get{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string keyName = string.Format("{0}-{1}", method.DeclaringType.FullName, method.Name);var result = MemoryCacheHelper.GetCacheItem<DXImageGalleryLoader>(keyName,delegate () { return newDXImageGalleryLoader().LoadData(); },new TimeSpan(0, 30, 0));//30分钟过期 returnresult;
}
}

以上代码通过

public static DXImageGalleryLoader Default

定义了一个静态的实例属性,这样这个 DXImageGalleryLoader 实例只会在程序第一次使用的时候构建并加载图片资源,后续都是从缓存里面读取,提高响应速度的同时,也会记住上次的选择界面内容。

以上就是整个功能的处理思路,以及一步步的优化处理,以便实现功能展示的同时,也提高响应速度,最终界面就是我们开始的时候介绍的那样。

单击或者选中系统图标后, 需要设置的按钮或者界面,就会及时更新图标展示,体验效果还是非常不错的。

由于这个界面功能的通用性,我把它作为系统界面基础模块,放到了我的框架BaseUIDx里面,各个系统模块都可以调用了。

在我们开发Winform界面的时候,往往需要绑定数据字典操作,也就是绑定一些下拉列表或者一些列表显示等,以便我们方便选择数据操作,常见的字典绑定操作就是对下拉列表的处理,本篇随笔是基于DevExpress界面的一些处理操作,原理也适用于常规Winform界面或者DotNetBar控件界面处理。另外对于缓存的处理,一般在基于单机版数据或者局域网API接口处理的字典绑定,速度是比较快的,基本上可以不用考虑缓存的处理,但是对于基于互联网API接口的数据处理,往往受限于带宽等原因,请求数据的速度没有那么快,那么需要做好数据缓存处理,才可能更好的提高用户体验。

1、常规字典列表的绑定

对于普通的下拉列表控件,我们绑定操作就是先获取字典数据列表,然后对它的数据项进行添加操作即可,为了方便,我们往往做成一个扩展函数的方式来进行处理,并把这些通用的扩展函数放到界面基类库里面方便重用,这样我们可以在设计到界面的数据绑定的时候,非常方便的调用了。

如类似下面的扩展函数定义。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="itemList">数据字典列表</param>
        /// <param name="defaultValue">控件默认值</param>
        /// <param name="emptyFlag">是否加入空值选项</param>
        public static void BindDictItems(this ComboBoxEdit combo, List<string> itemList, string defaultValue, bool emptyFlag = true)
{
combo.Properties.BeginUpdate();
//可以加快 combo.Properties.Items.Clear();
combo.Properties.Items.AddRange(itemList);
if(emptyFlag)
{
combo.Properties.Items.Insert(
0, "");
}
if (itemList.Count > 0)
{
combo.SetDropDownValue(defaultValue);
}

combo.Properties.EndUpdate();
//可以加快 }

还有有时候我们需要一个显示文本、值的键值对来对字典进行处理,如定义的CListItem对象

    /// <summary>
    ///框架用来记录字典键值的类,用于Comobox等控件对象的值传递/// </summary>
[Serializable]public classCListItem
{
/// <summary> ///显示内容/// </summary> public string Text { get; set; }/// <summary> ///实际值内容/// </summary> public string Value { get; set; }
}

这样我们绑定列表的扩展函数在定义一个函数,如下所示。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="itemList">数据字典列表</param>
        /// <param name="defaultValue">控件默认值</param>
        /// <param name="emptyFlag">是否加入空值选项</param>
        public static void BindDictItems(this ComboBoxEdit combo, List<CListItem> itemList, string defaultValue, bool emptyFlag = true)
{
combo.Properties.BeginUpdate();
//可以加快 combo.Properties.Items.Clear();
combo.Properties.Items.AddRange(itemList);
if(emptyFlag)
{
combo.Properties.Items.Insert(
0, new CListItem(""));
}
if (itemList.Count > 0)
{
if (!string.IsNullOrEmpty(defaultValue))
{
combo.SetComboBoxItem(defaultValue);
}
else{
combo.SelectedIndex
= 0;
}
}

combo.Properties.EndUpdate();
//可以加快 }

当然,除了上面的这两个处理,我们还可以定义很多不同类型的重载方法,以便更方便处理相关的控件的字典数据绑定。

对于固定数据源List<string>、或者List<CListItem>来说,我们绑定的操作就非常简单。

List<CListItem> itemList= new List<CListItem>() { new CListItem(""), new CListItem("") };
txtItem.BindDictItems(itemList);

然后获取对应字典值的方式,我们可以定义一个扩展函数来处理,如下代码所示。

        /// <summary>
        ///获取下拉列表的值/// </summary>
        /// <param name="combo">下拉列表</param>
        /// <returns></returns>
        public static string GetComboBoxValue(thisComboBoxEdit combo)
{
CListItem item
= combo.SelectedItem asCListItem;if (item != null)
{
returnitem.Value;
}
else{return "";
}
}

对于以上的操作,我们这里还没有涉及到字典模块里面的数据源,只是提供一些常规的固定列表,我们知道,大多数的数据字典我们是通过字典模块来进行维护的。

因此我们也需要动态的从字典库上获取对应的字典集合来进行绑定。字典的数据,我们可以通过字典类型或者字典代码来获取,如下是通过字典类型获取对应的字典列表代码。

BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);

有了这些数据的获取方法,我们就可以通过扩展函数来进一步扩展我们绑定字典类别的方式了,如下扩展函数所示。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        public static void BindDictItems(this ComboBoxEdit combo, string dictTypeName, stringdefaultValue)
{
Dictionary
<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

BindDictItems(combo, itemList, defaultValue);
}

使用的时候,就非常简单了,如下代码是实际项目中对字典列表绑定的操作,字典数据在字典模块里面统一定义的。

        /// <summary>
        ///初始化数据字典/// </summary>
        private voidInitDictItem()
{
txtInDiagnosis.BindDictItems(
"入院诊断");
txtLeaveDiagnosis.BindDictItems(
"最后诊断");//初始化代码 this.txtFollowType.BindDictItems("随访方式");this.txtFollowStatus.BindDictItems("随访状态");
}

这样就非常简化了我们对字典数据源的绑定操作了,非常方便易读,下面是其中一个功能界面的下拉列表展示。

2、使用缓存提高界面响应速度

前面介绍了通过扩展函数来进一步扩展我们绑定字典类别的方式了,如下扩展函数所示。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        public static void BindDictItems(this ComboBoxEdit combo, string dictTypeName, stringdefaultValue)
{
Dictionary
<string, string> dict =BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);
List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

BindDictItems(combo, itemList, defaultValue);
}

如果是基于服务接口的方式(通过Web API或者WCF方式)获取字典列表,那么BLLFactory<T>的方式就修改为CallerFactory<T>的方式获取数据了,如下扩展函数所示。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="control">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        /// <param name="emptyFlag">是否添加空行</param>
        public static void BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool emptyFlag = true)
{
Dictionary
<string, string> dict = CallerFactory<IDictDataService>.Instance.GetDictByDictType(dictTypeName);
List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

control.BindDictItems(itemList, defaultValue, emptyFlag);
}

也就是通过服务接口工厂方法调用

CallerFactory<IDictDataService>.Instance.GetDictByDictType(dictTypeName);

而获取数据字典列表的内容,这个可以配置为Web API访问方式、WCF访问方式,底层就是调用客户端封装的代理方法获取就是了。例如对于Web API调用来说就是通过客户端直接访问Web API服务接口获取数据的,实现代码如下所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public Dictionary<string, string> GetDictByDictType(stringdictTypeName)
{
var action =System.Reflection.MethodBase.GetCurrentMethod().Name;string url = GetTokenUrl(action) + string.Format("&dictTypeName={0}", dictTypeName.UrlEncode());

Dictionary
<string, string> result = JsonHelper<Dictionary<string, string>>.ConvertJson(url);returnresult;
}

由于字典数据是相对比较固定的,一般时效不是那么及时都没问题,由于这部分数据是通过网络的方式获取的,反复的调用获取是会耗费一定的时间。

为了提高用户响应速度,我们可以把它放到客户端的缓存里面(非服务器缓存),设置一定的失效时间,在失效时间内,我们数据不再反复的从网络接口获取,而是直接通过缓存里面提取,速度非常快,同时也提高了界面响应速度。

但是为了不影响已有代码,我们可以继续在扩展函数的实现上做一些扩展即可,首先我们定义一个公共的获取字典数据的方法,如下所示。

        /// <summary>
        ///获取字典类型的通用处理/// </summary>
        /// <param name="dictTypeName">字典类型</param>
        /// <param name="isCache">是否缓存,默认为true</param>
        /// <returns></returns>
        private static Dictionary<string, string> GetDictByDictType(string dictTypeName, bool isCache = true)
{
Dictionary
<string, string> dict = null;if(isCache)
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}-{2}", method.DeclaringType.FullName, method.Name, dictTypeName);

dict
= MemoryCacheHelper.GetCacheItem<Dictionary<string, string>>(key,delegate() {return CallerFactory<IDictDataService>.Instance.GetDictByDictType(dictTypeName);
},
new TimeSpan(0, 30, 0));//30分钟过期 }else{
dict
= CallerFactory<IDictDataService>.Instance.GetDictByDictType(dictTypeName);
}
returndict;
}

通过使用 MemoryCacheHelper.GetCacheItem<Dictionary<string, string>> 的方式,我们可以把它设置为缓存处理方式,如果在失效时间内,则从缓存里面提取。

这样原来的绑定下拉列表的扩展方法获取字典数据,从这个公共的接口里面获取即可,而我们也仅仅是增加一个具有默认值的缓存与否的参数,用来决定是否使用缓存模式,默认为使用缓存处理。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="control">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        /// <param name="emptyFlag">是否添加空行</param>
        public static void BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true)
{
var dict =GetDictByDictType(dictTypeName, isCache);

List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

control.BindDictItems(itemList, defaultValue, emptyFlag);
}

这样原来的数据下拉列表绑定的方式没有变化,依旧是我们原来的代码,但是默认采用缓存方式来绑定基于网络接口(混合框架模式)获取的字典数据。

        /// <summary>
        ///初始化数据字典/// </summary>
        private voidInitDictItem()
{
//初始化代码 this.txtSurgeryType.BindDictItems("手术方式");this.txtIsFirstTime.BindDictItems("首发");this.txtWHOGrade.BindDictItems("病理WHO分级");this.txtLesionPart.BindDictItems("病灶部位");this.txtOccupation.BindDictItems("病人职业");this.txtRelapse.BindDictItems("复发");this.txtPathologyGrade.BindDictItems("病理分级");this.txtSymptom.BindDictItems("初发症状");this.txtAnesthesiaMethod.BindDictItems("麻醉方法");this.txtSpecimenDetail.BindDictItems("具体标本情况");
}

得到的编辑界面如下所示,使用缓存接口,对于大量字典数据显示的界面,界面显示速度有了不错的提升。

而对于一些特殊列表的字典显示,如需要通过拼音首字母进行检索功能的下拉列表,我们依旧可以使用这种绑定的方式实现缓存处理的。

如字典绑定的扩展函数如下所示,这样就统一了整个字典列表的绑定操作,比较容易记住。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        public static void BindDictItems(this CustomGridLookUpEdit combo, string dictTypeName, string defaultValue, bool isCache = true)
{
string displayName =dictTypeName;const string valueName = "值内容";const string pinyin = "拼音码";var dt = DataTableHelper.CreateTable(string.Format("{0},{1},{2}", displayName, valueName, pinyin));var dict =GetDictByDictType(dictTypeName, isCache);foreach (string key indict.Keys)
{
var row =dt.NewRow();
row[displayName]
=key;
row[valueName]
=dict[key];
row[pinyin]
=Pinyin.GetFirstPY(key);
dt.Rows.Add(row);
}

combo.Properties.ValueMember
=valueName;
combo.Properties.DisplayMember
=displayName;
combo.Properties.DataSource
=dt;
combo.Properties.PopulateViewColumns();
combo.Properties.View.Columns[valueName].Visible
= false;
combo.Properties.View.Columns[displayName].Width
= 400;
combo.Properties.View.Columns[pinyin].Width
= 200;
combo.Properties.PopupFormMinSize
= new System.Drawing.Size(600, 0);if (!string.IsNullOrEmpty(defaultValue))
{
combo.EditValue
=defaultValue;
}
}

界面效果如下所示。

以上就是常规单机版数据绑定操作,以及基于网络版缓存数据的数据字典绑定操作,我们在界面代码的处理上没有任何差异,只是辅助扩展函数做一些调整就可以很好的变化过来了,这样对于我们界面代码的重用或者调整是非常便利的,同时缓存的使用,对于网络性能有所差异的地方,速度也会明细的有所提高。以上就是对于字典模块的一些处理上的分享,希望对大家开发Winform界面代码有所帮助和启发。