2023年2月

在前面随笔介绍的《
ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理
》里面,介绍了如何改进和完善审计日志和登录日志的应用服务端和Winform客户端,由于篇幅限制,没有进一步详细介绍Winform界面的开发过程,本篇随笔介绍这部分内容,并进一步扩展Winform界面的各种情况处理,力求让它进入一个新的开发里程碑。

1、回顾审计日志和登陆日志管理界面

前面介绍了如何扩展审计日志应用服务层(Application Service层)和ApiCaller层(API客户端调用封装层),同时也展示审计日志和登录日志在Winform界面的展示,由于整个ABP框架目前我还是采用了.net core的开发路线,所有的封装项目都是基于.net core基础上进行的。不过由于目前Winform还没有能够以 .net core进行开发,所以界面端还是用.net framework的方式开发,不过可以调用 .net standard的类库。

下面是审计日志的列表展示界面,和我之前的Winform框架一样的布局,因此我重用了Winform框架里面公用类库项目、基础界面封装项目、分页控件等内容,因此整个界面看起来还是很一致的。

由于审计日志主要供底层记录,因此在界面不能增加增删改的操作,我们只需要分页查询,和导出记录即可,如下窗体界面所示。

而明细内容,可以通过双击或者右键选择菜单打开即可弹出新的展示界面,主要展示审计日志里面的各项信息。

而对于用户登录日志来说,处理方式差不多,也是通过在列表中查询展示,并在列表中整合右键菜单或者双击处理,可以查看登录明细内容。

通过双击或者右键选择菜单打开即可弹出新的展示界面,主要展示登录日志里面的各项信息。

2、Winform界面代码实现

上面展示了列表界面和查看明细界面,实际上我们Winform的界面内部是如何处理的呢,我们这里对其中的一些关键处理进行分析介绍。

