2023年2月

在我们开发各种项目应用的时候,往往都是基于一定框架进行,同时配合专用的代码生成工具,都是为了快速按照固定模式开发项目,事半功倍,本篇随笔对基于ABP开发框架的技术点进行分析和ABP框架项目快速开发实现进行介绍,抛砖引玉的对我们技术人员的日常技术工作进行一定的总结,希望大家不吝支持。

1、ABP开发框架的项目开发

下面对ABP框架的数据库支持、管理端界面、功能模块划分、ABP框架特点、模块开发支持等方面进行介绍,如下图所示。

我们知道,一般我们开发某个项目的时候,往往都会关注项目默认支持的数据库,以及后续能够支持的一些数据库,对于一般的应用,当然是希望它能够支持多种数据库。

对于ABP框架来说,它的底层是基于.netcore的Entity Framework Core ,它对于常规的数据库基本上都是支持的,如下所示。

关于ABP框架使用Oracle数据库及数据库的迁移,可以参考我的随笔《
ABP框架使用Oracle数据库,并实现从SQLServer中进行数据迁移的处理

关于ABP框架使用Mysql数据库及数据库的迁移,可以参考我的随笔《
ABP框架使用Mysql数据库,以及基于SQLServer创建Mysql数据库的架构和数据

现在框架基本上都是多端应用的了,所以在ABP框架中整合Winform管理端、Vue&element的BS前端,以及公司动态网站用于发布产品和网站信息等都是常见的应用,有时候,我们还需要根据功能的需要,增加一些小程序的支持,这些对于Web API后端来说,都是很容易接入的应用处理。

Winform应用端,其实很多时候是很必要的,提供众多丰富的功能展示,以及很好的用户交互能力,特别在对接硬件或者打印处理的时候。

和我们的Winform框架一样,Winform应用端提供封装良好的框架组件模块,包括权限系统、字典模块、附件管理模块、自动升级、分页控件、公用类库及界面基类模块等模块。

而基于Vue + Element 的前端界面,和基于Winform的ABP框架一样,使用同一个API后端,模块包括用户管理、组织机构管理、角色管理、菜单管理、功能管理及权限分配,日志管理、字典管理、产品管理等管理功能,可实现用户的功能及数据权限进行控制管理。

公司动态门户网站,有时候用于我们发布网站信息和产品信息的一个门户网站,采用了Bootstrap-Vue界面组件,由于大多数门户网站都是基于Bootstrap栅格系统的,因此基于最新Bootstrap-Vue界面组件也就是最佳选择的了,而且可以重用很多Bootstrap的网站模板案例。这样也同时保持了前端模块同时也是基于Vue的,摒弃了以前基于JQuery的繁琐操作DOM处理。

有了这些管理端,基本上也就满足了大部分的需求,不过由于现在微信小程序的广泛应用,有时候我们针对一些业务模块功能,可以根据需要推出一些小程序应用场景,这些对接我们的统一授权系统,以及统一的WebAPI调用机制即可满足。

前面介绍了,基于ABP开发框架的Winform端、Vue&Element前端,都整合了很多常规的模块,如权限、字典、附件等常见的模块,这些既可以是系统性的管理模块,也是基础性数据的支撑,同时也会提供众多组件给我们应用模块进行使用的。

而我们在框架提供这些内容外,都需要根据具体业务的需要,按照框架的架构、编码标准要求,增量式的开发一些业务模块界面,一般最好的清空下是借助辅助工具的使用,能够节省大量的时间,同时降低难度,事半功倍。

2、ABP框架的代码生成

借助我们ABP框架的定制代码生成工具Database2sharp,可以对Winform界面开发、Vue&Element界面开发、ABP框架后端代码进行全栈式的开发处理。

Database2sharp它通过整合框架相关的生成规则,我们可以增加对应的ABP框架后端代码的生成,如下代码生成工具界面所示。

Database2Sharp关于ABP框架的Winform界面配置如下界面所示,可以定制化的生成Winform界面代码。

在代码生成工具Database2Sharp界面里面,选择【代码生成】【ABP的Vue+Element界面代码】,如下所示。

确认生成代码后,我们可以看到对应的API调用JS类和Vue视图文件,如下所示。

3、ABP框架的特点

ABP框架,主要可以概括为下面几点。

详细的话,ABP框架涉及到的内容,包括下面这些。

  • 依赖注入
    ,这个部分使用 Castle windsor (依赖注入容器)来实现依赖注入,这个也是我们经常使用IOC来处理的方式;
  • Repository仓储模式
    ,已实现了Entity Framework、NHibernate、MangoDB、内存数据库等,仓储模式可以快速实现对数据接口的调用;
  • 身份验证与授权管理
    ,可以使用声明特性的方式对用户是否登录,或者接口的权限进行验证,可以通过一个很细粒度的方式,对各个接口的调用权限进行设置;
  • 数据有效性验证
    ,ABP自动对接口的输入参数对象进行非空判断,并且可以根据属性的申请信息对属性的有效性进行校验;
  • 审计日志记录
    ,也就是记录我们对每个接口的调用记录,以及对记录的创建、修改、删除人员进行记录等处理;
  • Unit Of Work工作单元模式
    ,为应用层和仓储层的方法自动实现数据库事务,默认所有应用服务层的接口,都是以工作单元方式运行,即使它们调用了不同的存储对象处理,都是处于一个事务的逻辑里面;
  • 异常处理
    ,ABP框架提供了一整套比较完善的流程处理操作,可以很方便的对异常进行进行记录和传递;
  • 日志记录
    ,我么可以利用Log4Net进行常规的日志记录,方便我们跟踪程序处理信息和错误信息;
  • 多语言/本地化支持
    ,ABP框架对多语言的处理也是比较友好的,提供了对XML、JSON语言信息的配置处理;
  • Auto Mapping自动映射
    ,这个是ABP的很重要的对象隔离概念,通过使用AutoMaper来实现域对象和DTO对象的属性映射,可以隔离两者的逻辑关系,但是又能轻松实现属性信息的赋值;
  • 动态Web API层
    ,利用这个动态处理,可以把Application Service 直接发布为Web API层,而不需要在累赘的为每个业务对象手工创建一个Web API的控制器,非常方便;
  • 动态JavaScript的AJax代理处理
    ,可以自动创建Javascript 的代理层来更方便使用Web Api,这个在Web层使用。

