2023年2月

在一般的检索界面中,基于界面易用和美观方便的考虑,我们往往只提供一些常用的条件查询进行列表数据的查询,但是有时候一些业务表字段很多,一些不常见的条件可能在某些场景下也需要用到。因此我们在通用的查询条件之外,一般可以考虑增加 一个高级查询的模块来管理这些不常见条件的查询处理。本篇随笔基于这个需求,综合ABP框架的特点,整合了高级查询模块功能的处理。

1、高级查询模块的回顾

我们知道,在界面布局中,一般常见的查询条件不能太多,否则会显得臃肿而且占用太多空间,非常不美观,因此常见的查询都是提供寥寥几个的输出条件进行列表记录的查询的。

又或者一些更多内容的界面,我们也是仅仅提供多几个条件,其他的想办法通过高级查询界面进行查询管理。

在早期博客里面《
Winform开发框架之通用高级查询模块
》,我曾经介绍过一款通用的高级查询界面处理,用在Winform框架里面,可以对数据表更多的字段进行统一的查询处理。

对于内容较多的查询,我们可以在主界面增加一个高级查询按钮入口,如上图所示,单击后,显示一个所有字段的列表,如下界面。

一般来说,查询条件分为文本输入,如姓名,邮件,名称等这些。

日期类型条件输入界面:

数字类型条件输入界面:

输入以上几种条件后,高级查询界面里面会显示友好的条件内容,确保用户能够看懂输入的条件,如下所示是输入几个不同类型的条件的显示内容。

以上是高级查询模块的思路,整体界面和处理逻辑虽然可以采用,但是在ABP框架模式下,以前的处理方式有所不同了,下面详细介绍一下如何在ABP框架模块下整合这个高级查询模块的内容。

2、ABP框架模块下的高级查询处理

我们先来了解一下最终在ABP框架下整合的高级查询模块界面如下所示。

可以设置一些模糊查询条件,以及一些区间的查询值,如下所示。

这个模块是以ABP框架的Web API获取数据,并通过Winform界面进行调用,从而形成了一个ABP+Winform的框架体系。

前面ABP框架系列介绍过,我们一般使用GetAll和分页条件DTO进行数据的检索,如下是产品分页DTO的定义

    /// <summary>
    ///用于根据条件分页查询,DTO对象/// </summary>
    public class ProductPagedDto : PagedAndSortedInputDto

