2023年2月

本篇继续《
基于Metronic的Bootstrap开发框架--工作流模块功能介绍
》,继续介绍基于Metronic的Bootstrap开发框架的工作模块功能,介绍工作流模块中相关业务表单的界面设计和管理操作,以及在业务表单中设计到的审批、发起会签、会签确认、会签、撤销、领导批示分阅、阅办等常规操作,以及一些明细表单的数据录入展示。

1、工作流主页视图

在工作流模块中,我们可以把一些待办或者已办的事项放在首页里面,方便登陆后直接可以参考最新的一些信息,如下界面所示。

而在工作流的业务表单模块中,我们可以提供了很多相关的业务表单管理界面,可以通过对应的功能菜单进入对应的工作流业务表单列表,以下是工作流模块提供的一些常见业务表单。

2、工作流业务表单

1)付款申请

付款申请列表管理如下所示,可以根据相关条件进行筛选查看相关申请单。

我们可以在新增入口创建一个新的付款申请单,如下界面所示。

在选择流程处理人中,单击按钮会弹出一个用户选择按钮供选择,如下界面所示。

在申请单的创建界面左下角,有一个【存为草稿】的按钮,单击可以保存该申请单到草稿里面,方便下次打开提交申请单,这个保存草稿是一个通用的操作,在各个申请单都有这样的按钮供选择。

提交申请单成功后,会返回对应申请单列表的主界面,会发现申请单处于【处理中】的状态,双击可以打开该申请单进行详细信息的查看。

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

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

同样,如果不通过该申请,那么可以选择【退回拟稿人处理】和【退回上一步处理】两个其他处理。

【退回拟稿人处理】是直接拒绝该申请单,让用户重新修改后可以再次提交的,【退回上一步处理】则是退回上一个步骤,如果过程涉及很多步骤,也就仅仅倒回一步。

当然,这个处理过程中,我们可以动态增加一个流程步骤,也就是【增加一步审批】,然后指定增加步骤的处理人即可,如下界面所示。

在查看明细的审批的处理按钮中,如果我们是流程发起人,那么我们可以撤销该申请单,撤销界面如下所示。

而流程日志以及打印功能是常规的处理操作,流程日志列出整个申请单处理流程的日志,以及申请单处理的历史信息和系统日志,如下界面所示。

对于有些流程处理步骤设置环节为会签的,那么会执行相关的会签操作。

会签是指创建一个或多个子流程供相关人员进行审批,等待全部人员完成处理后再次回到主流程上,然后决定是否继续流转到下一个流程步骤上去,一般的申请单的主流程如下所示。

这里设置的会签处理就是其中一个步骤,一旦会签处理步骤发起会签,就会构建多个可供审批的子流程了,如下所示。

例如对于进入发起会签的申请单,我们可以看到功能按钮中有一个【发起会签】的功能,如下所示。

发起会签的时候,我们需要指定相应会签的人员,如下是发起会签的审批界面。

而如果我们是会签人员列表中的用户,我们查看对应的申请单明细的时候,就会发现有【会签】的功能按钮,如下界面所示。

会签是所有人员全部批准通过才会确认通过,这个可以在会签结果审核界面进行处理即可,会签的审批界面如下所示。

所有参与会签人员提交意见后,发起会签的人员会看到【会签确认】的功能按钮,如下所示。

会签确认是有发起人决定是否通过该项会签,并进行下一步处理的功能,它是一个标准的审批功能。

完成后,我们查看对应的申请单,可以看到对应步骤的会签处理信息,都会呈现在这个表单明细里面,如下所示。

【批示分阅】步骤则是我们希望给相关人员传阅了解申请单的一个处理过程,同样它是一个选择多个用户参与传阅的处理,如下是【批示分阅】审批步骤。

完成后,整个申请单信息如下界面所示。

以上就是付款申请单,涉及到常规审批过程、会签过程(包括发起会签、会签、会签确认)、领导批示分阅、阅办等过程,一般情况下,我们可能涉及到一个或者几个处理过程,而这些过程我们是在流程模板里面进行定义的。

一旦定义好后,新生成的申请就以流程模板的步骤为参考申请对应的流程步骤,同时我们也可以在审批过程中,动态增加一些流程步骤,这样的弹性设置在一些临时需要增加一些审批步骤是非常方便的。

2)报销申请

在报销申请单处理中,往往提交 一些主表信息外,也会涉及到一些报销明细的录入,我们这里提供了一个表格直接录入数据的方式,减少用户维护数据的复杂性,非常方便。

上面明细清单信息,可以通过新增记录按钮,增加一条空白的记录,然后部分字段可以通过列表方式进行选择,保存提交申请单的时候,会自动把明细数据一并保存的。

保存申请单后,系统直接跳到【我的审批工作】界面,如下界面所示。

或者我们也可以查看报销申请单的列表界面。

查看具体报销申请单信息界面如下所示,其右上角的审批、撤销、流程日志、打印等功能按钮也会根据情况显示出来。

而这里的明细清单,则以表格方式进行列出,非常直观友好。

3)资产领用

