分类 其它 下的文章

我们先看看GMap.NET的定义:

GMap.NET是一个强大、免费、跨平台、开源的.NET控件,它在Windows Forms 和WPF环境中能够通过Google, Yahoo!, Bing, OpenStreetMap, ArcGIS, Pergo, SigPac等实现寻找路径、地理编码以及地图展示功能,并支持缓存和运行在Mobile环境中。

GMap.NET是一个开源的GEO地图定位和跟踪程序。就像谷歌地图、雅虎地图一样,可以自动计算两地的距离,定位经纬度,与Google地图不同的是,该项目是建立在C#语言WinForm基础上的。可以对地图放大缩小,进行城市标记等。

GMap.NET的项目地址是
http://greatmaps.codeplex.com/
,我们可以下载相关的例子和源码进行学习和研究。我在Google上搜过相关的项目,好像介绍的文章不多,不过不影响这个控件的强大和易用。我们先看看它的界面截图:

GMap.NET号称是可以支持很多种地图来源的,不过我试了一下,好像有部分是有些问题,最好的效果是GoogleMapChina,如上图所示。

我用GoogleMapChina可以放大到很详细的街道图,做了一个地址查询的例子,如下所示:

控件可以绘出两地的行车线路或者步行线路等,而且能够算出两地的距离,不过对于地理编码的解析好像不是很准确,也获取不到公交线路等信息,不过应付一般的应用,应该是蛮不错的了。

这个控件默认使用了右键按住作为拖动,和GoogleMap用鼠标左键作为拖动有点不太一样(不过可以通过this.gMapControl1.DragButton = MouseButtons.Left;来实现左键拖动),两者皆能够支持滚轮放大缩小的操作。控件还支持经纬度的精确定位,绘制图标(支持绿色、红色的图标、十字符号等标记),支持中心点移动 ,导出地图图片等功能。由于地图控件支持路线的绘制,所以应该支持一般的GIS应用中的轨迹回放功能的。由于地图控件支持鼠标位置和经纬度坐标的转换功能,因此,可以随意获取到相关的经纬度信息。

这个Winform的地图控件,虽然对比Web的GoogleMap来说,很多功能还不具备,但是较普通的MapX和MapXtreme或者ArcGis等传统的GIS来说,不用付太多的费用(甚至不用付费用),就可以使用上精细的地图,不得不说是一个好消息。

在前面随笔介绍的《
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项目里面,就看到对应的窗体界面效果了。

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

我们在使用EasyUI的时候,很多情况下需要使用到表格控件datagrid,这个控件控件非常强大,使用起来很简洁,但是我在使用中,发现对于一个表里面的外键字段进行转义,并显示引用表的一些名称的操作,却显得比较难以实现,找了很多资料,基本上没有找到对应的解决方案。本文主要介绍我对这种外键字段转义的操作的实现方式,以便供大家参考了解。

1、DataGrid的初始化操作

在了解对内容的解析前,我们先来了解EasyUI里面Datagrid的初始化操作过程,然后逐步进行分析,寻求解决方式。

一般情况下,Datagrid内容的初始化代码如下所示,注意下面红色部分的内容Customer_ID,就是我们需要转换为客户名称的处理。因为我们这里返回的表数据包含了一个外键ID:Customer_ID,我需要把它转换一下客户的名称。

        //实现对DataGird控件的绑定操作