而PagedAndSortedInputDto也是自定义的类,它主要用来承载一些分页和排序的信息,如下所示

    /// <summary>
    ///带有排序对象的分页基类/// </summary>
    public classPagedAndSortedInputDto : PagedInputDto, ISortedResultRequest
{
/// <summary> ///排序信息/// </summary> public string Sorting { get; set; }

其中的PagedInputDto也是自定义类,主要承载分页信息。

    /// <summary>
    ///分页对象/// </summary>
    public classPagedInputDto : IPagedResultRequest
{
[Range(
1, int.MaxValue)]public int MaxResultCount { get; set; }

[Range(
0, int.MaxValue)]public int SkipCount { get; set; }publicPagedInputDto()
{
MaxResultCount
= int.MaxValue;
}
}

这样的构建,我们可以传递分页和排序信息,因此在GetAll函数里面,就可以根据这些条件进行数据查询了。

而我们通过重写过滤条件和排序处理,就可以实现数据的分页查询了。对于产品信息的过滤处理和排序处理,我们重写函数如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override IQueryable<Product>CreateFilteredQuery(ProductPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID .WhereIf(!input.ProductNo.IsNullOrWhiteSpace(), t => t.ProductNo.Contains(input.ProductNo)) //如需要精确匹配则用Equals .WhereIf(!input.BarCode.IsNullOrWhiteSpace(), t => t.BarCode.Contains(input.BarCode)) //如需要精确匹配则用Equals .WhereIf(!input.MaterialCode.IsNullOrWhiteSpace(), t => t.MaterialCode.Contains(input.MaterialCode)) //如需要精确匹配则用Equals .WhereIf(!input.ProductType.IsNullOrWhiteSpace(), t => t.ProductType.Contains(input.ProductType)) //如需要精确匹配则用Equals .WhereIf(!input.ProductName.IsNullOrWhiteSpace(), t => t.ProductName.Contains(input.ProductName)) //如需要精确匹配则用Equals .WhereIf(!input.Unit.IsNullOrWhiteSpace(), t => t.Unit.Contains(input.Unit)) //如需要精确匹配则用Equals .WhereIf(!input.Note.IsNullOrWhiteSpace(), t => t.Note.Contains(input.Note)) //如需要精确匹配则用Equals .WhereIf(!input.Description.IsNullOrWhiteSpace(), t => t.Description.Contains(input.Description)) //如需要精确匹配则用Equals//状态 .WhereIf(input.Status.HasValue, t => t.Status==input.Status)//成本价区间查询 .WhereIf(input.PriceStart.HasValue, s => s.Price >=input.PriceStart.Value)
.WhereIf(input.PriceEnd.HasValue, s
=> s.Price <=input.PriceEnd.Value)//销售价区间查询 .WhereIf(input.SalePriceStart.HasValue, s => s.SalePrice >=input.SalePriceStart.Value)
.WhereIf(input.SalePriceEnd.HasValue, s
=> s.SalePrice <=input.SalePriceEnd.Value)//特价区间查询 .WhereIf(input.SpecialPriceStart.HasValue, s => s.SpecialPrice >=input.SpecialPriceStart.Value)
.WhereIf(input.SpecialPriceEnd.HasValue, s
=> s.SpecialPrice <=input.SpecialPriceEnd.Value)
.WhereIf(input.IsUseSpecial.HasValue, t
=> t.IsUseSpecial == input.IsUseSpecial) //如需要精确匹配则用Equals//最低折扣区间查询 .WhereIf(input.LowestDiscountStart.HasValue, s => s.LowestDiscount >=input.LowestDiscountStart.Value)
.WhereIf(input.LowestDiscountEnd.HasValue, s
=> s.LowestDiscount <=input.LowestDiscountEnd.Value)//创建日期区间查询 .WhereIf(input.CreationTimeStart.HasValue, s => s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}
/// <summary> ///自定义排序处理/// </summary> /// <param name="query">可查询LINQ</param> /// <param name="input">查询条件Dto</param> /// <returns></returns> protected override IQueryable<Product> ApplySorting(IQueryable<Product>query, ProductPagedDto input)
{
//按创建时间倒序排序 return base.ApplySorting(query, input).OrderByDescending(s => s.CreationTime);//时间降序 }

虽然我们一般在界面上不会放置所有的条件,但是高级查询模块倒是可以把分页条件DTO里面的条件全部摆上去的。

高级查询模块的条件如下所示。

我们高级查询里面的条件还是以GetAll里面的对象分页查询Dto里面的属性,我们需要根据这些条件进行构建,也需要以这些属性的类型进行一个控件的选择。

因此我们需要一个属性的名称说明,以及在高级查询模块的列表界面中对显示那些字段进行控制,如下代码所示。

        privateFrmAdvanceSearch dlg;/// <summary>
        ///高级查询的操作/// </summary>        
        private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";

通过 SetFieldTypeList<ProductPagedDto> 的处理,我们把分页对象的查询属性和类型赋值给了高级查询模块,让它根据类型来创建不同的输入显示,如常规的字符串、数值区段、日期区段,下拉列表等等。

对于下拉列表,我们需要绑定它的数据源,如下代码所示。

 dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表
 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表

而对于一些常规的固定列表,也可以以类似的方式加入下拉列表

    //固定转义的列表
    var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);

或者

    dlg.AddColumnListItem("Sex", "男,女");//固定列表

因此整个调用高级查询模块的代码如下所示

    privateFrmAdvanceSearch dlg;/// <summary>
    ///高级查询的操作/// </summary>        
    private async voidAdvanceSearch()
{
if (dlg == null)
{
dlg
= newFrmAdvanceSearch();
dlg.SetFieldTypeList
<ProductPagedDto>();//通过分页对象获取查询属性和类型 dlg.ColumnNameAlias = awaitProductApiCaller.Instance.GetColumnNameAlias();
dlg.DisplayColumns
= "ProductNo,BarCode,MaterialCode,ProductType,ProductName,Unit,Price,SalePrice,SpecialPrice,IsUseSpecial,LowestDiscount,Note,Description,Status,CreatorUserId,CreationTime";#region 下拉列表数据dlg.AddColumnListItem("ProductType", await DictItemUtil.GetDictListItemByDictType("产品类型"));//字典列表 dlg.AddColumnListItem("Status", await DictItemUtil.GetDictListItemByDictType("产品状态"));//字典列表//固定转义的列表 var specialList = new List<CListItem>() { new CListItem("特价", "True"), new CListItem("一般", "False") };
dlg.AddColumnListItem(
"IsUseSpecial", specialList);//dlg.AddColumnListItem("Sex", "男,女");//固定列表//dlg.AddColumnListItem("Credit", await ProductApiCaller.Instance.GetFieldList("Credit"));//动态列表 #endregiondlg.ConditionChanged+= newFrmAdvanceSearch.ConditionChangedEventHandler(dlg_ConditionChanged);
}
dlg.ShowDialog();
}

在处理获取数据GetData函数的时候,我们需要根据高级查询进行一定的切换,以便显示正确的过滤条件,如下代码所示是获取数据的处理。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<ProductDto>>GetData()
{
ProductPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else{//构建分页的条件和查询条件 pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 ProductNo = this.txtProductNo.Text.Trim(),
BarCode
= this.txtBarCode.Text.Trim(),
MaterialCode
= this.txtMaterialCode.Text.Trim(),
ProductType
= this.txtProductType.Text.Trim(),
ProductName
= this.txtProductName.Text.Trim(),
Description
= this.txtDescription.Text.Trim(),
};
//日期和数值范围定义//创建时间,需在ProductPagedDto中添加DateTime?类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreationTimeStart =CreationTime.Start;
pagerDto.CreationTimeEnd
=CreationTime.End;

}
var result = awaitProductApiCaller.Instance.GetAll(pagerDto);returnresult;
}

在高级查询的处理方式下,我们是传入一个列表的分页对象属性,然后传入一个分页DTO对象,就可以构建出我们需要的分页查询条件,传递给Web API端获取对应条件的数据了。

    pagerDto = new ProductPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
= dlg.GetPagedResult(pagerDto);

而高级查询模块,所需要处理的逻辑就是需要根据不同的属性类型,赋值常规的属性值或者区段属性值,从而构建出分页对应的属性条件即可。

如果是区段(包括日期或者数值)的,我们分页查询条件里面,会有一个ABCStart,ABCEnd的对象属性,依照这个规则,获取到对应的用户输入,采用反射方式赋值DTO对象即可。

前面介绍了很多ABP系列的文章,一步一步的把我们日常开发中涉及到的Web API服务构建、登录日志和操作审计日志、字典管理模块、省份城市的信息维护、权限管理模块中的组织机构、用户、角色、权限、菜单等内容,以及配置管理模块,界面的高级查询处理等内容,同时我们把整个开发理念结合我们的代码生成工具Database2Sharp进行快速的开发,快速开发分为两个部分:一个是Web API部分的ABP框架代码生成,包括领域实体层、应用服务层、公用DTO和接口层、API调用Caller层等内容的生成,并且整合了我们封装的基类,以便生成的代码更加简洁,但却具有更加强大、易用的功能函数;另一个快速生成的部分是界面生成,我们根据我们在Winform领域多年的开发,整合了分页、公用类库和Winform基础界面类的基础上实现了快速的Winform界面生成,并且Winform界面生成可以根据需要配置列表查询条件、列表显示内容,编辑界面的字段显示等等内容,并且生成的Winform界面自动具有和ABP快速框架整体调用通用规则。

1、ABP框架背景知识介绍

ABP
是ASP.NET Boilerplate的简称,ABP是一个
开源
且文档友好的
应用程序框架
。ABP不仅仅是一个框架,它还提供了一个
最徍实践
的基于
领域驱动设计(DDD)
的体系结构模型。学习使用ABP框架也有一段时间了,一直想全面了解下这个框架的整个来龙去脉,并把想把它使用历程整理成一个系列出来,不过一直没有下笔来写这篇文章的开篇,就是希望能够深入了解,再深入了解一些,希望自己能够理解透彻一些,不能误人子弟,也不想和网上千篇一律的翻译官网的内容,官网的英文介绍也已经很详细了,于是我觉得还是以实际使用的过程进行一定的整理会更好。

初次了解ABP框架,对它还是非常惊艳的,它基本上是.NET 领域架构的集大成者,几乎囊括了我们.NET领域排的上名的各种技术应用,而且它本身可以支持.net framework和.net core两种技术流派,对它的介绍也是非常感兴趣。

1)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层使用。