一般来说,对于数据库的操作,用标准仓储模式处理就可以了,而且我们基于框架的基类封装,可以减少很多标准的处理代码,只需要简单继承一下关系即可。

其中基类MyAsyncServiceBase 已经封装了常见的数据库操作处理。

而这个MyAsyncServiceBase 构造函数包含了相关的DTO对象,方便用于整个对象接口的处理,如查询条件、返回对象DTO、领域对象、主键类型等等。

如果不需要其他业务对象的操作,那么就保留默认的仓储对象即可,如果需要使用其他业务对象处理,那么同时引入,并在构造函数中注入所需的仓储对象,如下面所示。

一般来说,这些集成关系,我们在利用代码生成工具生成代码的时候,都已经完全处理好的了,各层的关系也都一一生成。

它主要是分为下面几个项目分层。

Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。

EntityFrameworkCore
实体框架核心层,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。

Application.Common和
Application应用层

:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。

Web.Core Web核心层
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。

Web.Core.Host Web API的宿主层
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。

Migrator数据迁移层
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。

在ABP开发框架中应用服务层ApplicationService类中,都会提供常见的一些如GetAll、Get、Create、Update、Delete等的标准处理接口,而由于在ApplicationService类定义的时候,都会传入几个不同的类型作为泛型的参数,实现强类型的类型处理,本篇随笔对于分页查询排序的实现处理做一个详细的介绍,介绍其中对分页查询条件的定义,子类应用服务层的条件查询逻辑重写、排序逻辑重写等规则的处理。

1、ApplicationService类的泛型定义

例如我们定义User应用服务层的UserApplicationService的时候,传入了几个不同类型的参数作为基类的泛型约束类型,如下所示。

[AbpAuthorize]public class UserAppService: MyAsyncServiceBase<User, UserDto, long, UserPagedDto, CreateUserDto, UserDto>, IUserAppService

同类型的字典数据应用服务层的定义如下所示,可以看到和UserAppService类似的。

其中MyAsyncServiceBase则是我们自定义的一个基类对象,主要是根据传入不同的参数构造不同的强类型对象返回。

    public abstract class MyAsyncServiceBase<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput, TGetInput, TDeleteInput>: 
AsyncCrudAppService
<TEntity, TEntityDto, TPrimaryKey, TGetAllInput, TCreateInput, TUpdateInput, TGetInput, TDeleteInput> where TEntity : class, IEntity<TPrimaryKey> where TEntityDto : IEntityDto<TPrimaryKey> where TUpdateInput : IEntityDto<TPrimaryKey> where TGetInput : IEntityDto<TPrimaryKey> where TDeleteInput : IEntityDto<TPrimaryKey>

这里UserApplicationService的服务层中参数的User类,对应是EFCore的领域对象,它的定义如下所示

    public class User : AbpUser<User>

由于User需要集成AbpUser基类的一些特性,因此有继承关系,它主要就是负责和数据库模型打交道的对象。

而如果不是类似User这样系统用到的基类对象,那么我们就需要如下定义,指定表单的名称,以及对象的约束条件了,如下字典的领域对象如下定义所示。

而 MyAsyncServiceBase 的第二个参数则是用于传递的DTO对象,可以认为它和数据库没有直接的关系,不过由于引入了AutoMapper,我们一般看它们的属性还是有很多相同的地方,不过DTO更加面向的是业务界面,而非存储处理。

如果对于一些界面特殊的数据信息,需要转换为领域对象的属性,则需要进行特别的自定义映射处理了。

如User的DTO对象定义如下所示。

而如果我们的DTO对象,不需要利用ABP进行参数内容的约束,那么可以更加简化一些条件,如下字典DTO对象所示。

对于类似下面的字典模块的应用服务层定义

其中第三个参数是主键ID的类型,如果为Int这是整形,这里是字符串类型,因此使用string。

第四个参数DictDataPagedDto就是分页查询的条件 ,这个DTO对象,主要就是获取客户端查询处理的条件的,因此可以根据需要查询的条件进行裁剪,默认利用代码生成工具Database2sharp生成的属性基本上包括了所有的数据库表属性名称了。如字典数据的查询条件比较简单,如下所示,除了包含一些分页条件信息外,就是包含所需要的查询条件属性了。

    /// <summary>
    ///用于根据条件分页查询/// </summary>
    public classDictDataPagedDto : PagedAndSortedInputDto
{
public DictDataPagedDto() : base() { }/// <summary> ///参数化构造函数/// </summary> /// <param name="skipCount">跳过的数量</param> /// <param name="resultCount">最大结果集数量</param> public DictDataPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
{
}
/// <summary> ///使用分页信息进行初始化SkipCount 和 MaxResultCount/// </summary> /// <param name="pagerInfo">分页信息</param> public DictDataPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
{
}
/// <summary> ///字典类型ID/// </summary> public virtual string DictType_ID { get; set; }/// <summary> ///类型名称/// </summary> public virtual string Name { get; set; }/// <summary> ///指定值/// </summary> public virtual string Value { get; set; }/// <summary> ///备注/// </summary> public virtual string Remark { get; set; }
}