列表界面的窗体初始化代码如下所示

    /// <summary>
    ///审计日志/// </summary>    
    public partial classFrmAuditLog : BaseDock
{
private const string Id_FieldName = "Id";//Id的字段名称 publicFrmAuditLog()
{
InitializeComponent();
//分页控件初始化事件 this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport);this.winGridViewPager1.OnEditSelected += newEventHandler(winGridViewPager1_OnEditSelected);this.winGridViewPager1.OnAddNew += newEventHandler(winGridViewPager1_OnAddNew);this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged +=newEventHandler(gridView1_DataSourceChanged);this.winGridViewPager1.gridView1.CustomColumnDisplayText += newDevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);this.winGridViewPager1.gridView1.RowCellStyle += newDevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);//关联回车键进行查询 foreach (Control control in this.layoutControl1.Controls)
{
control.KeyUp
+= new System.Windows.Forms.KeyEventHandler(this.SearchControl_KeyUp);
}
//屏蔽某些处理 this.winGridViewPager1.ShowAddMenu = false;this.winGridViewPager1.ShowDeleteMenu = false;
}

这些是使用分页控件来初始化一些界面的处理事件,不要一看就抱怨需要编写这么多代码,这些基本上都是代码生成工具生成的,后面会介绍。

其实窗体的加载的时候,主要逻辑是初始化字典列表和展示列表数据,如下代码所示。

        /// <summary>
        ///编写初始化窗体的实现,可以用于刷新/// </summary>
        public override async voidFormOnLoad()
{
awaitInitDictItem();awaitBindData();
}

其中这里都是使用async和await 配对实现的异步处理操作。我们对于审计日志列表来说,字典模块没有需要字典绑定信息,那么默认为空不用修改。

        /// <summary>
        ///初始化字典列表内容/// </summary>
        private asyncTask InitDictItem()
{
//初始化代码//await this.txtCategory.BindDictItems("报销类型"); await Task.FromResult(0);
}

那么我们主要处理的就是BindData的数据绑定操作了。

        /// <summary>
        ///绑定列表数据/// </summary>
        private asyncTask BindData()
{
this.winGridViewPager1.DisplayColumns = "Id,BrowserInfo,ClientIpAddress,ClientName,CreationTime,Result,UserId,UserNameOrEmailAddress";this.winGridViewPager1.ColumnNameAlias = await UserLoginAttemptApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义//获取分页数据列表 var result = awaitGetData();//设置所有记录数和列表数据源 this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息 this.winGridViewPager1.DataSource =result.Items;this.winGridViewPager1.PrintTitle = "用户登录日志报表";
}

其中我们通过 调用服务端接口 GetColumnNameAlias 来获取对应的别名,其实我们也可以在Winform客户端设置对等的别名处理,如下代码所示。

            #region 添加别名解析

            //this.winGridViewPager1.AddColumnAlias("Id", "Id");//this.winGridViewPager1.AddColumnAlias("BrowserInfo", "浏览器");//this.winGridViewPager1.AddColumnAlias("ClientIpAddress", "IP地址");//this.winGridViewPager1.AddColumnAlias("ClientName", "客户端");//this.winGridViewPager1.AddColumnAlias("CreationTime", "时间");//this.winGridViewPager1.AddColumnAlias("Result", "结果");//this.winGridViewPager1.AddColumnAlias("UserId", "用户ID");//this.winGridViewPager1.AddColumnAlias("UserNameOrEmailAddress", "用户名或邮件");

            #endregion

只是基于服务端更加方便,也减少客户端的编码了。

而获取数据主要通过 GetData 函数进行统一获取对应的列表和数据记录信息,如下是GetData的函数实现。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<UserLoginAttemptDto>>GetData()
{
//构建分页的条件和查询条件 var pagerDto = new UserLoginAttemptPagedDto(this.winGridViewPager1.PagerInfo)
{
UserNameOrEmailAddress = this.txtUserNameOrEmailAddress.Text.Trim(),
};
//日期和数值范围定义//时间,需在UserLoginAttemptPagedDto中添加DateTime?类型字段CreationTimeStart和CreationTimeEnd var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型 pagerDto.CreationTimeStart =CreationTime.Start;
pagerDto.CreationTimeEnd
=CreationTime.End;var result = awaitUserLoginAttemptApiCaller.Instance.GetAll(pagerDto);returnresult;
}

这个函数里面,主要是接收列表界面里面的查询条件,并构建对应的分页查询条件,这样根据条件DTO就可以请求服务器的数据了。

前面讲了,这个过滤条件并返回对应的数据,主要就是在Application Service层,设置CreateFilteredQuery的控制逻辑即可,如下所示。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input">分页查询Dto对象</param>
        /// <returns></returns>
        protected override IQueryable<AuditLog>CreateFilteredQuery(AuditLogPagedDto input)
{
//构建关联查询Query var query = from auditLog inRepository.GetAll()
join user
in_userRepository.GetAll() on auditLog.UserId equals user.Id into userJoinfrom joinedUser inuserJoin.DefaultIfEmpty()whereauditLog.UserId.HasValueselect new AuditLogAndUser { AuditLog = auditLog, User =joinedUser };//过滤分页条件 returnquery
.WhereIf(
!string.IsNullOrEmpty(input.UserName), t =>t.User.UserName.Contains(input.UserName))
.WhereIf(input.ExecutionTimeStart.HasValue, s
=> s.AuditLog.ExecutionTime >=input.ExecutionTimeStart.Value)
.WhereIf(input.ExecutionTimeEnd.HasValue, s
=> s.AuditLog.ExecutionTime <=input.ExecutionTimeEnd.Value)
.Select(s
=>s.AuditLog);
}

这里就不在赘述服务层的逻辑代码,主要关注我们本篇的主题,Winform的界面实现逻辑。

上面通过GetData获取到服务端数据后,我们就可以把列表数据绑定到分页控件上面,让分页控件调用GridControl 进行展示出来即可。

            //设置所有记录数和列表数据源
            this.winGridViewPager1.PagerInfo.RecordCount =result.TotalCount;this.winGridViewPager1.DataSource = result.Items;

数据的导出操作,我们这里也顺便提一下,虽然这些代码是基于代码生成工具生成的,不过还是提一下逻辑处理。

数据的导出操作,主要就是通过GetData获取到数据后,转换为DataTable,并通过Apose.Cell进行写入Excel文件即可,如下代码所示。

        /// <summary>
        ///导出的操作/// </summary>        
        private async voidExportData()
{
string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName));if (!string.IsNullOrEmpty(file))
{
//获取分页数据列表 var result = awaitGetData();var list =result.Items;
DataTable dtNew
= DataTableHelper.CreateTable("序号|int,Id,时间,用户名,服务,操作,参数,持续时间,IP地址,客户端,浏览器,自定义数据,异常,返回值");
DataRow dr;
int j = 1;for (int i = 0; i < list.Count; i++)
{
dr
=dtNew.NewRow();
dr[
"序号"] = j++;
dr[
"Id"] =list[i].Id;
dr[
"浏览器"] =list[i].BrowserInfo;
dr[
"IP地址"] =list[i].ClientIpAddress;
dr[
"客户端"] =list[i].ClientName;
dr[
"自定义数据"] =list[i].CustomData;
dr[
"异常"] =list[i].Exception;
dr[
"持续时间"] =list[i].ExecutionDuration;
dr[
"时间"] =list[i].ExecutionTime;
dr[
"操作"] =list[i].MethodName;
dr[
"参数"] =list[i].Parameters;
dr[
"服务"] =list[i].ServiceName;
dr[
"用户名"] =list[i].UserName;
dr[
"返回值"] =list[i].ReturnValue;
dtNew.Rows.Add(dr);
}
try{string error = "";
AsposeExcelTools.DataTableToExcel2(dtNew, file,
outerror);if (!string.IsNullOrEmpty(error))
{
MessageDxUtil.ShowError(
string.Format("导出Excel出现错误:{0}", error));
}
else{if (MessageDxUtil.ShowYesNoAndTips("导出成功,是否打开文件?") ==System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}

而对于编辑或者查看界面,如下所示。

它的实现逻辑主要就是获取单个记录,然后在界面上逐一绑定控件内容显示即可。

        /// <summary>
        ///数据显示的函数/// </summary>
        public async override voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) if (!string.IsNullOrEmpty(ID))
{
#region 显示信息 var info = awaitAuditLogApiCaller.Instance.Get(ID.ToInt64());if (info != null)
{
tempInfo
= info;//重新给临时对象赋值,使之指向存在的记录对象 txtBrowserInfo.Text=info.BrowserInfo;
txtClientIpAddress.Text
=info.ClientIpAddress;
txtClientName.Text
=info.ClientName;
txtCustomData.Text
=info.CustomData;
txtException.Text
=info.Exception;
txtExecutionDuration.Value
=info.ExecutionDuration;
txtExecutionTime.SetDateTime(info.ExecutionTime);
txtMethodName.Text
=info.MethodName;
txtParameters.Text
=ConvertJson(info.Parameters);
txtServiceName.Text
=info.ServiceName;if(info.UserId.HasValue)
{
txtUserId.Value
=info.UserId.Value;
}
txtUserName.Text
= info.UserName;//转义的用户名 }#endregion}else{
}
this.btnAdd.Visible = false;this.btnOK.Visible = false;
}

当然对于新增或编辑的界面,我们需要处理它的保存或者更新的操作事件,虽然审计日志不需要这些操作,不过生成的编辑窗体界面,依旧保留这些处理逻辑,如下代码所示。

        /// <summary>
        ///新增状态下的数据保存/// </summary>
        /// <returns></returns>
        public async override Task<bool>SaveAddNew()
{
AuditLogDto info
= tempInfo;//必须使用存在的局部变量,因为部分信息可能被附件使用 SetInfo(info);try{#region 新增数据tempInfo= awaitAuditLogApiCaller.Instance.Create(info);if (tempInfo != null)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
return false;
}
/// <summary> ///编辑状态下的数据保存/// </summary> /// <returns></returns> public async override Task<bool>SaveUpdated()
{
AuditLogDto info
= awaitAuditLogApiCaller.Instance.Get(ID.ToInt64());if (info != null)
{
SetInfo(info);
try{#region 更新数据tempInfo= awaitAuditLogApiCaller.Instance.Update(info);if (tempInfo != null)
{
//可添加其他关联操作 return true;
}
#endregion}catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return false;
}

我们可以根据实际的需要,对我们业务对象的窗体进行一定的改造即可。

3、复杂一点的WInform界面处理

例如对于前面的列表界面,一个比较复杂一点的列表展示内容,需要在查询条件中绑定字典列表,并对列表记录的一些状态进行特殊展示等,以及需要考虑增加、导入、导出等功能按钮,这些默认的列表生成界面就有的。

如下是对于产品信息的一个界面展示,也是基于ABP框架构建的服务进行数据展示的例子。

和前面介绍的例子一样,也是基于分页控件进行展示的,我们来看看状态的处理吧。

由于状态和用户信息,我们在数据库里面记录的是整形的数据信息,也就是状态为0,1的这样,以及用户ID等,我们如果需要转义给客户端使用,那么我们需要在对应的DTO里面增加一些字段进行承载,如下所示是产品信息的DTO对象,除了本身CreateProductDto必须有的字段外,我们另外增加了两个属性,如下代码所示。

然后我们在应用服务接口的ConvertDto转义函数里面增加自己的处理转义逻辑即可,如下代码所示。

        /// <summary>
        ///对记录进行转义/// </summary>
        /// <param name="item">dto数据对象</param>
        /// <returns></returns>
        protected override voidConvertDto(ProductDto item)
{
//如需要转义,则进行重写 #region 参考代码 //用户名称转义 if(item.CreatorUserId.HasValue)
{
//需在ProductDto中增加CreatorUserName属性 item.CreatorUserName=_userRepository.Get(item.CreatorUserId.Value).UserName;
}
if(item.Status.HasValue)
{
item.StatusDisplay
= item.Status.Value == 0 ? "正常" : "停用";
}
#endregion}

这样客户端就可以采用这两个属性展示信息了。

前面也介绍了,对于产品类型属性,我们一般是一个字典信息的,因此我们可以集成绑定字典的处理,如下代码所示。

这个BindDictItems是扩展函数,通过扩展函数,我们对控件类型的绑定字典操作进行处理即可,具体的逻辑代码如下所示。

    /// <summary>
    ///扩展函数封装/// </summary>
    internal static classExtensionMethod
{
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="control">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="emptyFlag">是否添加空行</param> public static async Task BindDictItems(this ComboBoxEdit control, string dictTypeName, bool isCache = true, bool emptyFlag = true)
{
await BindDictItems(control, dictTypeName, null, isCache, emptyFlag);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表/// </summary> /// <param name="control">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="defaultValue">控件默认值</param> /// <param name="emptyFlag">是否添加空行</param> public static async Task BindDictItems(this ComboBoxEdit control, string dictTypeName, string defaultValue, bool isCache = true, bool emptyFlag = true)
{
var dict = awaitDictItemUtil.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);
}

......

最后我们可以看到,字典列表的效果如下所示。

新增产品信息界面如下所示。

4、基于代码工具的Winform界面快速生成

这些都是标准的Winform界面模板,因此可以利用代码生成工具进行快速开发,利用代码生成工具Database2Sharp快速生成来实现ABP优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

ABP框架的基础代码生成我们就不再这里介绍了,主要介绍下Winform展示界面和编辑界面的快速生成即可。

在生成Abp框架的Winform界面面板中,配置我们查询条件、列表展示、编辑展示内容等信息后,就可以生成对应的界面,然后复制到项目中使用即可,整个过程是比较快速的,这些开发便利可是花了我很多反复核对和优化NVelocity模板的开发时间的。

如下是代码生成工具Database2Sharp关于ABP框架的Winform界面配置。

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

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

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

在前面两篇随笔《
ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理
》和《
ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程
》开始介绍了权限管理的内容,其中只是列出了内部的权限系统的审计和登陆信息,以及对Winform界面的整合,本篇随笔继续介绍ABP开发框架的权限控制管理内容,包括用户、角色、机构、权限等方面,以及该框架在Winform方面的应用集成。

1、ABP框架的权限控制管理内容

我们知道,权限管理一般都会涉及到用户、组织机构、角色,以及权限功能等方面的内容,ABP框架的基础内容也是涉及到这几方面的内容,其中它们之间的关系基本上是多对多的关系,它们的关系如下所示。

不过在官网下载的框架里面,包含权限管理这些应用服务层和展示层的内容并不完整,只是简单的包括了用户和角色的基础管理,而且很多权限管理所需要的基础功能并没有提供。

根据ABP框架提供的基础数据库表,我们可以进一步整理权限管理几个重要概念和真实数据库表之间的对应关系,基于这个基础上,我们可以完善整个权限管理模块内容。

上图是ABP基础框架中权限模块里面包含的一些主对象表和中间表,中间表主要用来存储两个对象之间的多对多关系,如角色包含多个用户,用户属于多个机构,机构包含多个角色等等。

2、基于ABP框架的权限管理模块

1)组织机构管理

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

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

添加角色界面如下所示。

2)角色管理

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

其中角色包含权限分配和角色成员的维护,如下是角色编辑界面,包含角色基本信息、权限、成员管理等。

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

用户成员则和机构的用户管理一样,可以指定多个用户。

3)用户管理

用户管理只需要管理用户基本的信息即可,我们如果需要分配角色可以在角色管理里面统一处理。当然,创建用户的时候,也可以ABP框架的收费版本界面一样,为用户指定角色和机构信息。

我这里主要是维护用户信息即可,用户列表界面如下所示。

用户编辑或者查看界面,除了可以看用户基础信息外,可以查看用户包所属的机构(多个),或者所属的角色(多个)

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

4)权限功能

严格来说,ABP框架并没有统一管理好权限功能点的,它没有任何表来存储这个功能集合,而是通过派生AuthorizationProvider的子类来定义权限功能点,这种需要通过指定AuthorizationProvider的子类的方式创建功能点,需要每次系统模块增加功能点的时候,编码一下,然后增加自己的功能点,如下界面所示。

这种方式可能能够满足大多数的需要,不过我如果需要增量开发,或者动态增加某些功能点的时候,就有点不方便了。

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

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

这些概念主要还是来源于我的Winform开发框架和混合式开发框架里面的控制思路,以及界面展示的处理。

这样我们就可以管理自己的权限功能点,并可以为指定的角色配置相关的控制功能点,如下表所示是角色的权限集合(系统中间表),也就是给角色分配的功能点,依旧是在原来的系统表里面存储。

3、权限控制在业务模块界面中的使用

我们拥有了用户、角色、机构、权限功能以及它们之间的关系后,我们可以按照一个完善的权限系统来创建对应的用户角色权限关系,并通过在客户端对界面权限的判断和服务端对操作权限的判断,实现完整的控制处理。

服务端由ABP框架内置权限进行管理,通过在AppService里面定义好增删改查等权限点,如引用服务层的基类设置了几个权限点的属性。

我们在子类里面指定这些操作的变量即可,如产品应用服务中,我们可以定义CreatePermissionName为 Product/Add 这样的名称,当然也可以自定义。

然后每次在Action中调用相应的检查即可,如下是对创建的判断检查。

或者更新操作的权限检查

如果对于导入、导出等其他权限,我们则可以通过调用

void CheckPermission(string permissionName);

来进行自己自定义权限名称的判断。

在客户端,我们登录成功后,获取用户的权限集合,然后在客户端进行判断即可进行权限的控制管理,可以控制菜单、按钮等界面元素,如下是整合了权限控制的产品信息管理界面。

分页列表展示界面的控制代码如下所示。

编辑或者查看界面的控制代码如下所示

这样我们 就可以整合了权限到业务管理模块里面,实现对菜单、按钮等元素的权限控制了。

首先在权限管理系统模块里面为用户角色添加对应的产品管理权限点。

产品信息界面展示如下所示。

如果在权限模块的角色里面取消对应的功能点,那么产品管理功能不可用。

在较早期的随笔《
ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用
》已经介绍了Web API调用类的封装处理,虽然这些调用类我们可以使用代码生成工具快速生成,不过自定义接口,还是需要我们对这些接口进行实现,以便发起对Web API的调用,并获得相应的数据返回。本篇随笔介绍使用API调用类的封装类,进行函数的抽象,根据方法名称的推断,构建URL或者WebClient的请求类型,从而实现所有API调用函数的简化处理。

1、ABP框架服务端和客户端的处理

ABP框架的架构图示,如下图所示(以字典模块为例说明)

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

而ABP的Web API调用类则需要对Web API接口调用进行封装,如下所示。

如对于字典模块的API封装类,它们继承一个相同的基类,然后实现特殊的自定义接口即可,这样可以减少常规的Create、Get、GetAll、Update、Delete等操作的代码,这些全部由调用基类进行处理,而只需要实现自定义的接口调用即可。

2、Web API调用类的简化处理

我们对于常规的Web API调用接口处理,如下代码所示。

        public async virtual Task<AuthenticateResult> Authenticate(string username, stringpassword)
{
var url = string.Format("{0}/api/TokenAuth/Authenticate", ServerRootAddress);var input = new{
UsernameOrEmailAddress
=username,
Password
=password
};
var result = await apiClient.PostAsync<AuthenticateResult>(url, input);returnresult;
}

这种方法的处理,就需要自己拼接URL地址,以及传递相关的参数,一般情况下,我们的Web API Caller层类的函数和Web API控制器的方法是一一对应的,因此方法名称可以通过对当前接口名称的推断进行获得,如下所示。

        public async Task<bool>ChangePassword(ChangePasswordDto input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) return await apiClient.PostAsync<bool>(url, input);
}

函数AddRequestHeaders 通过在调用前增加对应的AccessToken信息,然后URL通过当前方法的推断即可构建一个完整的URL,但是这个也仅仅是针对POST的方法,因为ABP框架根据方法的名称前缀的不同,而采用POST、GET、Delete、PUT等不同的HTTP处理操作。

如GET方法,则是需要使用GET请求

        public async Task<List<RoleDto>> GetRolesByUser(EntityDto<long>input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url =GetUrlParam(input, url);var result = await apiClient.GetAsync<List<RoleDto>>(url);returnresult;
}

而对于删除方法,则使用下面的DELETE请求,DELETE 和PUT操作,需要把参数串联成GET的URL形式,类似 url += string.Format("?Id={0}", id); 这样方式

        public virtual asyncTask Delete(TDeleteInput input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) url +=GetUrlParam(input, url);var result = awaitapiClient.DeleteAsync(url);returnresult;
}