除了这些重要特性外,ABP框架还有很多一些特别的功能或者概念。

  • 多租户支持(每个租户的数据自动隔离,业务模块开发者不需要在保存和查询数据时写相应代码;
  • 软删除支持(继承相应的基类或实现相应接口,会自动实现软删除)
  • 系统设置存取管理(系统级、租户级、用户级,作用范围自动管理)
  • EventBus实现领域事件(Domain Events)
  • 模块以及模块的依赖关系实现插件化的模块处理等等

ABP框架主要还是基于领域驱动的理念来构建整个架构的,其中领域驱动包含的概念有 域对象Entities、仓储对象Repositories、域服务接口层Domain Services、域事件Domain Events、应用服务接口Application Services、数据传输对象DTOs等。

ABP官方网站:http://www.aspnetboilerplate.com,从里面可以查看很详细的案例和文档说明,可以根据需要下载不同类型的基础框架。

ABP GitHub源码地址:https://github.com/aspnetboilerplate,可以下载整个基础的框架内容,以及相关的样板案例代码。

基于ABP框架基础上,我们整理完善了整个权限体系,以及基于这个基础上进行的业务系统快速开发,我们整理后的ABP快速开发框架的架构图示,如下图所示(以字典模块为例说明)

针对Web API接口调用的封装,为了适应客户端快速调用的目的,这个封装作为一个独立的封装层,以方便各个模块之间进行共同调用。

2、基于Winform的ABP快速开发框架功能介绍

1)登陆界面

2)主体框架界面

主体框架界面采用的是基于菜单的动态生成,以及多文档的界面布局,具有非常好的美观性和易用性。

左侧的功能树列表和顶部的菜单模块,可以根据角色拥有的权限进行动态构建,不同的角色具有不同的菜单功能点,如下是测试用户登录后具有的界面。

3)用户管理界面

用户列表包括分页查询、导入、导出用户数据,以及可以利用右键菜单进行新增、编辑用户记录,或者对指定用户进行重置密码操作。

用户编辑界面如下所示。

当然可以查看这个用户本身拥有的权限功能点,如下界面所示。

用户数据导入 ,可以根据指定模板的格式进行填写后,然后导入指定的文件内容,如下所示。

导出文件则是把列表中现有的数据进行导入,导出后提示是否打开Excel文件进行查看。

4)组织机构管理

组织机构主要就是一个层级的对象关系,一般包含但不限于公司、部门、工作组等的定义,其中组织机构包含用户成员和角色成员的关系,如下界面所示。

组织机构的树形列表可以进行拖动实现不同层级的变化

或者通过右键菜单进行编辑修改操作

组织机构可以修改机构名称和对应的父类节点,如下界面所示。

组织机构包含的成员可以添加多个人员记录,添加界面如下所示。

添加角色界面如下所示。

5)角色管理

角色信息没有层级关系,可以通过列表展示。

其中角色包含权限分配、角色成员和拥有菜单的维护,如下是角色编辑界面,包含角色基本信息、拥有权限、包含成员、拥有菜单的管理等。菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单。

菜单编辑界面如下所示。

角色的权限包含系统可以用的权限,并可以勾选为角色设置所需的功能点,如下界面所示。

用户成员则和机构的用户管理一样,可以指定多个用户,这里不再赘述。

而菜单对于角色来说,应该是一种界面资源,可以通过配置进行管理对应角色用户的菜单,如下界面所示。

通过配置好的菜单,用户登录系统后,系统根据当前用户具有的菜单项目,动态构建显示对应的列表菜单和工具栏菜单。

6)功能管理

严格来说,ABP框架并没有统一管理好权限功能点的,它没有任何表来存储这个功能集合,而是通过派生AuthorizationProvider的子类来定义权限功能点

我在这个基础上引入了一个权限功能的表用来存储功能点的,然后提供管理界面来动态维护这些功能点。如下界面所示。

我们刚才在角色里面看到可以分配的权限内容,就是基于这个权限表的信息展示。

这样我们可以动态添加或者批量添加所需要的功能点,并且和整个权限管理模块串联起来,形成一个完整的控制体系。