function InitGrid(queryData) {
$(
'#grid').datagrid({ //定位到Table标签,Table标签的ID是grid url: '/Contact/FindWithPager', //指向后台的Action来获取当前用户的信息的Json格式的数据 title: '客户联系人',
iconCls:
'icon-view',
height:
650,
width: function () {
return document.body.clientWidth * 0.9 },//自动宽度 nowrap: true,
autoRowHeight:
false,
striped:
true,
collapsible:
true,
pagination:
true,
pageSize:
50,
pageList: [
50, 100, 200],
rownumbers:
true,//sortName: 'Seq',//根据某个字段给easyUI排序 sortOrder: 'asc',
remoteSort:
false,
idField:
'ID',
queryParams: queryData,
//异步查询的参数 columns: [[
{ field:
'ck', checkbox: true }, //选择 { title:'客户ID', field: 'Customer_ID', width: 180, sortable: true},{ title:'编号', field: 'HandNo', width: 80, sortable: true},
{ title:
'姓名', field: 'Name', width: 80, sortable: true},
{ title:
'身份证号码', field: 'IDCarNo', width: 120, sortable: true},
{
title:
'出生日期', field: 'Birthday', width: 120, sortable: true,
formatter: function (value, rec, index) {
if (value ==undefined) {return "";
}
if ((value + '').indexOf('1900') == 0) {return "";
}
returnvalue;
}
}
]],
toolbar: [{
id:
'btnAdd',
text:
'添加',
iconCls:
'icon-add',
handler: function () {
ShowAddDialog();
//实现添加记录的页面 }
},
'-', {
id:
'btnEdit',
text:
'修改',
iconCls:
'icon-edit',
handler: function () {
ShowEditOrViewDialog();
//实现修改记录的方法 }
},
'-', {
id:
'btnDelete',
text:
'删除',
iconCls:
'icon-remove',
handler: function () {
Delete();
//实现直接删除数据的方法 }
},
'-', {
id:
'btnView',
text:
'查看',
iconCls:
'icon-table',
handler: function () {
ShowEditOrViewDialog(
"view");//实现查看记录详细信息的方法 }
},
'-', {
id:
'btnReload',
text:
'刷新',
iconCls:
'icon-reload',
handler: function () {
//实现刷新栏目中的数据 $("#grid").datagrid("reload");
}
}],
onDblClickRow: function (rowIndex, rowData) {
$(
'#grid').datagrid('uncheckAll');
$(
'#grid').datagrid('checkRow', rowIndex);
ShowEditOrViewDialog();
}
})
};

2、尝试失败的操作

1)使用格式化函数Formatter

针对以上的处理,有些人可能很快就想到使用格式化Formatter来实现了,一般情况下处理转义和自定义显示操作,非此莫属。

有可能想使用的代码如下所示。

{
title:
'客户名称', field: 'Customer_ID', width: 180, sortable: true,
formatter:
function(value, row) {
$.ajaxSettings.async
= false;
$.getJSON(
"/Customer/GetCustomerName?id=" + value, function(result) {return "<span>" + result + "</span>";
});
}
},

但是,如果这样做,你会发现格式化函数没有办法调用ajax的操作,获取对应的数据,也就是无法进行解析客户的ID为名称。因此这种方法,失败!

2)使用onLoadSuccess函数

这个onLoadSuccess函数,本意就是在Datagrid顺利加载后执行的函数,一般情况下,我想在加载后,在更新表格里面的数据,如下面的代码所示。

                onLoadSuccess: function(data) {var rows = $("#grid").datagrid("getRows");if (rows.length >= 1) {for (var i = 0; i < rows.length; i++) {
$.getJSON(
"/Customer/GetCustomerName?id=" + rows[i].Customer_ID, function(result) {
$(
'#grid').datagrid('updateRow', { index: 0, row: { field: result } });
});
}
}
}

下面的方法也是通过Ajax的方式获取数据,然后进行更新,不过很不幸,也不通过,无法正常解析。

3、成功解析的方式

好了,既然无法通过上面脚本的方式来进行解析,我们通过曲线救国的方式,应该也是可以的。

由于Datagrid显示的数据是下面的方式就可以的

            var result = new { total = list.Count, rows = list };

那么我们在返回数据给datagrid的控制器函数里面,对返回的内容,增加一个“
客户名称
”的信息,应该就可以了。

1)转换内容为DataTable并增加字段

但是我的框架里面,返回的内容都设置为了List<T>的这种方式,也就是T代表的是实体类,我们很难改变实体类里面的属性并赋值,那么我们也可以把它转换为DataTable了。

            //增加一个客户名称字段,然后进行解析,构建一个DataTable返回
            DataTable dtReturn = DataTableHelper.ListToDataTable<ContactInfo>(list);