对于更新的操作,使用了PUT方法

        public async virtual Task<TEntityDto>Update(TUpdateInput input)
{
AddRequestHeaders();
//加入认证的token头信息 string url = GetActionUrl(MethodBase.GetCurrentMethod());//获取访问API的地址(未包含参数) var result = await apiClient.PutAsync<TEntityDto>(url, input, null);returnresult;
}

上面这些方法,我们根据规律,其实可以进一步进行简化,因为这些操作大多数类似的。

首先我们看到变化的地方,就是根据方法的前缀采用GET、POST、DELETE、PUT方法,还有就是URL串联字符串的不同,对于GET、Delete方法,参数使用的是组成URL方式,参数使用的是JSON提交内容方式。

根据这些变化,我们在基类提炼一个统一的处理方法DoActionAsync 来处理这些不同的操作。

        /// <summary>
        ///根据方法名称自动执行GET/POST/PUT/DELETE请求方法/// </summary>
        /// <param name="method"></param>
        /// <param name="input"></param>
        protected virtual async Task DoActionAsync(MethodBase method, object input = null)
{
await DoActionAsync<object>(method, input);
}
        /// <summary>
        ///根据方法名称自动执行GET/POST/PUT/DELETE请求方法/// </summary>
        /// <param name="method"></param>
        /// <param name="input"></param>
        protected virtual async Task<TResult> DoActionAsync<TResult>(MethodBase method, object input = null)
{
AddRequestHeaders();
//加入认证的token头信息 string action =GetMethodName(method);var url = string.Format("{0}/api/services/app/{1}/{2}", ServerRootAddress, DomainName, action);//获取访问API的地址(未包含参数) var httpVerb =DynamicApiVerbHelper.GetConventionalVerbForMethodName(action);if(httpVerb == HttpVerb.Get || httpVerb ==HttpVerb.Delete)
{
if (input != null)
{
//Get和Delete的操作,需要组装URL参数 url =GetUrlParam(input, url);
}
}
int? timeout = null;return await apiClient.DoActionAsync<TResult>(url, timeout, httpVerb.ToString().ToLower(), input);
}