另外我们还可以通过左侧树列表的右键菜单管理列表,如下所示可以级联删除对应的节点及其下面所有子节点。

系统登录后,客户端自动获取对应用户的角色功能点,然后我们每次打开一个新的业务窗体,客户端会进行界面的权限逻辑控制,如果没有权限的,那么不可以访问操作,如下是禁止了产品信息的导入、导出、新增、编辑等操作功能,如下界面所示产品界面被动态取消相关权限后,界面禁止了某些操作功能。

7)菜单管理

Winform主界面的开发,基本上都是标准的界面,在顶部放置工具栏,中间区域则放置多文档的内容,但是在顶部菜单比较多的时候,就需要把菜单分为几级处理,如可以在顶部菜单放置一二级菜单,这种方式在一般功能点不算太多的情况下,呈现的界面效果较为直观、也较为美观。不过随着一些系统功能的增多,这种方式可能就会显得工具栏比较拥挤,那么我们是否可以在左侧放置一个树形列表,这样通过树形列表的收缩折叠,就可以放置非常多的菜单功能了。

在ABP快速开发框架里面,我们依旧采用Winform开发框架里面用到的菜单维护方式,在权限模块系统中维护菜单内容,并在用户登录系统后,动态加载菜单展示,并通过菜单的配置信息,可以动态展示不同的窗体信息。

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

左侧列表依旧可以通过右键菜单进行维护管理。

双击树列表或者右侧的列表,都可以对已有的菜单进行编辑,菜单编辑界面如下所示。

我们可以通过选择图标按钮进行菜单图标的选择,如下是选择菜单图片的界面。

这样我们可以采用DevExpress内置漂亮的系统图标,也可选择系统文件里面的图标文件。

如果用户登录系统后,觉得自己管理的菜单有些多,那么可以通过菜单配置的方式,选择性的呈现某些菜单,把其他部分的菜单隐藏起来,这个就是自定义应用菜单界面,界面如下所示。

8)系统登录日志

登录日志,这个就是用户尝试登录的时候,留下的记录信息,其中包括用户的登录用户名,ID,IP地址、登录时间,以及登录是否成功的状态等信息。

这个登录日志,就是在系统登录尝试的时候,留下的记录,可供管理员进行跟踪了解某个账户的使用情况,也可以根据这些登录信息进行一个统计报表的分析。

9)系统审计日志

审计日志,设置我们在访问或者调用某个应用服务层接口的时候,横切面流下的一系列操作记录,其中记录我们访问的服务接口,参数,客户端IP地址,访问时间,以及异常等信息,这些操作都是在ABP系统自动记录的,如果我们需要屏蔽某些服务类或者接口,则这些就不会记录在里面,否则默认是记录的。

审计日志的明细展示界面如下所示。

10)字典管理

字典管理界面,左侧列出字典类型,并对字典类型下的字典数据进行分页展示,右侧则利用分页展示对应字典类型的字典数据,字典管理界面如下所示。

新增或者编辑窗体界面如下

批量添加字典内容的界面如下所示。

省份城市行政区的模块管理,也主要是为了提供一个三级联动的字典列表给界面使用,因此这里对这些统计局的数据进行一个维护和展示出来,如下界面所示。

11)参数配置模块

一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查询来使用,本ABP快速开发框架引入了另外一种配置信息的定义,实现更加简化的处理。

参数配置管理模块界面如下所示。

配置模块使用主要特点是以键为操作对象,然后内容是JSON序列化后的内容,可以存储用户自定义的类的序列号字符串,通过整合了SettingsProvider.net组件,使得我们操作配置内容更加方便和动态化。

12) 附件管理模块

该模块其实是很通用的一个模块,例如我们的一些日常记录,可能会伴随着有图片、文档等的附件管理,如果为每个业务对象都做一个附件管理,或者每次开发系统都重新做,那么效率肯定没有直接采用通用的附件管理那么方便快捷了。

其实这个模块是基于我的Winform开发框架开发的,同样现在的ABP快速开发框架的Winform端依旧可以使用这些模块的界面和大概的处理规则,我们把它利用ABP框架的获取数据方式调整一下即可。

首先我们创建一个独立的控件,用于放到编辑数据记录窗口里面,如下所示。

这样在项目中集成(如数据编辑窗口),直接拖动这个控件到界面中,运行就可以看到下面的效果了。

由于一般创建记录的时候,给他指定一个GUID的附件组ID,这样我们在数据记录保存前,我们就可以上传附件了,如下所示。

而且在这个过程中,可以随时查看自己在该记录中已经上传的附件。如果附件不够,可以随时启动上传操作,附件支持多选文件,然后一次性,启动后台线程操作方式,把文件上传及附件记录保存到数据库。

由于是附件管理,因此有可能上传各种文件,包括Word文档、Excel文档、压缩文件,以及各种类型的图片,因此为了方便对图片的查看,这个控件集成了图片查看控件,可以非常方便直接读取图片附件的数据流作为对象展示,该图片控件支持对图片的滚动放大缩小、左右翻转、选择放大、图片移动、保存图片等功能,不需要查看,直接使用ESC退出即可。

附件管理在很多信息化管理系统中很普遍使用,例如我在病人管理系统界面里面,可以在一个界面里面分门别类管理很多影像学的图片资料,通过查看附件,可以看到其中一些图片附件的缩略图,需要进一步查看,可以双击图片即可实现预览效果。

通用附件管理模块,就是基于这些特点,实现图片、Excel文档、Word文档和PDF等文档的在线预览和管理操作,界面截图如下所示。

以上就是框架里面主要的模块内容展示,当然我们可以结合代码生成工具来快速开发自己的业务管理模块,而这些主要就是设计好数据库后,对框架代码和界面代码的统一生成后进行整合到主体框架里面即可,可以获得高效、统一、快速的开发体验。