2、分页查询排序的实现处理

前面我们介绍了应用服务层中利用泛型基类的参数定义,可以强类型返回各项不同数据接口,这种就是非常弹性化的设计模式了。

ABP+Swagger负责API接口的开发和公布,如下是API接口的管理界面。

进一步查看GetAll的API接口说明,我们可以看到对应的条件参数,如下所示。

这些是作为查询条件的处理,用来给后端获取对应的条件信息,从而过滤返回的数据记录的。

那么我们前端界面也需要根据这些参数来构造查询界面,我们可以通过部分条件进行处理即可,其中MaxResultCount和SkipCount是用于分页定位的参数。

我们来看看基类对于查询分页排序的处理函数,从而了解它的处理规则。

        public virtual async Task<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input)
{
//判断权限 CheckGetAllPermission();//获取分页查询的条件 var query =CreateFilteredQuery(input);//根据条件获取所有记录数 var totalCount = awaitAsyncQueryableExecuter.CountAsync(query);//对查询内容排序和分页 query =ApplySorting(query, input);
query
=ApplyPaging(query, input);//返回领域实体对象 var entities = awaitAsyncQueryableExecuter.ToListAsync(query);//构造返回结果集,并转换实体类为DTO对应 return new PagedResultDto<TEntityDto>(
totalCount,
entities.Select(MapToEntityDto).ToList()
);
}

其中 CreateFilteredQuery 、ApplySorting和 ApplyPaging 都是利用可以子类重写的函数实现弹性化的逻辑调整处理。

在基类中,默认的CreateFilteredQuery 提供了简单的返回所有列表的处理,并不处理查询条件,这个具体的条件过滤由子类实现逻辑的。

protected virtual IQueryable<TEntity>CreateFilteredQuery(TGetAllInput input)
{
returnRepository.GetAll();
}

而列表排序处理ApplySorting的基类函数,基类提供了标准的对Sorting 属性进行条件排序,否则就根据主键ID进行倒序排序处理,如下代码所示。

protected virtual IQueryable<TEntity> ApplySorting(IQueryable<TEntity>query, TGetAllInput input)
{
//Try to sort query if available var sortInput = input asISortedResultRequest;if (sortInput != null)
{
if (!sortInput.Sorting.IsNullOrWhiteSpace())
{
returnquery.OrderBy(sortInput.Sorting);
}
}
//IQueryable.Task requires sorting, so we should sort if Take will be used. if (input isILimitedResultRequest)
{
return query.OrderByDescending(e =>e.Id);
}
//No sorting returnquery;
}

而基类的分页的处理ApplyPaging逻辑,主要就是转换为标准的接口进行处理,如下代码所示。

protected virtual IQueryable<TEntity> ApplyPaging(IQueryable<TEntity>query, TGetAllInput input)
{
//Try to use paging if available var pagedInput = input asIPagedResultRequest;if (pagedInput != null)
{
returnquery.PageBy(pagedInput);
}
//Try to limit query result if available var limitedInput = input asILimitedResultRequest;if (limitedInput != null)
{
returnquery.Take(limitedInput.MaxResultCount);
}
//No paging returnquery;
}

以上是标准基类提供的几个可以重写的默认实现,一般来说,我们会通过子类重写逻辑实现的方式进行逻辑重写的。

如对于字典模块的条件信息,我们可以进行重写,以便实现自定义的条件查询处理,如下DictDataAppService应用服务层的重写处理。

/// <summary>
///自定义条件处理/// </summary>
/// <param name="input"></param>
/// <returns></returns>
protected override IQueryable<DictData>CreateFilteredQuery(DictDataPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.Name.IsNullOrWhiteSpace(), t =>t.Name.Contains(input.Name))
.WhereIf(
!string.IsNullOrEmpty(input.Remark), t =>t.Remark.Contains(input.Remark))
.WhereIf(
!string.IsNullOrEmpty(input.Value), t => t.Value ==input.Value)
.WhereIf(
!string.IsNullOrEmpty(input.DictType_ID), t => t.DictType_ID ==input.DictType_ID);
}

而对于属性比较复杂的查询,我们适当调整这个函数的处理基类,一般都可以根据代码生成工具进行生成的,特殊条件自己微调一下就没问题了。

如用户应用服务层类UserAppService的重写自定义条件的函数,代码如下所示。