这样,有了这两个函数的支持,我们可以简化很多操作代码了。

例如对于Update方法,简化的代码如下所示。

        public async virtual Task<TEntityDto>Update(TUpdateInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}

对于删除操作,简化的代码依旧也是一行代码

        public virtual asyncTask Delete(TDeleteInput input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}

GET操作,也是一行代码

        public async virtual Task<TEntityDto>Get(TGetInput input)
{
return await DoActionAsync<TEntityDto>(MethodBase.GetCurrentMethod(), input);
}

现在你看到,所有的客户端API封装类调用,都已经非常简化,大同小异了,主要就是交给基类函数进行推断调用处理即可。

如用户操作的APICaller类的代码如下所示。

这样我们再多的接口,都一行代码调用解决问题,非常简单,从此客户端封装类的实现就非常简单了,只需要注意有没有返回值即可,其他的都没有什么不同。

只需要注意的是,我们定义接口的时候,尽可能使用复杂类型对象,这样就可以根据对象属性名称和值进行构建URL或者JSON的了。

在前面随笔《
ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理
》中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现了对组织机构、角色、用户、权限等管理,其中没有涉及菜单部分,本篇随笔介绍在ABP框架中实现菜单的管理,菜单是作为Winform或者Web动态构建界面的一个重要元素,同时也是作为角色权限控制的部分资源。