详细的代码生成工具开发过程,可以了解随笔《
利用代码生成工具生成基于ABP框架的代码
》、《
ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程

ABP框架代码生成

最终根据根据选择数据库表信息,一键生成相关ABP架构分层代码,文件结构如下所示。

ABP框架的Winform界面开发

设置好后直接生成,代码工具就可以依照模板来生成所需要的WInform列表界面和编辑界面的内容了,如下是生成的界面代码。

放到VS项目里面,就看到对应的窗体界面效果了。

生成界面后,进行一定的布局调整就可以实际用于生产环境了,省却了很多时间。

系列文章列表如下所示:

DevExpress提供的树形列表控件TreeList和树形下拉列表控件TreeListLookupEdit都是非常强大的一个控件,它和我们传统Winform的TreeView控件使用上有所不同,我一般在Winform开发中根据情况混合使用这些控件,不过整体来看,基于DevExpress的TreeList和TreeListLookupEdit表现相对比较好看一些,本篇随笔主要介绍这两个控件在实际Winform项目中的使用处理。

1、DevExpress的TreeList控件使用

例如在菜单管理中,我们知道菜单一般情况下是层次节点的,我们为了直观显示的需要,一般把菜单用树列表控件进行展示,其中就会用到我们说的TreeList控件,如下界面所示。

其中TreeList和一个输入SearchControl来一起协同使用,可以提高界面的友好性,我们可以通过输入关键字进行节点的过滤显示。

如输入过滤内容后查询过滤树列表节点,如下所示,这样可以给用户快速模糊检索指定的树节点。

有了大概的感官认识,我们来了解下TreeList控件的使用情况

在菜单界面的设计视图下,我们添加一个ContextMenuStrip的右键菜单控件,然后编辑一些菜单项目,如下界面所示。

然后拖入一个图片集合的ImageCollection控件,并为它增加一些控件图片(也可以保留为空,然后动态指定,如本例一样)

初始化树列表,我们一般分为几个函数,一个是初始化树列表,一个是绑定查询过滤的处理,一个是把数据绑定到树列表上去,如下代码所示。

        private async void FrmMenu_Load(objectsender, EventArgs e)
{
//列表信息 InitTree();
InitSearchControl();
awaitBindTree();
}

上面使用了异步操作,我们一般也可以不用异步,这里根据情况处理吧。

初始化树列表的操作代码如下所示。

        /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();//添加显示列 this.tree.Columns.Add(new TreeListColumn { FieldName = "Id", Caption = "Id" });//增加一个隐藏的字段,存储需要的ID this.tree.Columns.Add(new TreeListColumn { FieldName = "Name", Caption = "菜单名称", Width = 160, VisibleIndex = 0});//设置树控件的层次关系及属性 tree.KeyFieldName = "Id";
tree.ParentFieldName
= "PID";this.tree.OptionsBehavior.Editable = false;this.tree.OptionsView.EnableAppearanceOddRow = true;this.tree.OptionsView.EnableAppearanceEvenRow = true;this.tree.OptionsDragAndDrop.DragNodesMode = DragNodesMode.None;//不允许拖动//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{//int maxCount = this.imageCollection1.Images.Count;//var index = e.Node.Level < maxCount ? e.Node.Level : 0;//e.SelectImageIndex = index; var id = string.Concat(e.Node.GetValue(Id_FieldName));int index = 0;
IdIndexDict.TryGetValue(id,
outindex);
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}
private async voidFocusedNodeChanged()
{
if (this.tree.FocusedNode != null)
{
IsNormalSearch
= false;awaitBindData();
}
}

初始化树列表的处理代码,我们还可以使用扩展函数进一步简化它,如下所示。

       /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();this.tree.OptionsDragAndDrop.DragNodesMode = DragNodesMode.None;//不允许拖动//控件扩展函数封装处理 this.tree.CreateColumn("Name", "菜单名称", 160, true);this.tree.InitTree("Id", "PID", null, false, false);//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e) =>{//int maxCount = this.imageCollection1.Images.Count;//var index = e.Node.Level < maxCount ? e.Node.Level : 0;//e.SelectImageIndex = index; var id = string.Concat(e.Node.GetValue(Id_FieldName));int index = 0;
IdIndexDict.TryGetValue(id,
outindex);
e.SelectImageIndex
=index;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}

初始化查询控件SearchControl的代码如下所示。

/// <summary>
///实现树节点的过滤查询/// </summary>
private voidInitSearchControl()
{
this.searchControl1.Client = this.tree;this.tree.FilterNode += (object sender, FilterNodeEventArgs e) =>{if (tree.DataSource == null)return;string nodeText = e.Node.GetDisplayText("Name");//参数填写FieldName if (string.IsNullOrWhiteSpace(nodeText))return;bool isExist = nodeText.IndexOf(searchControl1.Text, StringComparison.OrdinalIgnoreCase) >= 0;if(isExist)
{
var node =e.Node.ParentNode;while (node != null)
{
if (!node.Visible)
{
node.Visible
= true;
node
=node.ParentNode;
}
else break;
}
}
e.Node.Visible
=isExist;
e.Handled
= true;
};
}

上面只是初始化树列表控件,我们还需要对它进行数据的绑定显示,树列表的绑定主要代码就是数据绑定和图标的绑定。

this.tree.SelectImageList = this.imageCollection1;this.tree.DataSource = result.Items;

不过我们图标由于是从数据源里面动态构建的,因此需要存在一个集合里面,方便赋值给树列表控件,如下是完整的绑定代码。

