ABP开发框架前后端开发系列---(8)ABP框架之Winform界面的开发过程

在前面随笔介绍的《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框架里面公用类库项目、基础界面封装项目、分页控件等内容,因此整个界面看起来还是很一致的。

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

image

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

image

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

image

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

image

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

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

image

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

/// <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框架构建的服务进行数据展示的例子。

image

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

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

image

然后我们在应用服务接口的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
        }

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

image

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

image

这个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);
    }

......

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

image

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

image

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

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

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

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

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

image

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

image

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

image

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容