一般来说,录入数据的时候,我们都采用在一个窗体界面中,根据不同内容进行录入,但是有时候涉及主从表的数据录入,从表的数据有时候为了录入方便,也会通过表格控件直接录入。在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界面效果上有所参考。

在我较早的时候,就开始研究和介绍ABP框架,ABP框架相对一些其他的框架,它整合了很多.net core的新技术和相关应用场景,虽然最早开始ABP框架是基于.net framework,后来也全部转向拥抱.net core,而另一条线的ABP VNext则是定位从.net core开始的,基础类库以Volo.Abp开始。ABP框架和ABP VNext框架两者在基类和分层上,都很多几乎是一样的,不过ABP VNext框架是后来者,规避了很多前辈ABP框架的一些缺点,最明显的就是更加模块化(弊端就是管理的独立项目暴增),不过对于大项目来说,功能模块的切分也是必要的。ABP VNext是一个内容很丰富的架构体系,因此准备做一个系列介绍ABP VNext相关架构的知识,本篇随笔介绍它的一些框架基础类继承关系。

1、ABP VNext技术概述

ABP VNext框架如果不考虑在微服务上的应用,其集成使用的难度会降低一个层级,不过ABP VNext和ABP框架一样,基础内容都会设计很多内容,如数据库都支持Oracle、SQLServer、MySql、PostgreSQL、SQLite,都有利用Redis作为分布式缓存,使用RabbitMQ作为事件总线的消息处理方式,使用MongoDB的NoSQL类型数据库作为特殊数据的存储服务,使用Quartz/HangFire作为定时任务的处理等。如果考虑引入微服务的话,会更需要了解
IdentityServer
服务,以及了解
Ocelot
库管理网关,使用
Elasticsearch
&
Kibana
来存储和可视化日志 (使用
Serilog
写日志),有时候感觉引入框架并非一件轻松的事情,各种知识点一股脑的涌来

我们开发复杂的系统,也是和建筑工人一样,一步步盖起房子来的,不同的是,有些人一块砖一块砖的盖,有些人采用预构件来构建,我们回到孩童的时候的思路,就是搭建积木的方式。

ABP VNext框架沿袭这种好习惯,把一些都简单化了,做起大项目来就更加方便了,类似搞一个乐高积木项目一样,不过我们约定了每个项目的基础分层部分,这样一来组装就标准化了。

如下面的一个项目,也可以当成它就是一个模块,和一个麻雀一样,五脏俱全,各个项目代表不同的功能,大家都这样做即可。

应用服务层:

Application.Contracts,包含应用服务接口和相关的数据传输对象(DTO)。
Application,包含应用服务实现,依赖于 Domain 包和 Application.Contracts 包。

领域层:
Domain.Shared,包含常量,枚举和其他类型.
Domain 包含实体, 仓储接口,领域服务接口及其实现和其他领域对象,依赖于 Domain.Shared 包.

基础设施层:

EntityFrameworkCore,包含EF的ORM处理,使用仓储模式,实现数据的存储功能。

HTTP 层
HttpApi项目, 为模块开发REST风格的HTTP API。
HttpApi.Client项目,它将应用服务接口实现远程端点的客户端调用,提供的动态代理HTTP C#客户端的功能。

各个层的依赖关系如下图所示。

我们把这些项目组成一个模块,即使这个模块只有一个表的处理功能,也是一个模块,它们构建成一个完整的模块内部生态层。

这样我们在以模块为基础单位,就可以单独开发,统一整合了,如下图所示。

这样,我们以相关的模块组合,以及一些辅助工具,就构成了整套框架的一个生态基础。

针对ABP VNext的前后端完全分离模式,我们给BS的前端,只需要提供API服务,以及接入详细说明即可,而给Winform、WPF、Console等基于.net的终端,则可以利用HTTP层的HttpApi.Client项目的动态客户端方式,避免编写API的客户端代理即可。

我详细参考了ABP VNext的基础框架类,以及一些应用模块项目的代码,它们基本上是提供了很多底层的支持,上层模块的支持,很多是在其商业版中的功能,并没有出现在应用模块中,如我们常见的权限系统的实现,它模块里面只是提供了简单的的角色和用户信息管理(而且很不完善),而我们往往需要扩展开来实现详细的用户、组织机构、角色、岗位、菜单、权限等功能的管理,才能算是一个完整的权限系统,另外还需要封装字典模块、附件管理模块等一些基础模块应用,这些就需要我们自己实现它的功能了。

