在前面随笔介绍的《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 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;
}

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

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

        /// <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();
}

那么我们主要处理的就是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 = ;
for (int i = ; 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;
}

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

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

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

如下是对于产品信息的一个界面展示,也是基于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 == ? "正常" : "停用";
}
#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);
} ......

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

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

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

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

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

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

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

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

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

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

ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程的更多相关文章

  1. ABP开发框架前后端开发系列---(14)基于Winform的ABP快速开发框架

    前面介绍了很多ABP系列的文章,一步一步的把我们日常开发中涉及到的Web API服务构建.登录日志和操作审计日志.字典管理模块.省份城市的信息维护.权限管理模块中的组织机构.用户.角色.权限.菜单等内 ...

  2. ABP开发框架前后端开发系列---(3)框架的分层和文件组织

    在前面随笔<ABP开发框架前后端开发系列---(2)框架的初步介绍>中,我介绍了ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以便基于数据库应用的简化处理.本篇随笔进一步对 ...

  3. ABP开发框架前后端开发系列---(2)框架的初步介绍

    在前面随笔<ABP开发框架前后端开发系列---(1)框架的总体介绍>大概介绍了这个ABP框架的主要特点,以及介绍了我对这框架的Web API应用优先的一些看法,本篇继续探讨ABP框架的初步 ...

  4. ABP开发框架前后端开发系列---(10)Web API调用类的简化处理

    在较早期的随笔<ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用>已经介绍了Web API调用类的封装处理,虽然这些调用类我们可以使用代码生成工具快 ...

  5. ABP开发框架前后端开发系列---(5)Web API调用类在Winform项目中的使用

    在前面几篇随笔介绍了我对ABP框架的改造,包括对ABP总体的介绍,以及对各个业务分层的简化,Web API 客户端封装层的设计,使得我们基于ABP框架的整体方案越来越清晰化, 也越来越接近实际的项目开 ...

  6. ABP开发框架前后端开发系列---(4)Web API调用类的封装和使用

    在前面随笔介绍ABP应用框架的项目组织情况,以及项目中领域层各个类代码组织,以及简化了ABP框架的各个层的内容,使得我们项目结构更加清晰.上篇随笔已经介绍了字典模块中应用服务层接口的实现情况,并且通过 ...

  7. ABP开发框架前后端开发系列---(11)菜单的动态管理

    在前面随笔<ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理>中介绍了基于ABP框架服务构建的Winform客户端,客户端通过Web API调用的方式进行获取数据,从而实现 ...

  8. ABP开发框架前后端开发系列---(9)ABP框架的权限控制管理

    在前面两篇随笔<ABP开发框架前后端开发系列---(7)系统审计日志和登录日志的管理>和<ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程>开始 ...

  9. ABP开发框架前后端开发系列---(12)配置模块的管理

    一般来说,一个系统或多或少都会涉及到一些系统参数或者用户信息的配置,而ABP框架也提供了一套配置信息的管理模块,ABP框架的配置信息,必须提前定义好配置的各项内容,然后才能在系统中初始化或者通过接口查 ...

随机推荐

  1. 使用Apache Tiles3.x构建界面布局

    Tiles是一个免费的开源模板Java应用程序的框架.基于复合模式简化的用户界面的构建.对于复杂的网站仍是最简单.最优雅的方式与任何MVC技术一起工作.Struts2对Tiles提供了支持,如今Til ...

  2. sql获取数据库的所有表以及名称字段

    获取数据库中所有的表 SELECT SysObjects.name AS Tablename FROM sysobjects WHERE xtype = 'U' 获取数据库中所有表的列名 SELECT ...

  3. 11991 - Easy Problem from Rujia Liu?(的基础数据结构)

    UVA 11991 - Easy Problem from Rujia Liu? 题目链接 题意:给一个长度n的序列,有m询问,每一个询问会问第k个出现的数字的下标是多少 思路:用map和vector ...

  4. ElasticSearch的基本用法与集群搭建 good

    一.简介 ElasticSearch和Solr都是基于Lucene的搜索引擎,不过ElasticSearch天生支持分布式,而Solr是4.0版本后的SolrCloud才是分布式版本,Solr的分布式 ...

  5. 1 DDD理论学习1 通用语言

    通用语言就是将事情描述清楚的语言 达到DDD的目标代码即设计,设计即代码.通俗的讲,也就是开发人员写的代码领域专家也能看懂. ddd模式跟传统模式的一个区别在于 传统先创建数据库表 再根据表创建类.而 ...

  6. Opencv决策树分类器应用

    机器学习在数据挖掘.计算机视觉.搜索引擎.医学诊断.证券市场分析.语言与手写识别等领域有着十分广泛的应用,特别是在数据分析挥着越来越重要的作用.在机器学习中,决策树是最基础且应用最广泛的归纳推理算法之 ...

  7. OpenCV实现朴素贝叶斯分类器诊断病情

    贝叶斯定理由英国数学家托马斯.贝叶斯(Thomas Baves)在1763提出,因此得名贝叶斯定理.贝叶斯定理也称贝叶斯推理,是关于随机事件的条件概率的一则定理. 对于两个事件A和B,事件A发生则B也 ...

  8. CMMI能力成熟度模型集成的过程域

    什么是CMMI CMMI全称是Capability Maturity Model Integration, 即能力成熟度模型集成,是由美国国防部(Office of the Secretary of ...

  9. Metropolis 采样与蒙特卡洛算法

    Metropolis 算法又叫 Metropolis 抽样,是模拟退火算法的基础,在早期的科学计算中蒙特卡洛方法(Monte Carlo)是对大量原子在给定温度下的平衡态的随机模拟,当蒙特卡洛算法计算 ...

  10. python 教程 第十五章、 结构布局

    第十五章. 结构布局 #!/usr/bin/env python #(1)起始行 "this is a module" #(2)模块文档 import sys #(3)模块导入 d ...