1、菜单的列表展示和管理

一般情况下,菜单的树形列表的显示可以分为多个节点,节点可以收缩也可以展开,当然节点是有不同的图标的了。这样就可以把很多功能点整合在一个树列表里面了,树的节点也可以分为很多级别,很多层次


如果我们想按照业务的范畴来区分,也可以分为多个模块展示,类似选项卡的方式,一个模块的功能菜单列表集合在一起展示,如下所示。

上面是我Winform开发框架和混合式开发框架的Winform界面中呈现菜单的界面,对于ABP开发框架来说,我们也只是获取数据方式不同,业务范畴的管理也没有什么不一样,我们依旧可以在服务器端配置好系统的菜单记录,然后基于ABP的Winform界面,同样管理这些内容即可。

下面是ABP框架中对于菜单资源管理的列表界面。

左边我们通过TreeList列表进行展示,右侧通过分页控件列表的方式进行展示,还是比较标准的Winform界面展示。

编辑或者创建菜单的界面如下所示。

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

2、菜单模块的实现逻辑

为了开发菜单模块,我们需要先定义好菜单的存储数据表,定义菜单表和角色菜单的中间关系表如下所示。

这个菜单模块定位为Web和Winform都通用的,因此菜单表中增加多了一些字段信息。

在数据库里增加这两个表后,就可以使用代码生成工具进行框架代码的生成和Winform界面代码的生成了。

生成框架后,对应的应用服务层类代码如下所示。

这个生成的类,默认具有基类的增删改查分页等接口方法,同时我们也会生成对应的Web API Caller层的类代码,代码如下所示。

Winform界面生成标准界面后进行布局的一定调整,左侧增加TreeList控件,设计界面如下所示。

获取列表数据的函数定义在GetData函数里面,函数代码如下所示。

        /// <summary>
        ///获取数据/// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<MenuDto>>GetData()
{
MenuPagedDto pagerDto
= null;if (advanceCondition != null)
{
pagerDto
= new MenuPagedDto(this.winGridViewPager1.PagerInfo);
pagerDto
=dlg.GetPagedResult(pagerDto);
}
else if(!IsNormalSearch && this.tree.FocusedNode != null)
{
//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
PID
= string.Concat(this.tree.FocusedNode.GetValue(Id_FieldName))
};
}
else{//构建分页的条件和查询条件 pagerDto = new MenuPagedDto(this.winGridViewPager1.PagerInfo)
{
//添加所需条件 Name = this.txtName.Text.Trim(),
WinformType
= this.txtWinformType.Text.Trim()
};
}
var result = awaitMenuApiCaller.Instance.GetAll(pagerDto);returnresult;
}