dtReturn.Columns.Add(
"CustomerName");foreach (DataRow row indtReturn.Rows)
{
row[
"CustomerName"] = BLLFactory<Customer>.Instance.GetCustomerName(row["Customer_ID"].ToString());
}
var result = new { total = dtReturn.Rows.Count, rows = dtReturn };

通过函数DataTableHelper.ListToDataTable<ContactInfo>(list);可以把列表的内容构建成一个DataTable的内容,并增加一个CustomerName的字段,然后遍历每一行,填入解析Customer_ID后的名称,并返回记录就可以了。

在视图里面,我们通过增加一个字段进行绑定就可以了,如下所示。

{
title:
'客户ID', field: 'Customer_ID', width: 180, sortable: true, hidden:true },
{ title:
'客户名称', field: 'CustomerName', width: 180, sortable: true },

2)在实体类基类增加一些额外的字段属性

刚才我们看到,把实体类列表转换为DataTable,然后并遍历赋值,挺麻烦的一件事,也可能影响一些性能,如果我们实体类里面有一些备用的属性作为内容解析,在界面上直接使用这些备用属性就可以了,这样会更加方便。

因此我在所有实体类的基类里面增加三个属性,Data1、Data2、Data3,有点类似Visio模具形状的属性设置了,呵呵。

    /// <summary>
    ///框架实体类的基类/// </summary>