以权限管理模块为例,它们虽然提供及基础的DTO和领域对象,没有提供完整应用层的实现,作为一个完整的应用系统,肯定不行,需要利用框架进一步实现才可以整合在项目中使用。

2、框架基础类继承关系

前面介绍了,本篇随笔作为系列的开篇,主要想介绍一下ABP VNext框架的一些基础类关系。

ABP VNext和ABP框架的基础类,虽然它们在项目管理上有所不同,不过它们的类关系层次继承关系,几乎没有太多的变化,有些一些层次上的调整而已。因此对于学习ABP或者ABP VNext框架来说,它们很多地方是共通的。

对于Application层来说,它是承接UI和领域层的中间层,因此它接收用户DTO对象,并且这些DTO对象为了和领域层的Entity层有映射关系,我们定义了一些基础类关系来协助它们,以方便DTO和Entity层之间的Mapping映射关系,从而通过约定方式承载系统的基础属性。

如ApplicationService层的相关DTO基类对象定义及继承关系如下图所示,其中右边是它们继承的接口,以及接口需要实现的属性信息。

注:上图ABP和ABP VNext框架,它们的基类定义和关系都是一样的。

而应用层有时候,需要对数据进行分页,并返回列表记录,那么下面的一些基类对象就是它的应用场景,通过定义分页信息和排序信息,可以让应用服务层获得相应的记录过滤,然后返回基于特定DTO对象的泛型列表,如下图所示。

注:上图ABP和ABP VNext框架,它们的基类定义和关系都是一样的。

ABP VNext框架的应用服务层类,提供了相关CRUD操作的基类,虽然我们有时候可以继承顶层ApplicationService进行开发,但是,为了方便,我们往往使用子类继承自CrudAppService,如下图所示。

首先定义相关自定义接口

public interfaceIBookAppService : 
ICrudAppService
< //定义CRUD方法 BookDto, //显示DTO Guid, //实体主键 PagedAndSortedResultRequestDto, //用于分页排序获取列表 CreateUpdateBookDto, //创建对象DTO CreateUpdateBookDto> //更新对象DTO {
}

然后实现该接口即可,如下所示。

public classBookAppService : 
CrudAppService
<Book, BookDto, Guid, PagedAndSortedResultRequestDto,CreateUpdateBookDto, CreateUpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid>repository)
:
base(repository)
{
}
}

CrudAppService实现了ICrudAppService接口中声明的所有方法. 然后,你可以添加自己的自定义方法或覆盖和自定义实现.

相对于ABP VNext的应用服务层基类,它们ABP框架的基类有所差异 ,它们分离了同步和异步的基类,不过基本上都使用异步基类居多,继承关系图如下所示。


它的服务层接口定义和接口实现的处理方式和ABP VNext的操作类似,就不再赘述了。

相对于前面介绍DTO层的基类定义,我们在框架的领域层也定义了类似的类和它的继承关系,和DTO一一对应,这样通过AutoMapping 的方式就可以自动处理他们的属性映射了,减少了很多繁琐的代码处理。

领域层的实体类关系和前面DTO关系类似,如下所示。

"聚合是域驱动设计中的一种模式.DDD的聚合是一组可以作为一个单元处理的域对象.例如,订单及订单系列的商品,这些是独立的对象,但将订单(连同订单系列的商品)视为一个聚合通常是很有用的。

如果是聚合根,如商品、订单和订单明细的关系场景,就可以应用到,ABP不强制你使用聚合根,实际上你可以使用上面定义的
Entity
类。

它们和领域的实体关系整合起来是一张关系图,如下所示。

这个在基类部分,和ABP框架有所差异,ABP VNext框架中的聚合根增加了扩展属性的接口定义和实现,以及领域事件的处理接口,如下所示。

它们的部分基类代码如下所示

namespaceVolo.Abp.Domain.Entities
{
[Serializable]
public abstract classAggregateRoot: BasicAggregateRoot,
IHasExtraProperties,
IHasConcurrencyStamp
{
public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }

[DisableAuditing]
public virtual string ConcurrencyStamp { get; set; }protectedAggregateRoot()
{
ConcurrencyStamp
= Guid.NewGuid().ToString("N");
ExtraProperties
= newExtraPropertyDictionary();this.SetDefaultsForExtraProperties();
}
public virtual IEnumerable<ValidationResult>Validate(ValidationContext validationContext)
{
returnExtensibleObjectValidator.GetValidationErrors(this,
validationContext
);
}
}