分页控件的数据绑定代码如下所示,这些都是根据Winform界面配置自动生成的代码。

            this.winGridViewPager1.DisplayColumns = "EmbedIcon,Name,Seq,Visible,Expand,WinformType,Tag,CreationTime";this.winGridViewPager1.ColumnNameAlias = await MenuApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义//获取分页数据列表
            var result = awaitGetData();//设置所有记录数和列表数据源
            this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的赋值,更新分页信息
            this.winGridViewPager1.DataSource = result.Items;

而TreeList列表是我们后来增加上去的,需要额外进行数据的绑定和处理,初始化树列表处理代码如下所示。

        /// <summary>
        ///初始化树控件/// </summary>
        private voidInitTree()
{
this.tree.Columns.Clear();//控件扩展函数封装处理 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;
};
//初始化树节点选择事件 this.tree.FocusedNodeChanged += delegate (objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}

获取列表数据并绑定树列表的数据源如下所示

        /// <summary>
        ///绑定树的数据源/// </summary>
        private asyncTask BindTree()
{
var pageDto = new MenuPagedDto();var result = awaitMenuApiCaller.Instance.GetAll(pageDto);this.tree.DataSource =result.Items;this.tree.ExpandAll();
}

而界面显示的时候,加载并显示左侧树列表数据如下代码所示。

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

删除菜单的时候,我们一般想把当前菜单和下面的子菜单一并级联删除,实现这个方法,我们需要在服务端自定义实现,如下是应用服务层的实现方法。

        /// <summary>
        ///移除节点和子节点/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
[UnitOfWork]public virtual async Task DeleteWithSubNode(EntityDto<string>input)
{
var children = await _repository.GetAllListAsync(ou => ou.PID ==input.Id);foreach (var child inchildren)
{
await DeleteWithSubNode(new EntityDto<string>(child.Id));//递归删除 }await_repository.DeleteAsync(input.Id);
}

我们这里显示声明了UnitOfWork标记,说明这个操作的原子性,全部成功就成功,否则失败的处理。

而客户端的Web API 封装调用类,对这个Web API接口的封装,根据上篇随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》简化后的处理代码如下所示。

3、角色菜单管理

菜单的管理整体操作和常规的业务表处理一样,没有太多特殊的地方,下面介绍一下角色包含菜单的管理操作。

前面介绍了角色包含菜单的管理界面如下所示。

界面主要是列出所有菜单,并勾选上该角色可以使用的菜单。这个角色包含的菜单和角色包含的权限处理上比较相似。

首先我们需要定义一个角色DTO对象中的菜单集合属性,如下所示。

在界面上获取勾选上的权限和菜单ID集合,存储在对应的列表里面。

        /// <summary>
        ///编辑或者保存状态下取值函数/// </summary>
        /// <param name="info"></param>
        private voidSetInfo(RoleDto info)
{
info.DisplayName
=txtDisplayName.Text;
info.Name
=txtName.Text;
info.Description
=txtDescription.Text;

info.Permissions
= GetNodeValues(this.tree, "Name");
info.Menus
= GetNodeValues(this.treeMenu, "Id");
}

在应用服务层的RoleAppService类里面,我们创建或者更新角色的时候,需要更新它的权限和菜单资源,如下代码是创建角色的函数。

        /// <summary>
        ///创建角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Create(CreateRoleDto input)
{
CheckCreatePermission();
var role = ObjectMapper.Map<Role>(input);
role.SetNormalizedName();

CheckErrors(
await_roleManager.CreateAsync(role));await CurrentUnitOfWork.SaveChangesAsync(); //It's done to get Id of the role. awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

同理,更新角色一样处理这两个部分的资源

        /// <summary>
        ///更新角色对象/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        public override async Task<RoleDto>Update(RoleDto input)
{
CheckUpdatePermission();
var role = await_roleManager.GetRoleByIdAsync(input.Id);

ObjectMapper.Map(input, role);

CheckErrors(
await_roleManager.UpdateAsync(role));awaitUpdateGrantedPermissions(role, input.Permissions);awaitUpdateGrantedMenus(role, input.Menus);returnMapToEntityDto(role);
}

以上就是菜单的管理,和角色包含菜单的维护操作,整个开发过程主要就是使用代码生成工具来快速生成框架各个层的代码,以及Winform界面的代码,这样在进行一定的函数扩展以及界面布局调整,就可以非常方便、高效的完整一个业务模块的开发工作了。

一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查询来使用,本篇随笔引入了另外一种配置信息的定义,实现更加简化的处理,本篇随笔着重介绍两者之间的差异和不同的地方。

1、ABP框架的配置管理

如下面是邮件配置信息,配置信息一般先继承自SettingProvider,初始化定义后,才能被系统所使用。

EmailSettingProvider
:继承自SettingProvider, 将
SMTP的各项设置封装成
SettingDefinition
,并以数组形式返回

配置的管理类,主要通过接口ISettingManager来进行统一管理的,底层协同了SettingStore配置存储和SetttingDefinitionMananger的配置定义管理两个部分。

这种方式的配置信息,糅合了配置项的定义(强制性),以及多语言特性的处理,根据不同的语言返回不同的配置名称,同时也整合了缓存信息的处理,以减少系统的一些消耗。

不过从上面的图示我们也可以看到,整个配置模块由于引入这些内容,导致处理起来必须按部就班的创建配置管理类,定义配置信息,重新编译系统后,然后才能进行信息的调用,因此这些配置信息必须预定义。而且管理起来协同这些类的处理,也略显得有点复杂化。

在ABP核心模块的启动过程中,会预先初始化这些配置管理类,如下代码所示

然后在AddSettingProviders中加入预先定义好的配置类。

接着在完成初始化过程中,有配置定义类统一根据这些配置对象,进行定义的初始化,这样才能在系统中进行使用。

配置定义的管理类接口,可以用下面这个图示进行说明。

以上就是在ABP框架中,基于配置模块的管理过程。

一般情况下,如果我们需要在Web API端中对这些接口进行调用管理,如对用户或者系统Email配置信息的获取和修改,那么我们需要定义一个配置接口服务(默认下载的ABP框架中没有公布这个接口定义和实现)。

如下我们定义一个SettingsAppService和他的接口

然后我们可以实现它的获取信息和修改信息的接口,如下所示是对系统级别的邮件参数进行配置管理。

        /// <summary>
        ///获取应用程序级别的邮件配置(系统邮件配置)/// </summary>
        /// <returns></returns>
        public async Task<EmailSettingsEditDto>GetEmailSettingsForApplication()
{
var smtpPassword = awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Password);return newEmailSettingsEditDto
{
DefaultFromAddress
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromAddress),
DefaultFromDisplayName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.DefaultFromDisplayName),
SmtpHost
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Host),
SmtpPort
= await SettingManager.GetSettingValueForApplicationAsync<int>(EmailSettingNames.Smtp.Port),
SmtpUserName
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.UserName),
SmtpPassword
=SimpleStringCipher.Instance.Decrypt(smtpPassword),
SmtpDomain
= awaitSettingManager.GetSettingValueForApplicationAsync(EmailSettingNames.Smtp.Domain),
SmtpEnableSsl
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.EnableSsl),
SmtpUseDefaultCredentials
= await SettingManager.GetSettingValueForApplicationAsync<bool>(EmailSettingNames.Smtp.UseDefaultCredentials)
};
}
/// <summary> ///更新应用程序级别的邮件配置(系统邮件配置)/// </summary> /// <returns></returns> public asyncTask UpdateEmailSettingsForApplication(EmailSettingsEditDto input)
{
awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromAddress, input.DefaultFromAddress);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.DefaultFromDisplayName, input.DefaultFromDisplayName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Host, input.SmtpHost);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Port, input.SmtpPort.ToString(CultureInfo.InvariantCulture));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UserName, input.SmtpUserName);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Password, SimpleStringCipher.Instance.Encrypt(input.SmtpPassword));awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.Domain, input.SmtpDomain);awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.EnableSsl, input.SmtpEnableSsl.ToString().ToLowerInvariant());awaitSettingManager.ChangeSettingForApplicationAsync(EmailSettingNames.Smtp.UseDefaultCredentials, input.SmtpUseDefaultCredentials.ToString().ToLowerInvariant());
}