/// <summary>
///自定义条件处理/// </summary>
/// <param name="input">查询条件Dto</param>
/// <returns></returns>
protected override IQueryable<User>CreateFilteredQuery(UserPagedDto input)
{
return Repository.GetAllIncluding(x => x.Roles) //base.CreateFilteredQuery(input) .WhereIf(input.ExcludeId.HasValue, t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.EmailAddress.IsNullOrWhiteSpace(), t => t.EmailAddress.Contains(input.EmailAddress)) //如需要精确匹配则用Equals .WhereIf(input.IsActive.HasValue, t => t.IsActive == input.IsActive) //如需要精确匹配则用Equals .WhereIf(input.IsEmailConfirmed.HasValue, t => t.IsEmailConfirmed == input.IsEmailConfirmed) //如需要精确匹配则用Equals .WhereIf(input.IsPhoneNumberConfirmed.HasValue, t => t.IsPhoneNumberConfirmed == input.IsPhoneNumberConfirmed) //如需要精确匹配则用Equals .WhereIf(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals .WhereIf(!input.PhoneNumber.IsNullOrWhiteSpace(), t => t.PhoneNumber.Contains(input.PhoneNumber)) //如需要精确匹配则用Equals .WhereIf(!input.Surname.IsNullOrWhiteSpace(), t => t.Surname.Contains(input.Surname)) //如需要精确匹配则用Equals .WhereIf(!input.UserName.IsNullOrWhiteSpace(), t => t.UserName.Contains(input.UserName)) //如需要精确匹配则用Equals .WhereIf(!input.UserNameOrEmailAddress.IsNullOrWhiteSpace(), t =>t.UserName.Contains(input.UserNameOrEmailAddress)|| t.EmailAddress.Contains(input.UserNameOrEmailAddress) ||t.FullName.Contains(input.UserNameOrEmailAddress))//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

可以看出会根据UserPageDto的属性不同,从而增加更多的处理条件,有的是完全匹配,有些这是模糊匹配,有些如日期则是范围匹配。

对于数值、日期等有区间范围的属性,我们条件的DTO对象中,往往都有一个Start和End的起始值参数的。

这样我们在利用Vue&Element的前端进行查询的时候,可以构造对应的区间参数了,如下前端代码所示。

有时候,为了简化前端的日期区间代码,我们可以通过辅助类来简化处理。

而自定义排序的处理,则可以根据实际的需要进行排序处理,对于自增长的ID类型,使用ID倒序显示倒是问题不大,而如果是字符串类型,本身是GUID的类型,那么使用ID类排序这是没有任何意义的,因此必须通过重写基类函数的方式实现逻辑重写。

/// <summary>
///自定义排序处理/// </summary>
/// <param name="query"></param>
/// <param name="input"></param>
/// <returns></returns>
protected override IQueryable<DictData> ApplySorting(IQueryable<DictData>query, DictDataPagedDto input)
{
//先按字典类型排序,然后同一个字典类型下的再按Seq排序 return base.ApplySorting(query, input).OrderBy(s=>s.DictType_ID).ThenBy(s =>s.Seq);
}

具体情况根据ID的特点或者排序的具体情况进行排序即可。

最后一项是分页的处理,则可以按标准的方式处理,默认可以不重写。

这样我们前面提到的几个函数的逻辑,我们根据实际情况重写部分逻辑即可,从而非常弹性化的实现了条件的处理,排序的处理,分页的处理等规则。

public virtual async Task<PagedResultDto<TEntityDto>>GetAllAsync(TGetAllInput input)
{
//判断权限 CheckGetAllPermission();//获取分页查询的条件 var query =CreateFilteredQuery(input);//根据条件获取所有记录数 var totalCount = awaitAsyncQueryableExecuter.CountAsync(query);//对查询内容排序和分页 query =ApplySorting(query, input);
query
=ApplyPaging(query, input);//返回领域实体对象 var entities = awaitAsyncQueryableExecuter.ToListAsync(query);//构造返回结果集,并转换实体类为DTO对应 return new PagedResultDto<TEntityDto>(
totalCount,
entities.Select(MapToEntityDto).ToList()
);
}

因此,不管是Winform端,或者Vue&Element的BS前端,都可以通过不同的条件信息进行快速的查询排序处理了。

菜单资源管理的列表界面界面如下所示

用户列表包括分页查询及列表展示、以及可以利用按钮进行新增、编辑、查看用户记录,或者对指定用户进行重置密码操作。

一般来说,录入数据的时候,我们都采用在一个窗体界面中,根据不同内容进行录入,但是有时候涉及主从表的数据录入,从表的数据有时候为了录入方便,也会通过表格控件直接录入。在Winform开发的时候,我们很多时候可以利用表格GridControl控件来直接录入数据;在BS的Vue&Elment前端项目中,也可以利用第三方组件vxe-table直接录入表格数据。本篇随笔对Winform和Vue&Elment中直接录入数据进行分别的介绍和对比。

1、在Winform中直接录入表格数据

我们可直接在底部进行数据的录入,包括主表记录和从表的明细记录,可以一气呵成的录入并进行保存处理的,界面效果如下所示。

GridView的主从关系需要设置好集合的映射关系,我们需要通过设置GridLevelNode集合实现主从表关系的处理的。

初始化从表的GridView2和主从表关系的代码如下所示

通过上面的初始化代码,指定了主从表的关系后,我们还需要对绑定的数据源进行一定的处理,才能够在GridControl控件上显示主从表关系的记录。

首先需要定义一个业务对象,用来存储主从关系的记录对象。

然后在BindData绑定数据的时候,代码处理如下即可。

数据保存的代码和前面的操作类似,我们需要分别对GridView1和GridView2的数据保存操作进行处理,如下代码所示。

GridView2的字典项目明细保存操作如下所示。

在流程管理里面,对于具有主从明细的报销业务表的数据处理,采用了下面的界面。

