在前面随笔介绍的《ABP开发框架先后端开发系列---(7)系统审计日志和登陆日志的管理》里面,介绍了如何改进和完善审计日志和登陆日志的应用服务端和Winform客户端,因为篇幅限制,没有进一步详细介绍Winform界面的开发过程,本篇随笔介绍这部份内容,并进一步扩展Winform界面的各类状况处理,力求让它进入一个新的开发里程碑。html
前面介绍了如何扩展审计日志应用服务层(Application Service层)和ApiCaller层(API客户端调用封装层),同时也展现审计日志和登陆日志在Winform界面的展现,因为整个ABP框架目前我仍是采用了.net core的开发路线,全部的封装项目都是基于.net core基础上进行的。不过因为目前Winform尚未可以以 .net core进行开发,因此界面端仍是用.net framework的方式开发,不过能够调用 .net standard的类库。数据库
下面是审计日志的列表展现界面,和我以前的Winform框架同样的布局,所以我重用了Winform框架里面公用类库项目、基础界面封装项目、分页控件等内容,所以整个界面看起来仍是很一致的。后端
因为审计日志主要供底层记录,所以在界面不能增长增删改的操做,咱们只须要分页查询,和导出记录便可,以下窗体界面所示。浏览器
而明细内容,能够经过双击或者右键选择菜单打开便可弹出新的展现界面,主要展现审计日志里面的各项信息。服务器
而对于用户登陆日志来讲,处理方式差很少,也是经过在列表中查询展现,并在列表中整合右键菜单或者双击处理,能够查看登陆明细内容。架构
经过双击或者右键选择菜单打开便可弹出新的展现界面,主要展现登陆日志里面的各项信息。框架
上面展现了列表界面和查看明细界面,实际上咱们Winform的界面内部是如何处理的呢,咱们这里对其中的一些关键处理进行分析介绍。异步
列表界面的窗体初始化代码以下所示async
/// <summary> /// 审计日志 /// </summary> public partial class FrmAuditLog : BaseDock { private const string Id_FieldName = "Id";//Id的字段名称 public FrmAuditLog() { InitializeComponent(); //分页控件初始化事件 this.winGridViewPager1.OnPageChanged += new EventHandler(winGridViewPager1_OnPageChanged); this.winGridViewPager1.OnStartExport += new EventHandler(winGridViewPager1_OnStartExport); this.winGridViewPager1.OnEditSelected += new EventHandler(winGridViewPager1_OnEditSelected); this.winGridViewPager1.OnAddNew += new EventHandler(winGridViewPager1_OnAddNew); this.winGridViewPager1.OnDeleteSelected += new EventHandler(winGridViewPager1_OnDeleteSelected); this.winGridViewPager1.OnRefresh += new EventHandler(winGridViewPager1_OnRefresh); this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1; this.winGridViewPager1.ShowLineNumber = true; this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged +=new EventHandler(gridView1_DataSourceChanged); this.winGridViewPager1.gridView1.CustomColumnDisplayText += new DevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText); this.winGridViewPager1.gridView1.RowCellStyle += new DevExpress.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; }
这些是使用分页控件来初始化一些界面的处理事件,不要一看就抱怨须要编写这么多代码,这些基本上都是代码生成工具生成的,后面会介绍。ide
其实窗体的加载的时候,主要逻辑是初始化字典列表和展现列表数据,以下代码所示。
/// <summary> /// 编写初始化窗体的实现,能够用于刷新 /// </summary> public override async void FormOnLoad() { await InitDictItem(); await BindData(); }
其中这里都是使用async和await 配对实现的异步处理操做。咱们对于审计日志列表来讲,字典模块没有须要字典绑定信息,那么默认为空不用修改。
/// <summary> /// 初始化字典列表内容 /// </summary> private async Task InitDictItem() { //初始化代码 //await this.txtCategory.BindDictItems("报销类型"); await Task.FromResult(0); }
那么咱们主要处理的就是BindData的数据绑定操做了。
/// <summary> /// 绑定列表数据 /// </summary> private async Task BindData() { this.winGridViewPager1.DisplayColumns = "Id,BrowserInfo,ClientIpAddress,ClientName,CreationTime,Result,UserId,UserNameOrEmailAddress"; this.winGridViewPager1.ColumnNameAlias = await UserLoginAttemptApiCaller.Instance.GetColumnNameAlias();//字段列显示名称转义 //获取分页数据列表 var result = await GetData(); //设置全部记录数和列表数据源 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 = await UserLoginAttemptApiCaller.Instance.GetAll(pagerDto); return result; }
这个函数里面,主要是接收列表界面里面的查询条件,并构建对应的分页查询条件,这样根据条件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 in Repository.GetAll() join user in _userRepository.GetAll() on auditLog.UserId equals user.Id into userJoin from joinedUser in userJoin.DefaultIfEmpty() where auditLog.UserId.HasValue select new AuditLogAndUser { AuditLog = auditLog, User = joinedUser }; //过滤分页条件 return query .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 void ExportData() { string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName)); if (!string.IsNullOrEmpty(file)) { //获取分页数据列表 var result = await GetData(); 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, out error); 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 void DisplayData() { InitDictItem();//数据字典加载(公用) if (!string.IsNullOrEmpty(ID)) { #region 显示信息 var info = await AuditLogApiCaller.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 = await AuditLogApiCaller.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 = await AuditLogApiCaller.Instance.Get(ID.ToInt64()); if (info != null) { SetInfo(info); try { #region 更新数据 tempInfo = await AuditLogApiCaller.Instance.Update(info); if (tempInfo != null) { //可添加其余关联操做 return true; } #endregion } catch (Exception ex) { LogTextHelper.Error(ex); MessageDxUtil.ShowError(ex.Message); } } return false; }
咱们能够根据实际的须要,对咱们业务对象的窗体进行必定的改造便可。
例如对于前面的列表界面,一个比较复杂一点的列表展现内容,须要在查询条件中绑定字典列表,并对列表记录的一些状态进行特殊展现等,以及须要考虑增长、导入、导出等功能按钮,这些默认的列表生成界面就有的。
以下是对于产品信息的一个界面展现,也是基于ABP框架构建的服务进行数据展现的例子。
和前面介绍的例子同样,也是基于分页控件进行展现的,咱们来看看状态的处理吧。
因为状态和用户信息,咱们在数据库里面记录的是整形的数据信息,也就是状态为0,1的这样,以及用户ID等,咱们若是须要转义给客户端使用,那么咱们须要在对应的DTO里面增长一些字段进行承载,以下所示是产品信息的DTO对象,除了自己CreateProductDto必须有的字段外,咱们另外增长了两个属性,以下代码所示。
而后咱们在应用服务接口的ConvertDto转义函数里面增长本身的处理转义逻辑便可,以下代码所示。
/// <summary> /// 对记录进行转义 /// </summary> /// <param name="item">dto数据对象</param> /// <returns></returns> protected override void ConvertDto(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 class ExtensionMethod { /// <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 = await DictItemUtil.GetDictByDictType(dictTypeName, isCache); List<CListItem> itemList = new List<CListItem>(); foreach (string key in dict.Keys) { itemList.Add(new CListItem(key, dict[key])); } control.BindDictItems(itemList, defaultValue, emptyFlag); } ......
最后咱们能够看到,字典列表的效果以下所示。
新增产品信息界面以下所示。
这些都是标准的Winform界面模板,所以能够利用代码生成工具进行快速开发,利用代码生成工具Database2Sharp快速生成来实现ABP优化框架类文件的生成,以及界面代码的生成,而后进行必定的调整就是本项目的代码了。
ABP框架的基础代码生成咱们就再也不这里介绍了,主要介绍下Winform展现界面和编辑界面的快速生成便可。
在生成Abp框架的Winform界面面板中,配置咱们查询条件、列表展现、编辑展现内容等信息后,就能够生成对应的界面,而后复制到项目中使用便可,整个过程是比较快速的,这些开发便利但是花了我不少反复核对和优化NVelocity模板的开发时间的。
以下是代码生成工具Database2Sharp关于ABP框架的Winform界面配置。
设置好后直接生成,代码工具就能够依照模板来生成所须要的WInform列表界面和编辑界面的内容了,以下是生成的界面代码。
放到VS项目里面,就看到对应的窗体界面效果了。
生成界面后,进行必定的布局调整就能够实际用于生产环境了,省却了不少时间。