介绍了一个报销申请的流程,我们再来介绍一个资产管理中的资产领用申请单,这也是一个比较有代表意义的流程申请单,具有明细处理的信息。

创建资产领用申请单界面如下所示,其中涉及了很多下拉列表联动选择的操作,明细的录入也是通过直接编辑表格的方式进行录入。

查看界面如下所示。

和其他流程表单不同的是,这个申请单中,在流程没有完成之前,明细清单可以在各个步骤进行编辑修改、删除等操作,由最后一个审批人进行维护并提交明细,流程完成后则不能继续编辑表单中的明细信息。

同时在流程完成后,会同时修改主资产信息的部分字段,从而完成了整个闭环的处理操作。

当然工作流模块中还有很多范例的表单,如开始列出的菜单中包含的内容。

不过由于其他界面上也比较类似,也就不再一一赘述,关键这些业务表单也是通过代码生成工具直接生成对应的Create、ViewDetail、Index视图代码和控制器代码的,因此这些业务表单的信息就不在一一介绍。

整个工作流模块,各个页面的职责比较清晰,基本上Create就是新建或者重新编辑、ViewDetail就是查看明细信息、index就是对应表单的列表界面显示,而其中涉及到的选择流程用户、选择资产等特殊操作,我们是定义一些共同的页面视图即可完成,在需要使用到的地方加入对应的页面即可,提高视图页面和脚本的重用性。

在前面随笔《
基于Metronic的Bootstrap开发框架--工作流模块功能介绍
》和《
基于Metronic的Bootstrap开发框架--工作流模块功能介绍(2)
》中介绍了Bootstrap开发框架的工作模块功能,前面文章也提及,通过代码生成工具直接生成对应的Create、ViewDetail、Index视图代码和控制器代码,本篇随笔介绍如何使用使用代码生成工具Database2Sharp快速生成工作流模块控制器和视图代码的过程。

1、工作流界面功能

工作流模块如果要增加一个业务表单的处理,那么界面包括了列表界面,创建和编辑申请单界面,查看申请单明细这几个界面,以及对应后台控制器的代码。其他共用的界面和代码,则是在整个工作流模块中通用的,不需要变化。

我们来关注下如果增加一个业务表单的情况下,需要的列表界面,创建和编辑申请单界面,查看申请单明细这几个界面。

这些使用代码生成工具Database2Sharp快速生成工作流模块界面,是集成了我们整个工作流处理方式,包括列表界面可以分页查询数据、编辑表单中选择用户、处理附件,以及查看明细界面中集成的各种流程处理步骤,包括审批、会签、退回、拒绝、查看流程日志、打印表单等等常规处理步骤。

2、使用代码生成工具Database2Sharp快速生成工作流界面

和常规的代码生成工具生成代码一样,我们打开代码生成工具,然后展开数据库表后,通过菜单的【Boostrap的Web界面代码生成】生成对应的代码即可。

通过选中对应的数据库表,就可以继续一步步处理了,最后确认代码生成即可。

生成代码后,我们可以看到在对应的目录有两个目录,MVCWebUI和WorkflowWebUI目录,如下所示。

两个目录MVCWebUI和WorkflowWebUI,其中MVCWebUI包含了常规Bootstrap框架的页面视图和控制器代码文件,如下所示。

而WorkflowWebUI目录则是我们这里需要重点关注的工作流视图页面代码文件,如下所示。

上面各个目录是对应我们业务表的内容,目录下面是有几个工作流模块中包括了列表界面,创建和编辑申请单界面,查看申请单明细这几个界面。

3、在项目中集成工作流界面代码

这几个工作流界面我们连同他们的目录一同复制到项目的视图目录里面即可,同时把常规Bootstrap界面中控制器复制到项目的控制器目录即可。

上面红框中就是我们一些工作流业务表单的视图目录,因此我们需要看看目录下面的几个文件。

集成这些页面代码后,我们还需要做一些基础的处理才能使用起来,就是需要定义一个业务表单信息。

1)流程模板定义

流程模板是我们开展一些工作流的基础,也就是说,我们先有特定流程的模板,然后才有具体的流程示例。

流程模板需要指定它的具体名称,另外有几个字段是必须注意的,就是它的对应业务表名和创建流程URL、查看流程URL这几个信息。

定义流程模板基本信息后,我们需要为这个流程模板设置对应的步骤,如下所示是增加一些流程步骤。

2)修改列表界面的表单ID

定义一个新的流程模板后,由于我们在流程管理界面中需要创建对应的申请单,那么我们需要知道这个流程模板的表单ID,因此需要在上面生成的工作流index.cshtml页面里面修改一个表单ID

创建定义完毕流程模板后,我们打开对应的表单记录,找到对应的表单ID

然后修改对应列表界面的formId为这个流程模板ID即可。

至此,这样整个界面就可以跑起来,而且也可以在列表页面里面直接创建对应表单的流程,类似下面的创建申请单界面。

创建业务申请单,那么也可以在业务受理列表里面创建。

关于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)权限的配置

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

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

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

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

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

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

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

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

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