而ABP 框架的聚合根部分,除了基类有所差异,处理一些特殊的信息外,基本上也是类似的。

以上就是ABP VNext的一些基类和关系图,希望能够促进我们了解ABP VNext框架的神秘之处,解开它的面纱。

如果你对ABP框架的知识点有兴趣,可以参考《
ABP框架使用
》,如果对于ABP框架VUE&Element前端开发有兴趣,可以参考《
循序渐进VUE+Element
》部分内容。

ABP VNext框架如果不考虑在微服务上的应用,也就是开发单体应用解决方案,虽然也是模块化开发,但其集成使用的难度会降低一个层级,不过ABP VNext和ABP框架一样,基础内容都会设计很多内容,如数据库都支持Oracle、SQLServer、MySql、PostgreSQL、SQLite,都有利用Redis作为分布式缓存,使用RabbitMQ作为事件总线的消息处理方式,使用MongoDB的NoSQL类型数据库作为特殊数据的存储服务,使用Quartz/HangFire作为定时任务的处理等。如果考虑引入微服务的话,会更需要了解
IdentityServer
服务,以及了解
Ocelot
库管理网关,使用
Elasticsearch
&
Kibana
来存储和可视化日志 (使用
Serilog
写日志),有时候感觉引入框架并非一件轻松的事情,各种知识点一股脑的涌来。

"作为面向服务架构(SOA)的一个变体,微服务是一种将应用程序分解成松散耦合服务的新型架构风格. 通过细粒度的服务和轻量级的协议,微服务提供了更多的模块化,使应用程序更容易理解,开发,测试,并且更容易抵抗架构侵蚀. 它使小型团队能够开发,部署和扩展各自的服务,实现开发的并行化.它还允许通过连续重构形成单个服务的架构. 基于微服务架构可以实现持续交付和部署."

ABP VNext 框架引入微服务后,就需要使用API网关来,ABP框架可以使用Ocelot来做网关统一处理上游的HTTP请求,并在内部网络上使用内部网关,处理微服务之间的调用,从而把微服务的调用接口统一为一个固定的模式处理。本篇随笔介绍一下网关的基本智知识,以及ABP VNext 框在引入Ocelot来做网关后的架构图场景,介绍一下ABP VNext 微服务的案例的基本情况。

1、网关和认证服务的介绍

API网关是系统暴露在外部的一个访问入口。就像一个公司的门卫承担着寻址、限制进入、安全检查、位置引导、等等功能。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理等等。
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。

Ocelot是一个用.NET Core技术实现并且开源的API网关技术,它的功能包括了:路由、请求聚合、服务发现、认证、鉴权、限流熔断、并内置了负载均衡器、Service Fabric、Butterfly Tracing等的集成。而且这些功能都只需要简单的配置即可完成。

Ocelot首先通过配置将HttpRequest对象保存到一个指定的状态直到它到达用来创建HttpRequestMessage对象并将创建的HttpRequestMessage对象发送到下游服务中的请求构造中间件。通过中间件来发出请求是Ocelot管道中做的最后一件事。它不会再调用下一个中间件。下游服务的响应会存储在每个请求 scoped repository中,并作为一个请求返回到Ocelot管道中。有一个中间件将HttpResponseMessage映射到HttpResponse对象并返回给客户端。

单网关服务示意图如下所示。


API 网关一般放到微服务的最前端,并且要让API 网关变成由应用所发起的每个请求的入口。这样就可以明显的简化客户端实现和微服务应用程序之间的沟通方式。

上游和下游描述消息流:所有 消息从上游流动到下游。

网关作为上游会接收所有的客户端请求,并路由到对应的下游服务器进行处理,再将请求结果返回。而这个上下游请求的对应关系也被称之为路由。

我们的下游服务接口都是公开的,没有经过任何的认证,只要知道接口的调用方法,任何人都可以随意调用,因此,很容易就造成信息泄露或者服务被攻击。

正如,我要找Wlling干活之前,我得先到 HR 部门那里登记并且拿到属于我自己的工卡,然后我带着我的工卡去找Wlling,亮出我是公司员工的身份,并且有权利要求他帮我完成一个任务。

IdentityServer4认证服务器有多种认证模式,包括用户密码、客户端等等。客户端需要先想IdentityServer4 请求认证,获得一个token,然后再带着这个token向下游服务发出请求。