这个界面中对于从表数据的录入处理代码如下所示。

        /// <summary>
        ///初始化明细表的GridView数据显示/// </summary>
        private voidInitDetailGrid()
{
//初始清空列 this.gridView1.Columns.Clear();//设置部分列隐藏 this.gridView1.CreateColumn("ID", "编号").Visible = false;this.gridView1.CreateColumn("Header_ID", "主表编号").Visible = false;this.gridView1.CreateColumn("Apply_ID", "申请单编号").Visible = false;//添加下拉列表列,并绑定数据源 this.gridView1.CreateColumn("FeeType", "费用类型", 100).CreateComboBox().BindDictItems("费用类型");//创建日期列并指定格式 var OccurTime = this.gridView1.CreateColumn("OccurTime", "发生时间", 120).CreateDateEdit();
OccurTime.EditMask
= "yyyy-MM-dd HH:mm";
OccurTime.DisplayFormat.FormatString
= "yyyy-MM-dd HH:mm";//创建数值列 this.gridView1.CreateColumn("FeeAmount", "费用金额").CreateSpinEdit();//创建备注列 this.gridView1.CreateColumn("FeeDescription", "费用说明", 200).CreateMemoEdit();//初始化GridView,可以新增列 this.gridView1.InitGridView(GridType.NewItem, false, EditorShowMode.MouseDownFocused, "");//转义列内容显示 this.gridView1.CustomColumnDisplayText += newCustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);//处理单元格的样式 this.gridView1.RowCellStyle += newRowCellStyleEventHandler(gridView1_RowCellStyle);//不允许头部排序 this.gridView1.OptionsCustomization.AllowSort = false;//绘制序号 this.gridView1.CustomDrawRowIndicator += (s, e) =>{if (e.Info.IsRowIndicator && e.RowHandle >= 0)
{
e.Info.DisplayText
= (e.RowHandle + 1).ToString();
}
};
//对输入单元格进行非空校验 this.gridView1.ValidateRow += delegate(objectsender, ValidateRowEventArgs e)
{
var result = gridControl1.ValidateRowNull(e, new string[]
{
"FeeType"});
};
//新增行的内容初始化 this.gridView1.InitNewRow += (s, e) =>{
gridView1.SetRowCellValue(e.RowHandle,
"ID", Guid.NewGuid().ToString());
gridView1.SetRowCellValue(e.RowHandle,
"Header_ID", tempInfo.ID);
gridView1.SetRowCellValue(e.RowHandle,
"Apply_ID", tempInfo.Apply_ID);
gridView1.SetRowCellValue(e.RowHandle,
"OccurTime", DateTime.Now);
};
}

保存处理的时候,我们获取明细列表,在写入从表记录后,继续保存明细列表即可。

        /// <summary>
        ///获取明细列表/// </summary>
        /// <returns></returns>
        private List<ReimbursementDetailInfo>GetDetailList()
{
var list = new List<ReimbursementDetailInfo>();for (int i = 0; i < this.gridView1.RowCount; i++)
{
var detailInfo = gridView1.GetRow(i) asReimbursementDetailInfo;if (detailInfo != null)
{
list.Add(detailInfo);
}
}
returnlist;
}
/// <summary> ///新增状态下的数据保存/// </summary> /// <returns></returns> public override boolSaveAddNew()
{
ReimbursementInfo info
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);
info.Creator
= base.LoginUserInfo.ID;
info.CreateTime
=DateTime.Now;try{#region 新增数据 bool succeed = BLLFactory<Reimbursement>.Instance.Insert(info);if(succeed)
{
//可添加其他关联操作 var list =GetDetailList();foreach(var detailInfo inlist)
{
BLLFactory
<ReimbursementDetail>.Instance.InsertUpdate(detailInfo, detailInfo.ID);
}
return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}

有时候,我们可以根据列表设置一些下拉列表字段,如下面所示。

或者禁用某些字段的录入,如下所示。

2、在Vue&Elment中直接录入表格数据

我在随笔《
在Vue前端界面中,几种数据表格的展示处理,以及表格编辑录入处理操作
》中介绍过在Vue前端界面中利用vxe-table表格组件实现数据的直接录入的。

vxe-table地址:

https://xuliangzhan_admin.gitee.io/vxe-table/#/table/start/install

https://github.com/x-extends/vxe-table

我们来看看它的常规使用代码,以及界面效果

如果需要录入新记录,通事件处理添加一个行即可。

这里继续介绍vxe-table表格组件的直接录入处理,特别是相关字典列表,以及级联处理操作。

例如,对于资产领用的明细清单处理,前端界面代码如下所示。

  <el-col:span="24">
    <el-form-itemlabel="明细清单">
      <div>
        <vxe-toolbar>
          <template#buttons>
            <vxe-buttonstatus="primary"content="新增"@click="insertEvent" />
            <vxe-buttonstatus="warning"content="删除"@click="removeSelectEvent" />
          </template>
        </vxe-toolbar>
        <vxe-tableref="xTable"border show-overflow keep-source resizable show-overflow :data="list":edit-config="{trigger: 'click', mode: 'row', showStatus: true}">
          <vxe-columntype="checkbox"width="60" />
          <vxe-columnfield="assetName"title="资产名称":edit-render="{name: 'input', events: {focus: assetNameFocusEvent}}" />
          <vxe-columnfield="assetCode"title="资产编码":visible="true" />
          <vxe-columnfield="lyDept"title="使用部门":edit-render="{name: '$select', options: deptList, events: {change: deptChangeEvent}}" />
          <vxe-columnfield="usePerson"title="使用人":edit-render="{name: '$select', options: usePersonList}" />
          <vxe-columnfield="keepAddr"title="存放地点":edit-render="{name: 'input', attrs: {type: 'text'}}" />
          <vxe-columnfield="unit"title="单位":edit-render="{name: 'input', attrs: {type: 'text'}}" />
          <vxe-columnfield="price"title="单价(元)":edit-render="{name: '$input', props: {type: 'float', digits: 2}}" />
          <vxe-columnfield="totalQty"title="数量":edit-render="{name: '$input', props: {type: 'integer'}}" />
          <vxe-columnfield="totalAmount"title="金额(元)":edit-render="{name: '$input', props: {type: 'float', digits: 2}}" />
          <vxe-columnfield="note"title="备注":edit-render="{name: 'input', attrs: {type: 'text'}}" />
        </vxe-table>
      </div>
    </el-form-item>
  </el-col>

我们在选择资产名称的时候,通过焦点事件弹出一个资产选择的对话框处理,如下所示的代码

events: {focus: assetNameFocusEvent}}