2、使用自定义的参数配置管理

我在较早的随笔《
Winform开发框架之参数配置管理功能实现-基于SettingsProvider.net的构建
》中介绍过对配置信息的管理实现,这种配置参数方式一直很好的应用在我的各个框架上,定义和使用都相对比较简单,能够满足绝大多数的应用场景,相对ABP框架的配置模块来说,简单易用。

首先我们定义一个用来存储通用配置信息的表,如下所示。

这个配置表的主要特点也是以键为操作对象,然后内容是JSON序列化后的内容,可以存储用户自定义的类的序列号字符串,这个是它的灵魂所在。和ABP框架仅仅存储简单类型的值有所不同。

和其他模块的定义一样,我们可以先根据常规表的方式,使用代码快速生成类的结构,如下所示。

    /// <summary>
    ///用户参数配置,应用层服务接口实现/// </summary>
[AbpAuthorize]public class UserParameterAppService : MyAsyncServiceBase<UserParameter, UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
private readonly IRepository<UserParameter, string>_repository;public UserParameterAppService(IRepository<UserParameter, string> repository) : base(repository)
{
_repository
=repository;
}

然后定义几个用于用户级别和系统程序级别的接口实现,如获取信息,修改信息等。

然后,在生成的Caller层类里面,增加以上的Web API接口调用的实现代码,如下所示

    /// <summary>
    ///用户参数配置的Web API调用处理/// </summary>
    public class UserParameterApiCaller : AsyncCrudApiCaller<UserParameterDto, string, UserParameterPagedDto, CreateUserParameterDto, UserParameterDto>, IUserParameterAppService
{
/// <summary> ///提供单件对象使用/// </summary> public staticUserParameterApiCaller Instance
{
get{return Singleton<UserParameterApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicUserParameterApiCaller()
{
this.DomainName = "UserParameter";//指定域对象名称,用于组装接口地址 }public async Task<UserParameterDto>GetSettingForUser(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForUser(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
public async Task<UserParameterDto>GetSettingForApplication(NameInputDto input)
{
return await DoActionAsync<UserParameterDto>(MethodBase.GetCurrentMethod(), input);
}
public asyncTask ChangeSettingForApplication(NameValueDto input)
{
awaitDoActionAsync(MethodBase.GetCurrentMethod(), input);
}
}

如果对于上面的DoActionAsyn的处理有疑问,可以参考之前随笔《
ABP开发框架前后端开发系列---(10)Web API调用类的简化处理
》进行了解。

我在之前介绍过的配置模块里面,结合过FireFoxDialog界面效果,实现较好的参数配置管理功能,如下界面所示。

我们本次使用这两个不同的配置模块,也希望使用这个来展现一下,以便更好的理解。

由于整合了SettingsProvider.net组件,我们只需要封装一下对数据库的存储获取方式就可以了。

    /// <summary>
    ///数据库参数存储设置/// </summary>
    public classDatabaseStorage : JsonSettingsStoreBase
{
/// <summary> ///配置级别/// </summary> public SettingScopes Scope { get; set; }/// <summary> ///构造函数/// </summary> publicDatabaseStorage()
{
this.Scope =SettingScopes.User;
}
/// <summary> ///参数构造函数/// </summary> /// <param name="scope">配置级别</param> publicDatabaseStorage(SettingScopes scope)
{
this.Scope =scope;
}
/// <summary> ///保存到数据库/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <param name="fileContents">参数内容</param> protected override void WriteTextFile(string filename, stringfileContents)
{
var info = newNameValueDto(filename, fileContents);if (this.Scope ==SettingScopes.Application)
{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForApplication(info));
}
else{
AsyncContext.Run(()
=>UserParameterApiCaller.Instance.ChangeSettingForUser(info));
}
}
/// <summary> ///从数据库读取/// </summary> /// <param name="filename">文件名称(类型名称)</param> /// <returns></returns> protected override string ReadTextFile(stringfilename)
{
var info = newNameInputDto(filename);

UserParameterDto result
= null;if (this.Scope ==SettingScopes.Application)
{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForApplication(info));
}
else{
result
= AsyncContext.Run(() =>UserParameterApiCaller.Instance.GetSettingForUser(info));
}
return result != null ? result.Content : null;
}
}

有了这个实现,这样在操作上,就不用管理这些内容如何获取和更新了,和之前的使用配置管理方式一致了。可以处理各种不同的配置对象信息。

先来看看默认ABP的配置处理方式,管理界面如下所示。

这里的配置存储咋ABP的AbpSettings表里面,如下所示,每项内容是以字符串方式独立存储的。

它的调用主要就是SettingsApiCaller的内容了,注意这个邮件配置,必须在EmailSettingProvider中提前定义好对象的信息。