/// <summary>
///绑定树的数据源/// </summary>
private asyncTask BindTree()
{
var pageDto = newMenuPagedDto();var result = awaitMenuApiCaller.Instance.GetAll(pageDto);//用来存放对应ID和index顺序的 IdIndexDict = new Dictionary<string, int>();//重新刷新节点图片 this.imageCollection1 = newImageCollection();this.imageCollection1.ImageSize = new Size(16, 16);if (result != null && result.Items != null)
{
foreach (var menuInfo inresult.Items)
{
var image =LoadIcon(menuInfo);this.imageCollection1.Images.Add(image);//为每个菜单ID添加一个图片序号,方便查找 if(!IdIndexDict.ContainsKey(menuInfo.Id))
{
int index = this.imageCollection1.Images.Count - 1;//最后一个序号 IdIndexDict.Add(menuInfo.Id, index);
}
}
}
//imageCollection变化了,需要重新赋值给treeList this.tree.SelectImageList = this.imageCollection1;this.tree.DataSource =result.Items;this.tree.ExpandAll();
}

如果我们需要获取指定树节点的绑定的值,我们可以通过当前的FocuseNode获取字段值,如下代码所示。

        /// <summary>
        ///编辑列表数据/// </summary>
        private asyncTask EditTreeData()
{
string ID = string.Concat(this.tree.FocusedNode.GetValue(Id_FieldName));if (!string.IsNullOrEmpty(ID))
{
FrmEditMenu dlg
= newFrmEditMenu();
dlg.ID
=ID;
dlg.OnDataSaved
+= newEventHandler(dlg_OnDataSaved);
dlg.InitFunction(LoginUserInfo, FunctionDict);
//给子窗体赋值用户权限信息 if (DialogResult.OK ==dlg.ShowDialog())
{
awaitBindTree();
}
}
}

以上就是TreeList控件的使用过程,主要注意的就是数据源的绑定和图标的绑定操作即可。