然后在事件中,我们弹出一个对话框来

assetNameFocusEvent ({ row, column }) {this.currentRow =rowthis.$refs.asset.show()
},

如果是下拉列表,我们可以绑定自己的数据源即可,如下面界面部门列表所示。

如果需要录入数据中的列表进行联动处理,如在事件ptypeChangeEvent中更新pnameList可以实现

<vxe-columnfield="attr3"title="Project type":edit-render="{name: '$select', options: ptypeList, props: {clearable: true}, events: {change: ptypeChangeEvent}}"></vxe-column>
<vxe-columnfield="attr4"title="Project name":formatter="formatPanmeLabel":edit-render="{name: '$select', options: pnameList, props: {clearable: true}}"></vxe-column>
//更新级联选项列表
updatePnameList (row) {
let ptype
=row.attr3
let pnameList
=[]if(ptype) {
let item
= this.cachePnameList.find(item => item.ptype ===ptype)if(item) {
pnameList
=item.pnameList
}
else{//模拟后台数据 Array.from(new Array(XEUtils.random(3, 8))).forEach((item, index) =>{
pnameList.push({
label: `${ptype}
-名称${index}`,
value: `${ptype}_${index}`
})
})
this.cachePnameList.push({ ptype, pnameList })
}
}
this.pnameList =pnameList
},
ptypeChangeEvent ({ row }) {
//类型切换时更新级联的下拉数据 row.attr4 = '' this.updatePnameList(row)
},

对于Vxe-table组件的列,它的edit-render标记它的类型处理。

列的类型可以是普通的input,也可以是其他类型,如数值类型,下拉列表,日期等类型,下面一些案例代码供参考。

<vxe-columntype="seq"width="60" />
<vxe-columnfield="name"title="Name":edit-render="{name: 'input', attrs: {type: 'text'}}"></vxe-column>
<vxe-columnfield="nickname"title="Role":edit-render="{name: 'input', attrs: {type: 'text', placeholder: '请输入昵称'}}"></vxe-column>
<vxe-columnfield="sex"title="Sex":edit-render="{name: '$select', options: sexList}"></vxe-column>
<vxe-columnfield="sex2"title="多选下拉":edit-render="{name: '$select', options: sexList, props: {multiple: true}}"></vxe-column>
<vxe-columnfield="num1"title="Amount":edit-render="{name: '$input', props: {type: 'float', digits: 2}}"></vxe-column>
<vxe-columnfield="date12"title="Date":edit-render="{name: '$input', props: {type: 'date', placeholder: '请选择日期'}}"></vxe-column>
<vxe-columnfield="date13"title="Week":edit-render="{name: '$input', props: {type: 'week', placeholder: '请选择日期'}}"></vxe-column>

<vxe-columnfield="age"title="ElInputNumber"width="160":edit-render="{}">
  <template#edit="{ row }">
    <el-input-numberv-model="row.age":max="35":min="18"></el-input-number>
  </template>
 </vxe-column>

其中列的Type类型如下定义所示,可以选择特定的类型。

如果input类型,那么可以选择各种格式的输入界面,如下所示。

具体的信息可以查看控件的API说明:
https://xuliangzhan_admin.gitee.io/vxe-table/#/column/api

以上就是关于在Winform中直接录入表格数据和在Vue&Elment中直接录入表格数据的介绍,总体来说都是比较方便的,数据可以分开主从表录入,也可以一并提供数据对象给后台一并处理,具体根据情况处理即可。

在Winform开发领域开发过十多年的项目中,见证着形形色色的架构和官方技术的应用,从最早类似Winform模式的WebForm技术,到接着的JQuery+界面组件,再到Asp.net Core的技术应用,以及后来的纯前端技术出现,Angular、React、Vue等前端技术和语言的广泛应用,Web应用越来越多,也越来越精细化,从最初的单服务,到单服务+分布式缓存,再到微服务集群的各种服务架构,Web开发的体系越来越庞大和复杂化,虽然引入更多先进技术、更多优秀架构,但同时也意味着更高的技术门槛,以及全栈式的开发需求或要求,Winform开发依旧占领一席之地。本篇随笔针对Winform开发的技术特点,针对自己多年的开发经验,对Winform开发的快速、健壮、解耦的一些领域提出几点微薄建议,抛砖引玉,希望大家多加指正。

1、Winform的技术概括

另一方面,作为Winform的替代者,Silverlight的盛行以及没落,WPF的逐步应用并重用,以及统一的通用 Windows 平台 (UWP),都充当着Winform的终结者的大任,不过即使应用范围缩减,Winform依旧很顽强,即使.netcore的大环境下,也依旧保留在Visual Studio 2022中,可见Winform开发的生命力之顽强。

Winform开发对比其他有不少优点,主要的特点还是开发方便,基于一定的框架,可以快速开发特定的业务管理系统。下面就WInform这个技术应用做一个个人的概括。

1)用户体验

在Winform应用里面,和其他Web系统比起来,它的用户体验是最好的,而且界面响应速度也比Web界面来的快捷一些,由于很多情况下,用户考虑使用方便性,如一些报表的展示、打印、导入导出文件的处理等常规的操作,都还是习惯使用Winform这种定制型非常好的界面来处理,毕竟大多数情况下,单位都有一套业务和数据的管理系统来处理这些业务。