ApiResources 为数组类型,表示identityserver管理的所有的下游服务列表。

  • Name: 下游服务名称
  • DisplayName: 下游服务别名

Clients
为数组类型,表示identityserver管理的所有的上游客户端列表

  • ClientId: 客户端id
  • ClientSecret: 客户端对应的密钥
  • GrantType: 该客户端支持的认证模式
  • Scope: 该客户端支持访问的下游服务列表,必须是在
    apiresources
    列表中登记的

当接入ocelot网关时,我们要达到内外互隔的特性,于是就把identityserver服务也托管到ocelot网关中,这样我们就能统一认证和服务请求时的入口。

如ABP案例中的微服务网关【PublicWebSiteGateway.Host】项目中的配置内容,配置服务器上下游的信息如下所示。

  "Routes": [
{
"DownstreamPathTemplate": "/api/productManagement/{everything}","DownstreamScheme": "https","DownstreamHostAndPorts": [
{
"Host": "localhost","Port": 44344}
],
"UpstreamPathTemplate": "/api/productManagement/{everything}","UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post"]
},
{
"DownstreamPathTemplate": "/api/blogging/{everything}","DownstreamScheme": "https","DownstreamHostAndPorts": [
{
"Host": "localhost","Port": 44357}
],
"UpstreamPathTemplate": "/api/blogging/{everything}","UpstreamHttpMethod": [ "Put", "Delete", "Get", "Post"]
}
],
"GlobalConfiguration": {"BaseUrl": "https://localhost:44397"},

多网关服务示意图如下所示,这种模式是针对不同的客户端来实现一个不同的API网关。

ABP VNext 框架里面也采用了多网关的应用,其微服务的整体架构图如下所示。

其中网关包含了后台管理应用网关【BackendAdminAppGateway.Host】,以及公开的应用接入网关【PublicWebSiteGateway.Host】,而内部网关服务【InternalGateway.Host】,则是用于内部微服务之间调用的统一网关解析。

ABP VNext框架中的微服务,有各个模块的微服务组成一个集合,一起为各个应用提供不同的数据处理服务。

2、ABP VNext项目的微服务项目

前面说到,ABP VNext 框架里面也采用了多网关的应用,其中网关包含了后台管理应用网关【BackendAdminAppGateway.Host】,以及公开的应用接入网关【PublicWebSiteGateway.Host】,而内部网关服务【InternalGateway.Host】,则是用于内部微服务之间调用的统一网关解析。

ABP VNext的微服务项目如下所示。

生成的数据库包含两个部分,其中基础数据库包含IdentityServer4所需的基础表,以及用户、角色、租户、日志、组织机构、权限等权限模块的基础表;另外一个部分就是业务模块的数据库了,如下所示。

我们通过AuthServer.Host和ProductService.Host项目,初始化相关的数据库。

最后获得两个初始数据库,包含基础的表信息。

之前随笔也提到过,虽然ABP VNext的官方提供了构建权限系统的相关表信息,但是组织机构、用户、角色业务表和中间表的管理没有在其对应的Identity项目中提供,官方提供的Identity项目如下所示。

这部分完善的应用接口及管理,他们是在ABP VNext商业版中进行开发并提供的,因此我们开发具体的应用所需的权限基础内容,需要自己进行项目模块的扩展,然后完善组织机构、角色、用户、菜单、日志(审计日志、对象修改日志)、权限点的管理和维护等内容。

3、微软的eShopOnContainer微服务架构

eShopOnContainer是基于Docker技术微服务架构demo,由微软架构师利用.net core技术实现并在github上开源,同时发布的还有关于微服务架构的白皮书(
点这里
),微服务架构是一个比较新的架构模式,通读白皮书并结合该demo代码,可以做到按图索骥的作用,对理解.net core技术实现微服务架构可以做到事半功倍。

在Github中的微软eShopOnContainer 项目地址:
https://github.com/dotnet-architecture/eShopOnContainers

eShopOnContainer 的开发架构示意图如下所示。

包含网关的架构架构图如下图所示,其中包含多个网关服务处理客户端的请求。

4、微服务的模块拆分

微服务根据功能或者应用场景进行拆分,如把一个大型复杂的系统应用,拆分为多个微服务应用模块,然后进行整合使用。

或者按下面界限上下文进行划分

不过微服务也不是拆分的越细越好,一般根据实际情况进行度量,引入微服务虽然能够解决一些技术上和性能上的问题,不过拆分过多可能会导致开发和维护上灾难。