如果我们还允许树列表拖动,以便进行父位置的调整,那么可以对它进行一个拖动的事件处理。

   this.tree.CalcNodeDragImageIndex += new DevExpress.XtraTreeList.CalcNodeDragImageIndexEventHandler(this.Tree_CalcNodeDragImageIndex);this.tree.DragDrop += new System.Windows.Forms.DragEventHandler(this.Tree_DragDrop);this.tree.DragOver += new System.Windows.Forms.DragEventHandler(this.Tree_DragOver);
        private void Tree_DragOver(objectsender, DragEventArgs e)
{
TreeListNode dragNode
= e.Data.GetData(typeof(TreeListNode)) asTreeListNode;
e.Effect
= GetDragDropEffect(sender asTreeList, dragNode);
}
private async void Tree_DragDrop(objectsender, DragEventArgs e)
{
TreeListNode dragNode, targetNode;
TreeList tl
= sender asTreeList;
Point p
= tl.PointToClient(newPoint(e.X, e.Y));

dragNode
= e.Data.GetData(typeof(TreeListNode)) asTreeListNode;
targetNode
=tl.CalcHitInfo(p).Node;//移动后更新数据 var id = string.Concat(dragNode.GetValue("Id")).ToInt64();var pid = string.Concat(targetNode.GetValue("Id")).ToInt64();await OrganizationUnitApiCaller.Instance.Move(new MoveOrganizationUnitDto() { Id = id, ParentId =pid });awaitBindTree();

e.Effect
=DragDropEffects.None;
}
private void Tree_CalcNodeDragImageIndex(objectsender, CalcNodeDragImageIndexEventArgs e)
{
TreeList tl
= sender asTreeList;if (GetDragDropEffect(tl, tl.FocusedNode) ==DragDropEffects.None)
e.ImageIndex
= -1; //no icon elsee.ImageIndex= 1; //the reorder icon (a curved arrow) }

2、DevExpress的TreeListLookupEdit控件使用

TreeList和TreeListLookupEdit一般都会成对出现,一般我们需要调整父节点的时候,都会涉及到这个树形的下拉列表TreeListLookupEdit控件的。

为了方便,我们一般都定义一个自定义控件来处理这些下拉列表的绑定,因此不需要每次都绑定数据以及初始化代码。

        private void FunctionControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
InitTree();
BindTree();
}
}
/// <summary> ///初始化树/// </summary> /// <returns></returns> private voidInitTree()
{
this.txtMenu.Properties.ValueMember = "Id";this.txtMenu.Properties.DisplayMember = "Name";var tree = this.treeListLookUpEdit1TreeList;
tree.Columns.Clear();
//控件扩展函数封装处理 tree.CreateColumn("Name", "菜单名称", 160, true);
tree.InitTree(
"Id", "PID", null, false, false);//设置树的图标集合及逐级图标 tree.SelectImageList = this.imageCollection1;
tree.CustomDrawNodeImages
+= (object sender, CustomDrawNodeImagesEventArgs e) =>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};
}
/// <summary> ///绑定树列表/// </summary> /// <returns></returns> public voidBindTree()
{
var tree = this.treeListLookUpEdit1TreeList;//绑定列表数据 var result = AsyncContext.Run(() => MenuApiCaller.Instance.GetAll(newMenuPagedDto() { }));if (result != null && result.Items != null)
{
tree.DataSource
=result.Items;
tree.ExpandAll();
//展开所有 }//执行绑定后处理 if (DataBinded != null)
{
DataBinded(
null, null);
}
}

其中 AsyncContext.Run 是把异步函数当做同步使用,一般我们在用户控件上,我们都使用同步操作,避免数据绑定延迟导致没有处理

通过制定ValueMember和DisplayMember就可以正常显示内容,以及在后面存储需要的值操作。

  this.txtMenu.Properties.ValueMember = "Id";this.txtMenu.Properties.DisplayMember = "Name";

TreeListLookupEdit控件里面还是有一个TreeList控件的,这个控件的操作和我们上面说的TreeList控件操作完全一样,我们按树列表的方式初始化并绑定它即可。

界面效果如下所示。

树列表控件展开如下所示。

缓存在一个大型一点的系统里面是必然会涉及到的,合理的使用缓存能够给我们的系统带来更高的响应速度。由于数据提供服务涉及到数据库的相关操作,如果客户端的并发数量超过一定的数量,那么数据库的请求处理则以爆发式增长,如果数据库服务器无法快速处理这些并发请求,那么将会增加客户端的请求时间,严重者可能导致数据库服务或者应用服务直接瘫痪。缓存方案就是为这个而诞生,随着缓存的引入,可以把数据库的IO耗时操作,转换为内存数据的快速响应操作,或者把整个页面缓存到缓存系统里面。本篇随笔主要介绍利用ABP框架的支持实现的服务端缓存处理和Winform客户端缓存的处理。

1、缓存文章回顾

缓存的重要性不言而喻,我在博客园里面也写了很多缓存相关的文章,都是基于实际系统的总结处理。


Winform里面的缓存使用


使用ConcurrentDictionary替代Hashtable对多线程的对象缓存处理


在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理


.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用


在.NET项目中使用PostSharp,使用CacheManager实现多种缓存框架的处理


在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度


C#开发微信门户及应用(48) - 在微信框架中整合CacheManager 缓存框架

上面这些都是和缓存相关的内容,一般来说,缓存有很多方式的实现,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,为了方便我们一般使用.net的内存缓存处理,如果我们需要序列化缓存内容,那么可以采用
MemoryCache
或者Redis缓存等。后来我们通过综合考虑,基于配置方式选择不同缓存方式,在后端一般可以使用
CacheManager
的缓存处理。

如下面是基于常规架构的缓存处理分层,如果是基于Web API的服务端,那么缓存一般可以在Web API层或者它的下面一层。

如果是基于可序列化的缓存处理,它在IIS或者其他Web 容器重新启动后,缓存不会丢失,如在Redis里面,有相关的缓存记录如下所示。

2、ABP服务端缓存处理

ABP提供了缓存的抽象,它内部使用了这个缓存抽象。虽然默认的实现使用了MemoryCache,通过配置也可以使用Redis等缓存,缓存的主要接口ICacheManager。

我们可以在应用服务层的构造函数里面,注入该接口,然后使用该接口获得一个缓存对象。

官方简单的应用服务层代码如下所示。

public classTestAppService : ApplicationService
{
private readonlyICacheManager _cacheManager;publicTestAppService(ICacheManager cacheManager)
{
_cacheManager
=cacheManager;
}

实际上,我们应用服务层应该会更加复杂一些,如下是我们ABP快速开发框架的应用服务层的代码

[AbpAuthorize]public class DictDataAppService : MyAsyncServiceBase<DictData, DictDataDto, string, DictDataPagedDto, CreateDictDataDto, DictDataDto>, IDictDataAppService
{
/// <summary> ///缓存管理接口/// </summary> private readonlyICacheManager _cacheManager;private readonly IRepository<DictData, string>_repository;public DictDataAppService(IRepository<DictData, string> repository, ICacheManager cacheManager) : base(repository)
{
_repository
=repository;
_cacheManager
= cacheManager;//依赖注入缓存 }

对于字典模块,我们一般获取接口如下所示。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeID(stringdictTypeId)
{
IList
<DictData> list = await Repository.GetAllListAsync(s => s.DictType_ID ==dictTypeId);

Dictionary
<string, string> dict = new Dictionary<string, string>();foreach (DictData info inlist)
{
if (!dict.ContainsKey(info.Name))
{
dict.Add(info.Name, info.Value);
}
}
returndict;
}

如果我们需要把它构建一个缓存接口,那么处理方式就是对它进行一个简单包装即可,如下代码所示。

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(使用缓存)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeIDCached(stringdictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间 return await _cacheManager.GetCache("DictDataAppService")
.GetAsync(dictTypeId, ()
=>GetDictByTypeID(dictTypeId));
}

默认缓存超时是60分钟,它可以改。如果你超过60分钟没有使用缓存中的项,会从缓存中自动移除。你可以配置指定的缓存或是全部的缓存。

我们可以在应用服务层模块类ApplicationModule类里面进行修改,实现对缓存的过期设置。

            //系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间//所有缓存设置为2小时
            Configuration.Caching.ConfigureAll(cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromHours(2);
});
//特殊指定为5分钟 Configuration.Caching.Configure("DictDataAppService", cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromMinutes(5);
});

Redis 缓存集成

默认缓存管理使用的是内存缓存。所以,如果你有多个并发的Web服务器使用同个应用,可能会成为一个问题,在这种情况下,你需要一个分布/集中缓存服务,你就可以简单的使用Redis做为你的缓存服务器。

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

Redis的代码遵循ANSI-C编写,可以在所有POSIX系统(如Linux,
*
BSD, Mac OS X, Solaris等)上安装运行。而且Redis并不依赖任何非标准库,也没有编译参数必需添加。

下载地址:
https://github.com/MSOpenTech/redis/releases
下载,安装为Windows服务即可。

安装后作为Windows服务运行,安装后可以在系统的服务里面看到Redis的服务在运行了,如下图所示。

安装好Redis后,还有一个Redis伴侣Redis Desktop Manager需要安装,这样可以实时查看Redis缓存里面有哪些数据,具体地址如下:
http://redisdesktop.com/download

下载属于自己平台的版本即可

下载安装后,打开运行界面,如果我们往里面添加键值的数据,那么可以看到里面的数据了。

我们来看看如何在ABP框架中使用Redis缓存

我们现在应用服务层模块里面配置好使用Redis,如下代码所示

[DependsOn(
................
typeof(AbpRedisCacheModule) //Redis缓存加入 )]public classApplicationModule : AbpModule
{
public override voidPreInitialize()
{
............
//使用Redis缓存 int DatabaseId = -1;int.TryParse(AppSettingConfig.GetAppSetting("RedisCache", "DatabaseId"), outDatabaseId);string connectionString = AppSettingConfig.GetAppSetting("RedisCache", "ConnectionString");
Configuration.Caching.UseRedis(options
=>{
options.ConnectionString
=connectionString;
options.DatabaseId
=DatabaseId;
});
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间//所有缓存设置为2小时//Configuration.Caching.ConfigureAll(cache =>//{//cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);//});//特殊指定为5分钟 Configuration.Caching.Configure("DictDataAppService", cache =>{
cache.DefaultSlidingExpireTime
= TimeSpan.FromMinutes(5);
});
}

Host项目配置文件,Appsetting.json配置文件如下所示,增加RedisCache的配置节点。

使用缓存处理的应用服务层接口实现如下所示

        /// <summary>
        ///根据字典类型ID获取所有该类型的字典列表集合(使用缓存)/// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByTypeIDCached(stringdictTypeId)
{
//系统缓存默认为60分钟,可以在模块中配置具体的时间,配置后则是具体配置时间 return await _cacheManager.GetCache("DictDataAppService").GetAsync(dictTypeId, () =>GetDictByTypeID(dictTypeId));
}

在测试接口页面中进行测试

查看缓存管理里面的内容,可以发现已经具有值了,如下所示。

这样我们就可以很容易的从内存缓存切换到Redis的缓存了。

实体缓存

虽然ABP缓存系统出于普通的目的,但有一个EntityCache基类,可帮你缓存实体。如果我们通过它们的Id获取的实体,我们可以用这个基类缓存它们,就不用再频繁地从数据库查询。

不过这里不对这个进行细讲了。

3、Winform客户端的缓存处理

除了在服务端进行缓存测试外,为了提高客户端的响应速度,我们还可以在Winform客户端中使用内存缓存进行缓存一些不常变化的内容的,这样可以避免频繁的请求网络接口,获取接口数据。

ABP基础模块里面也提供了一个简单的缓存类,我们可以使用它进行缓存处理。

我曾经在之前一篇随笔《
在Winform开发框架中下拉列表绑定字典以及使用缓存提高界面显示速度
》对字典模块中使用缓存进行了说明,这个我们也可以调整为ABP快速开发框架中Winform客户端的字典处理方式。

ABP中有两种cache的实现方式:MemroyCache 和 RedisCache. 如下图,两者都继承至ICache接口。ABP核心模块封装了MemroyCache 来实现ABP中的默认缓存功能。 Abp.RedisCache这个模块封装RedisCache来实现缓存。

我们可以在Winform客户端中使用AbpMemoryCache是实现内存缓存的处理。

例如我们在界面模块中使用一个字典辅助类来封装对字典模块的调用,同时可以使用缓存方式进行获取。

使用缓存处理的逻辑,如下所示

主要就是判断键值是否存在,否则就设置内存缓存即可。

然后在编写一个字典控件的扩展函数,如下所示。

        /// <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()
{
txtInDiagnosis.BindDictItems(
"入院诊断");
txtLeaveDiagnosis.BindDictItems(
"最后诊断");//初始化代码 this.txtFollowType.BindDictItems("随访方式");this.txtFollowStatus.BindDictItems("随访状态");
}

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

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

录制视频也是一个很有意思的过程,一般需要提前准备好相关的提纲,以及整个思路的过程,在开始尝试的时候,可能还会因为有些特殊问题,而不得不重来一遍,录制视频开始用的是Snagit的软件,这个软件对视频的录制效果还是不错的,后来进一步了解并使用了OBS Studio的视频录制软件,这个软件是更专业的视频录制软件,而且它是免费的,而且提供很多控制功能,另外还可以把摄像头的视频投射在右下角(或者自己定义位置),对视频录制处理,本来想通过使用Adobe Premiere Pro 来进行一定的视频加工处理的,例如可以制作视频片头,特效等,不过这个软件是一个很强大的软件,暂时没有功夫细细研究吧。

下面是录制的一些视频,供读者了解我们开发框架的各个相关内容吧。

1、框架模块视频介绍(Winform、混合框架)

1)
公用类库介绍

2)
Winform分页控件

3)
字典管理模块介绍

4)
权限管理模块介绍

5)
附件管理模块

6)
自动升级模块

7)
人员管理模块

8)
主体框架介绍

2、Winform、混合框架开发视频

介绍Winform框架、混合框架的增量式开发的过程,各个特定主题操作介绍

1)Winform框架增量开发过程

2)Winform开发过程-客户信息表

3)Winform开发过程-产品信息表

4)混合框架增量开发过程

5)混合框架增量开发过程-产品信息

6)开发框架-底层数据库相关处理

7)开发框架-主从表界面生成

8)混合框架图片显示及存储

8)混合框架之WebAPI接入的增量开发过程

3、Bootstrap开发框架介绍

介绍Bootstrap开发框架的增量式开发的过程,各个特定主题操作介绍

1)Bootstrap开发框架介绍--基础框架

2)Bootstrap开发框架介绍--MVC框架

3)Bootstrap开发框架介绍--Bootstrap介绍

4)Bootstrap开发框架介绍—重要插件使用

4、ABP快速开发框架视频

介绍ABP快速开发框架的功能特点、框架界面,以及实现模块增量式开发的过程,各个特定主题操作介绍

1)ABP快速开发框架介绍

后续会增加相关主题,到时候再进行文章更新。