2)数据敏感

另外很多情况下,如一些事业单位、机构什么,他们的数据是比较敏感的,不希望对外公开,网络的引入会提供数据外泄的可能,另外它们也是经常处于内网的环境下,因此一个单机版的程序就可以搞定他们的日常业务处理了,这种特别的业务环境,注定了使用Winform来处理会更胜一筹。

3)开发便利

Winform开发的程序,发布共享比较容易,直接安装就可以使用,可以不需要部署在云端(虽然我的混合框架方式可以访问Web API、WCF等服务获取数据,透明的数据处理);而且Winform的界面开发起来非常方便,结合界面套件,可以做出非常棒的界面效果。另外从开发角度上讲,Web前端的技术淘汰非常快,Winform的技术积累反而是在逐步加固的过程,因此对于一些开发人员来说,迭代Winform开发的应用会更加方便,也更加熟练,因此只要客户在用,系统兼容,这种Winform的程序会一直保留下去。

4)应用广泛

Winform开发的程序,其应用领域真的是覆盖方方面面,包括常规的数据库应用,以及Socket套接字的处理,底层硬件串口或者Modbus协议对接,基础打印、读卡器、射频开发、USB、摄像头的控制处理等,可以说Web能做的Winform基本都能做,而Winform能做的Web不一定能做到。

2、如何实现Winform的模块化开发?

对于如何实现Winform应用的模块化开发,其实本身Winform来源于早期Delphi、VB时代,它们已经是在控件的拖动处理、事件驱动方面都已经完善至极,对一些简单的应用,拖动一些控件,再增加一些事件响应处理即可完成主要的功能,这个也是很多早期共享软件盛行的重要原因。

随着程序功能的日益复杂化,Winform程序呈现模块化,把一些功能处理、UI界面等按照横向或者竖向划分,进行界面性、功能性的不同程度的分离,如出现了很多界面组件,如ActiveSkin/Skin++、DotNetBar、DevExpress等等,它们负责界面的渲染和简单的功能响应,对于业务性的界面则只能由开发者完成处理,但是已经实现一定程度的横向功能分离了。

通过利用这些UI组件,使得我们的Winform界面更加丰富,就像穿上了很高贵的服饰,面目一新,不过它还是不能解决程序复杂度的问题,有时候因为界面特性的原因,可能引入更多的复杂性难题,提供更多更精细难懂的事件来处理。

在我们开发一个应用的时候,往往需要集成一些应用场景,如基础的用户、角色、机构、权限、日志等权限管理系统,以及字典参考,附件管理等内容,以及一些封装好的分页处理控件,公用类库等支持类库等。

因此我们在这个基础上开发Winform项目,会更加高效快速。

Winform开发的相关业务逻辑(包括界面UI),都可以封装在一个简单的DLL文件中,需要的时候进行引用和调用即可,使用起来非常方便,这种方便也使得Winform程序的模块化更加简单和高效。

通过在解决方案中引入相关开发好的模块,直接调用使用即可,非常快速简单。

开发程序模块,除了一些业务模块外,大多数都是类似拼积木的方式,把它们在主界面中引入即可。

而Winform模块的调用,只需要简单的调用或者带入相关参数即可。

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

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

软件和建筑工程很类似,都是需要构建一个庞大而功能完整的一个系统,而工程化也意味着需要多人协作,那么就需要把一个庞大的系统横向或者纵向划分为各个可以独立施工完成的模块,虽然各个模块之间有所衔接或者交互,但是基本上可以以模块化的方式来构建,这个也是工程化开发的精髓所在。

以一个软件管理系统为例,我们尽可能把精力焦点放在客户的业务需求上,而对于常规的如权限控制、字典管理等一些常用的内容,由于它们的处理逻辑在特定领域上基本上比较固定一些,可以尽可能独立并重复使用。

3、如何实现Winform应用的解耦开发?

随着应用模块的复杂化,独立化,这个解耦的问题就是所有场景开发的共同问题,因此三板斧的操作大多数类似,虽然Winform特点又有所不同。

以数据库应用支持为例,对于不同模块的应用,可能需要同时支持不同数据库,通过配置实现切换不同数据库的调用。

因此在开发模块的时候,我们需要先定义统一的数据库处理接口,然后为不同类型的数据库访问做一个独立的实现处理。

在实际需求中,你往往不能决定客户需要用什么数据库,那么需要根据实际需求或者环境进行数据库类型的选型,如果是单机版为了方便可以使用SQLite,如果是已有业务系统或者需要响应速度快一些的,那么考虑使用SQLServer或者Mysql、有些历史原因的可能会用PostgreSQL或者Oracle等等。那么框架的弹性就需要支持多种数据库的了,这种支持不能导致太大的工作量最好,否则会弄得焦头烂额的。

框架底层数据库访问采用了微软企业库实现,因此在处理多种数据库访问的时候,能够提供统一的访问处理操作,同时对不同的数据库支持操作也是非常不错的。下图是框架底层数据库的支持情况。

采用不同的数据库,我们需要为不同数据库的访问层进行生成处理,如为SQLServer数据的表生成相关的数据访问层DALSQL,里面放置各个表对象的内容,不过由于采用了相关的继承类处理和基于数据库的代码生成,需要调整的代码很少。

而对于更高一级的应用数据来源,有可能来源于Web API,也有可能来源于数据库,也可能来源于WCF服务等方式的综合处理,那么可以再次利用接口,实现分离的方式,再次将实现解耦,标准的三板斧处理操作即可。