[DataContract]public classBaseEntity
{
#region 在实体类存储一些特殊的数据 /// <summary> ///用来给实体类传递一些额外的数据,如外键的转义等,该字段不保存到数据表中/// </summary> [DataMember]public string Data1 { get; set; }/// <summary> ///用来给实体类传递一些额外的数据,如外键的转义等,该字段不保存到数据表中/// </summary> [DataMember]public string Data2 { get; set; }/// <summary> ///用来给实体类传递一些额外的数据,如外键的转义等,该字段不保存到数据表中/// </summary> [DataMember]public string Data3 { get; set; }#endregion}

有了这些备用的属性,我们就可以解决外键转义的内容存储问题了。

在查询里面,我们只需要遍历一次,把转换好的内容赋值给对应的实体类属性就好了。

            foreach (ContactInfo info inlist)
{
//增加一个特殊字段的转义 info.Data1 = BLLFactory<Customer>.Instance.GetCustomerName(info.Customer_ID);
}

最后在视图里面,我们的代码如下所示。

{
title:
'客户ID', field: 'Customer_ID', width: 180, sortable: true, hidden:true},
{ title:
'客户名称', field: 'Data1', width: 180, sortable: true },

主界面里面的列表展示如下所示。

数据导入界面里面的列表展示如下所示。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

前几天,有一位园友写了一篇不错的文章《
WinForm 清空界面控件值的小技巧
》,文章里面介绍了怎么清空界面各个控件值的一个好技巧,这个方法确实是不错的,在繁杂的界面控件值清理中,可谓省时省力。

本人在开发Winform程序中,也有一个类似的小技巧,不是清空控件值,而是赋值,给复选框赋值和获取值的小技巧,分享讨论一下。

应用场景是这样的,如果你有一些需要使用复选框来呈现内容的时候,如下面两图所示:

以上的切除部分的内容,是采用在GroupBox中放置多个CheckBox的方式;其实这个部分也可以使用Winform控件种的CheckedListBox控件来呈现内容。如下所示。

不管采用那种控件,我们都会涉及到为它赋值的麻烦,我这里封装了一个函数,可以很简单的给控件 赋值,大致代码如下。

CheckBoxListUtil.SetCheck(
this
.groupRemove, info.切除程度);

那么取控件的内容代码是如何的呢,代码如下:

info.切除程度
=
CheckBoxListUtil.GetCheckedItems(
this
.groupRemove);

赋值和取值通过封装函数调用,都非常简单,也可以重复利用,封装方法函数如下所示。



代码


public

class
CheckBoxListUtil
{

///

<summary>


///
如果值列表中有的,根据内容勾选GroupBox里面的成员.

///

</summary>


///

<param name="group">
包含CheckBox控件组的GroupBox控件
</param>


///

<param name="valueList">
逗号分隔的值列表
</param>



public

static

void
SetCheck(GroupBox group,
string
valueList)
{

string
[] strtemp
=
valueList.Split(
'
,
'
);

foreach
(
string
str
in
strtemp)
{

foreach
(Control control
in
group.Controls)
{
CheckBox chk

=
control
as
CheckBox;

if
(chk
!=

null

&&
chk.Text
==
str)
{
chk.Checked

=

true
;
}
}
}
}


///

<summary>


///
获取GroupBox控件成员勾选的值

///

</summary>


///

<param name="group">
包含CheckBox控件组的GroupBox控件
</param>


///

<returns>
返回逗号分隔的值列表
</returns>



public

static

string
GetCheckedItems(GroupBox group)
{

string
resultList
=

""
;

foreach
(Control control
in
group.Controls)
{
CheckBox chk

=
control
as
CheckBox;

if
(chk
!=

null

&&
chk.Checked)
{
resultList

+=

string
.Format(
"
{0},
"
, chk.Text);
}
}

return
resultList.Trim(
'
,
'
);
}


///

<summary>


///
如果值列表中有的,根据内容勾选CheckedListBox的成员.

///

</summary>


///

<param name="cblItems">
CheckedListBox控件
</param>


///

<param name="valueList">
逗号分隔的值列表
</param>



public

static

void
SetCheck(CheckedListBox cblItems,
string
valueList)
{

string
[] strtemp
=
valueList.Split(
'
,
'
);

foreach
(
string
str
in
strtemp)
{

for
(
int
i
=

0
; i
<
cblItems.Items.Count; i
++
)
{

if
(cblItems.GetItemText(cblItems.Items[i])
==
str)
{
cblItems.SetItemChecked(i,

true
);
}
}
}
}


///

<summary>


///
获取CheckedListBox控件成员勾选的值

///

</summary>


///

<param name="cblItems">
CheckedListBox控件
</param>


///

<returns>
返回逗号分隔的值列表
</returns>



public

static

string
GetCheckedItems(CheckedListBox cblItems)
{

string
resultList
=

""
;

for
(
int
i
=

0
; i
<
cblItems.CheckedItems.Count; i
++
)
{

if
(cblItems.GetItemChecked(i))
{
resultList

+=

string
.Format(
"
{0},
"
, cblItems.GetItemText(cblItems.Items[i]));
}
}

return
resultList.Trim(
'
,
'
);
}

}

以上代码分为两部分, 其一是对GroupBox的控件组进行操作,第二是对CheckedListBox控件进行操作。

这样在做复选框的时候,就比较方便一点,如我采用第一种GroupBox控件组方式,根据内容勾选的界面如下所示。

应用上面的辅助类函数,如果你是采用GroupBox方案,你就可以随便拖几个CheckBox控件进去就可以了,也犯不着给他取个有意义的名字,因为不管它是张三还是李四,只要它的父亲是GroupBox就没有问题了。

我们了解ABP框架内部自动记录审计日志和登录日志的,但是这些信息只是在相关的内部接口里面进行记录,并没有一个管理界面供我们了解,但是其系统数据库记录了这些数据信息,我们可以为它们设计一个查看和导出这些审计日志和登录日志的管理界面。本篇随笔继续ABP框架的系列介绍,一步步深入了解ABP框架的应用开发,介绍审计日志和登录日志的管理。

1、审计日志和登录日志的基础

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

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

我们查看系统数据库,可以看到对应这两个部分的日志表,如下所示。

在ABP框架内部基础项目Abp里面,我们可以看到对应的领域对象实体和Store管理类,不过并没有在应用层的对应服务和相关的DTO,我们需要实现一个审计日志和登陆日志的管理功能界面,界面效果如下所示。

我们搜索ABP项目,查找到审计日志的相关类(包含领域对象实体和Store管理类),如下界面截图。

同样对于系统登录日志对象,我们查找到对应的领域实体和对应的Manger业务逻辑类。

这些也就代表它们都有底层的实现,但是没有服务层应用和DTO对象,因此我们需要扩展这些内容才能够管理显示这些记录信息。

前面介绍过,默认的一般应用服务层和接口,都是会进行审计记录写入的,如果我们需要屏蔽某些应用服务层或者接口,不进行审计信息的记录,那么需要使用特性标记[DisableAuditing]来管理。

如我们针对审计日志应用层接口的访问,我们不想让它多余的记录,那么就设置这个标记即可。

或者屏蔽某些接口

另外,如果我们不想公布某些特殊的接口访问,那么我们可以通过标记 [RemoteService(false)]  进行屏蔽,这样在Web API层就不会公布对应的接口了。

如对于审计日志的记录,增删改我们都不允许客户端进行操作,那么我们把对应的应用服务层接口屏蔽即可。

2、系统审计日志和登录日志的完善

前面介绍了,审计日志和登陆日志的处理,Abp系统只是做了一部分底层的内容,我们如果进行这些信息的管理,我们需要完善它,增加对应的DTO类和应用服务层接口和接口实现。

首先我们根据底层的领域实体对象的属性,复制过来作为对应DTO对象的属性,并增加对应的分页条件DTO对象,由于我们不需要进行创建,因此不需要增加Create***Dto对象类。

如对于审计日志的DTO对象,我们定义如下所示(主要复制领域对象的属性)。

而分页处理的DTO对象如下所示,我们主要增加一个用户名和创建时间区间的条件。

对于登录日志的DTO对象,我们依葫芦画瓢,也是如此操作即可。

登录日志的分页对象Dto如下所示、

完善了这些DTO对象,下一步我们需要创建对应的应用服务层类,这样我们才能在客户端通过Web API获取对应的数据。

首先我们来定义审计日志应用服务类,如下所示。

    [DisableAuditing] //屏蔽这个AppService的审计功能
[AbpAuthorize]public class AuditLogAppService : AsyncCrudAppService<AuditLog, AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{private readonly IRepository<AuditLog, long>_repository;private readonlyIAuditingStore _stroe;private readonly IRepository<User, long>_userRepository;public AuditLogAppService(IRepository<AuditLog, long> repository, IAuditingStore stroe, IRepository<User, long> userRepository) : base(repository)
{
_repository
=repository;
_stroe
=stroe;
_userRepository
=userRepository;
}

......

其中我们需要IRepository<User, long>用来转义用户ID为对应的用户名,这样对于我们显示有帮助。

默认来说,这个应用服务层已经具有常规的增删改查、分页等基础接口了,但是我们不需要对外公布增删改接口,我们需要重写实现把它屏蔽。

        /// <summary>
        ///屏蔽创建接口/// </summary>
        [RemoteService(false)]public override Task<AuditLogDto>Create(AuditLogDto input)
{
return base.Create(input);
}
/// <summary> ///屏蔽更新接口/// </summary> [RemoteService(false)]public override Task<AuditLogDto>Update(AuditLogDto input)
{
return base.Update(input);
}
/// <summary> ///屏蔽删除接口/// </summary> [RemoteService(false)]public override Task Delete(EntityDto<long>input)
{
return base.Delete(input);
}

那么我们就剩下GetAll和Get两个方法了,我们如果不需要转义特殊内容,我们就可以不重写它,但是我们这里需要对用户ID转义为用户名称,那么需要进行一个处理,如下所示。

[DisableAuditing]public override Task<PagedResultDto<AuditLogDto>>GetAll(AuditLogPagedDto input)
{
var result = base.GetAll(input);foreach (var item inresult.Result.Items)
{
ConvertDto(item);
//对用户名称进行解析 }returnresult;
}
[DisableAuditing]
public override Task<AuditLogDto> Get(EntityDto<long>input)
{
var result = base.Get(input);
ConvertDto(result.Result);
returnresult;
}
/// <summary> ///对记录进行转义/// </summary> /// <param name="item">dto数据对象</param> /// <returns></returns> protected virtual voidConvertDto(AuditLogDto item)
{
//用户名称转义 if(item.UserId.HasValue)
{
item.UserName
=_userRepository.Get(item.UserId.Value).UserName;
}
//IP地址转义 if (!string.IsNullOrEmpty(item.ClientIpAddress))
{
item.ClientIpAddress
= item.ClientIpAddress.Replace("::1", "127.0.0.1");
}
}

这里主要就用户ID和IP地址进行一个正常的转义处理,这个也是我们常规接口需要处理的一种常见的情况之一。

排序我们是以执行时间进行排序,倒序显示即可,因此重写排序函数。

        /// <summary>
        ///自定义排序处理/// </summary>
        /// <param name="query"></param>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<AuditLog> ApplySorting(IQueryable<AuditLog>query, AuditLogPagedDto input)
{
return base.ApplySorting(query, input).OrderByDescending(s => s.ExecutionTime);//时间降序 }

一般情况下,我们就基本完成了这个模块的处理了,这样我们在界面上在花点功夫就可以调用这个API接口进行显示信息了,如下界面是我编写的审计日志分页列表显示界面。

明细展示界面如下所示。

上面列表界面管理中,如果我们还能够以用户进行过滤,那就更好了,因此需要添加一个用户名进行过滤(注意不是用户ID),系统表里面没有用户名称。

如果我们需要用户名称过滤,如下界面所示。

那么我们就需要在应用服务层的过滤函数里面处理相应的规则了。

我们先创建一个审计日志和用户信息的集合对象,如下所示。

    /// <summary>
    ///审计日志和用户的领域对象集合/// </summary>
    public classAuditLogAndUser
{
public AuditLog AuditLog { get;set;}public User User { get; set; }
}

然后在 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);
}

上面其实就是先通过EF的关联表查询,返回一个集合记录,然后在判断用户名是否在集合里面,最后返回所需的实体对象列表。

这个EF的关联表查询非常关键,这个也是我们联合查询的精髓所在,通过LINQ的方式,可以很方便实现关联表的查询处理并获得对应的结果。

而对于用户登录日志,由于系统记录了用户名,那么过滤用户名,这不需要这么大费周章关联表进行处理,只需要判断数据库字段对应情况即可,这种方便很多。

        /// <summary>
        ///自定义条件处理/// </summary>
        /// <param name="input"></param>
        /// <returns></returns>
        protected override IQueryable<UserLoginAttempt>CreateFilteredQuery(UserLoginAttemptPagedDto input)
{
return base.CreateFilteredQuery(input)
.WhereIf(
!string.IsNullOrEmpty(input.UserNameOrEmailAddress), t =>t.UserNameOrEmailAddress.Contains(input.UserNameOrEmailAddress))
.WhereIf(input.CreationTimeStart.HasValue, s
=> s.CreationTime >=input.CreationTimeStart.Value)
.WhereIf(input.CreationTimeEnd.HasValue, s
=> s.CreationTime <=input.CreationTimeEnd.Value);
}

同样系统用户登录日志界面如下所示。

用户登录明细界面效果如下所示。

以上就是对于审计日志和用户登录日志的扩展实现,包括了对相关DTO的增加和实现应用服务层接口,以及对Web API Caller层的实现。

    /// <summary>
    ///审计日志的Web API调用处理/// </summary>
    public class AuditLogApiCaller : AsyncCrudApiCaller<AuditLogDto, long, AuditLogPagedDto>, IAuditLogAppService<AuditLogDto, long, AuditLogPagedDto>{/// <summary>
        ///提供单件对象使用/// </summary>
        public staticAuditLogApiCaller Instance
{
get{return Singleton<AuditLogApiCaller>.Instance;
}
}
/// <summary> ///默认构造函数/// </summary> publicAuditLogApiCaller()
{
this.DomainName = "AuditLog";//指定域对象名称,用于组装接口地址 }
}

由于只是部分实现功能,我们还是可以基于前面介绍开发模式(利用代码生成工具Database2Sharp快速生成)来实现ABP优化框架类文件的生成,以及界面代码的生成,然后进行一定的调整就是本项目的代码了。

代码生成工具的ABP项目代码模板,和基于ABPWinform界面代码的模板,是我基于实际项目的反复优化和验证,并尽量减少冗余代码而完成的一种快速开发方式,基于这样开发方式可以大大减少项目开发的难度,提高开发效率,并完全匹配整个框架的需要,是一种非常惬意的快速开发方式。