        privateEmailSettingsEditDto GetParameter()
{
EmailSettingsEditDto param
= AsyncContext.Run(() =>SettingsApiCaller.Instance.GetEmailSettingsForApplication());if(param == null)
{
param
= newEmailSettingsEditDto();
}
returnparam;
}
public override voidOnInit()
{
var parameter =GetParameter();if (parameter != null)
{
this.txtEmail.Text =parameter.DefaultFromAddress;this.txtLoginId.Text =parameter.SmtpUserName;this.txtPassword.Text =parameter.SmtpPassword;this.txtPassword.Tag =parameter.SmtpPassword;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpHost;this.txtUseSSL.Checked =parameter.SmtpEnableSsl;
}
}

下面我们再来看看自定义的配置管理方式。如下是自定义配置模块获取显示的内容。

这个配置是系统级别的,它的获取方式如下所示。

    public partial classPageEmailApplication : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageEmailApplication()
{
InitializeComponent();
if (!this.DesignMode)
{
store
= newDatabaseStorage(SettingScopes.Application);
settings
= newSettingsProvider(store);
}
}
public override voidOnInit()
{
EmailParameter parameter
= settings.GetSettings<EmailParameter>();if (parameter != null)
{
this.txtEmail.Text =parameter.Email;this.txtLoginId.Text =parameter.LoginId;this.txtPassword.Text =parameter.Password;this.txtPassword.Tag =parameter.Password;this.txtPop3Port.Value =parameter.Pop3Port;this.txtPop3Server.Text =parameter.Pop3Server;this.txtSmtpPort.Value =parameter.SmtpPort;this.txtSmtpServer.Text =parameter.SmtpServer;this.txtUseSSL.Checked =parameter.UseSSL;
}
}

以上是标准的SettingsProvider.net的组件调用方式,我们不用知道具体的数据存储,只需要把内容直接GetSetting方式获取出来即可。

而保存内容,直接通过使用SaveSettings保存即可。

                EmailParameter parameter = settings.GetSettings<EmailParameter>();if (parameter != null)
{
parameter.Email
= this.txtEmail.Text;
parameter.LoginId
= this.txtLoginId.Text;
parameter.Password
= this.txtPassword.Text;
parameter.Pop3Port
= Convert.ToInt32(this.txtPop3Port.Value);
parameter.Pop3Server
= this.txtPop3Server.Text;
parameter.SmtpPort
= Convert.ToInt32(this.txtSmtpPort.Value);
parameter.SmtpServer
= this.txtSmtpServer.Text;
parameter.UseSSL
= this.txtUseSSL.Checked;

settings.SaveSettings
<EmailParameter>(parameter);
}

其中 EmailParameter 类是我们定义的一个类,用来承载相关的配置信息,如下所示。它支持默认值,加密处理等设置。

    /// <summary>
    ///邮箱设置/// </summary>
    public classEmailParameter
{
/// <summary> ///邮件账号/// </summary> //[DefaultValue("wuhuacong@163.com")] public string Email { get; set; }/// <summary> ///POP3服务器/// </summary> [DefaultValue("pop.163.com")]public string Pop3Server { get; set; }/// <summary> ///POP3端口/// </summary> [DefaultValue(110)]public int Pop3Port { get; set; }/// <summary> ///SMTP服务器/// </summary> [DefaultValue("smtp.163.com")]public string SmtpServer { get; set; }/// <summary> ///SMTP端口/// </summary> [DefaultValue(25)]public int SmtpPort { get; set; }/// <summary> ///登陆账号/// </summary> public string LoginId { get; set; }/// <summary> ///登陆密码/// </summary> [ProtectedString]public string Password { get; set; }/// <summary> ///使用SSL加密/// </summary> [DefaultValue(false)]public bool UseSSL { get; set; }
}

由于SettingsProvider.net组件的支持,我们还可以把配置信息当成本地文件存储起来,对于一些需要存为文件的方式的配置,非常不错。

    public partial classPageReport : PropertyPage
{
privateSettingsProvider settings;privateISettingsStorage store;publicPageReport()
{
InitializeComponent();
if (!this.DesignMode)
{
//PortableStorage: 在运行程序目录创建一个setting的文件记录参数数据 store = newPortableStorage();
settings
= newSettingsProvider(store);
}
}

以上就是介绍了ABP配置管理模块的实现原理和客户端的调用,以及使用自定义配置管理模块的方式进行处理更加动态化或者灵活一点的配置信息,使用自定义配置信息管理服务,整合了SettingProvider.net的支持,可以实现更好的参数配置管理体验。