混合框架的分层架构图

4、如何实现Winform开发的简单化开发

由于Winform开发业务的日益复杂化,我们往往需要花费很多精力来做一些基础性的东西,虽然逻辑很简单,但是一些繁琐性的工作却很难避免,那么我们平时进行Winform开发的时候,如何才能实现Winform开发中的简化处理呢?

首先我们通过模块化来切分大项目的复杂度,然后再次进行类继承的方式减少类级别的代码,重用基类特性。

前面我们介绍采用了一些模块化的方式来组装项目,可以简单的理解为积木组合的方式。如权限管理模块、字典管理模块、自动更新模块、人员管理模块、通讯录管理模块、分页控件、公用类库等方面,这些可以在项目中根据情况采用对应的模块,非常高效,整个主体框架也有对应接入这些模块的案例代码供参考,因此可以更加直观明了。

而另一方面,我们在对类的重用方面,也是精益求精,尽量做到精简重复代码,提高开发效率的目的。

界面类提供封装继承,减少界面的一些繁琐设置。

界面通过基类的封装处理,我们可以把大多数通用的处理或者逻辑调用顺序进行封装,子类实现一些特殊的函数即可,同时可以统一调用基类的处理操作。

我们以常规业务模块界面来分析,主要有明细查看或编辑界面、列表界面,那么这些界面为了方便使用,我们可以抽象一部分界面处理逻辑或者共有部分内容,放在界面基类BaseEdit或者BaseDock等基类里面,如下界面所示。

基础业务类,我们同样也是采用重用基类方式,实现接口函数的封装处理。

以及业务类继承关系,当然还有其他更多的,基本上都提供一些基类来处理,简化代码。

Web API的控制器设计对象继承关系,如下所示:

除了这些基础性的代码级别的简化外,我们更高层次的简化,可以通过数据库=》代码生成的方式简化处理,通过数据库信息,我们可以进一步生成我们应用业务的相关类或者界面部分,从而可以快速提高开发效率。

如对于普通的Winform界面项目,利用代码生成工具Database2Sharp,我们生成后的项目结构视图如下所示。

而对于混合框架方案的项目,我们生成的界面项目结构视图如下所示。

对于Winform开发,可以根据Winform框架或者混合框架的窗体界面类,生成标准的界面代码,列表界面默认具有分页查询、导入导出、高级查询、编辑、删除事件绑定,编辑界面则具有获取数据并显示在控件,保存后执行更新或者写入的操作。

如在代码生成工具里面,设置主从表的界面生成如下所示。

生成的标准的主从表界面如下界面所示。

通过框架项目定制化的代码生成工具,可以快速的生成我们所需要的相关分层的代码,并继承好相关的类关系,以及标准调用代码,可以极大提高我们应用模块的开发速度,而且由于利用通过检验的标准件,开发出来的模块,除了快速外,也是健壮的。

另外们把常规的列表界面,新增、编辑、查看、导入等界面放在一起,除了列表页面,其他内容以弹出层对话框的方式进行处理,如下界面示意所示。

在基于DevExpress的多文档窗口界面中,我们一般使用XtraTabbedMdiManager来管理多文档窗口的一些特性,如顶部菜单,页面的关闭按钮处理,以及一些特殊的设置,本篇随笔介绍这些特点,并着重介绍它的几个属性设置,使得在Winform框架的多文档界面中实现双击子窗口单独弹出或拖出及拽回的处理。

1、Winform框架的多文档界面效果

多文档界面有其非常好的窗口内容管理便利,现在很多BS的窗口,都讲究多窗口话,主要就是为了方便不同内容的展示及参考处理。在Winform的DevExpress界面中,我们使用了DevExpress控件XtraTabbedMdiManager的特点,使得管理多文档界面更加方便。

XTraTabbedMdiManager控件这个控件可以实现在右边以Tab方式展现多文档布局,这样对用户操作来说,可以一次性打开多个窗体进行操作,方便很多,也必将美观,是一种常见的布局展现。为了在窗体激活的时候,在顶部显示关闭按钮,其他的不显示,那么需要设置XTraTabbedMdiManager控件的ClosePageButtonShowMode = InActiveTabPageHeader 即可实现了。

我们先来看看Winform开发框架中的多文档界面效果

这些都是基于多文档界面的展示,可以有效的整理相关的内容划分,并很好的实现窗口的管理。

2、多文档界面中实现双击子窗口单独弹出或拖出及拽回的处理

多文档窗口的拖入拖出的界面效果如下所示。

这些设置处理起来其实很容易,只需要设置好xtraTabbedMdiManager对象即可。

只需要在xtraTabbedMdiManager1属性中设置FloatOnDoubleClick和FloatOnDrag为True,并且设置FloatPageDragMode为FullWindow即可,如下界面设置所示。

而如果需要让顶部可关闭窗口的按钮出现在活动选项卡窗口上,只需要设置

ClosePageButtonShowMode为InActiveTabPageHeader即可,

如下界面所示。

另外可以设置多文档的选项卡右键菜单,增加一些关闭窗口的操作,如下所示。

那么添加一个popupmenu对象,让设置好菜单选项及事件处理,如下所示

并指定它的Ribbon对象即可。

利用xtraTabbedMdiManager组件,可以为我们实现丰富的界面布局展现,当然,要设计好一个系统界面,除了考虑界面的布局美观性、还要考虑图标的协调性、还有就是整体的框架,要可以比较好的适应这些布局控件的操作。

以上知识点,希望对大家在开发Winform界面效果上有所参考。