ABP開發(fā)框架前后端開發(fā)系列---(8)ABP框架之Winform界面的開發(fā)過程

在前面隨筆介紹的《ABP開發(fā)框架前后端開發(fā)系列---(7)系統(tǒng)審計日志和登錄日志的管理》里面,介紹了如何改進和完善審計日志和登錄日志的應用服務端和Winform客戶端,由于篇幅限制,沒有進一步詳細介紹Winform界面的開發(fā)過程,本篇隨筆介紹這部分內(nèi)容,并進一步擴展Winform界面的各種情況處理,力求讓它進入一個新的開發(fā)里程碑。

1、回顧審計日志和登陸日志管理界面

前面介紹了如何擴展審計日志應用服務層(Application Service層)和ApiCaller層(API客戶端調(diào)用封裝層),同時也展示審計日志和登錄日志在Winform界面的展示,由于整個ABP框架目前我還是采用了.net core的開發(fā)路線,所有的封裝項目都是基于.net core基礎上進行的。不過由于目前Winform還沒有能夠以 .net core進行開發(fā),所以界面端還是用.net framework的方式開發(fā),不過可以調(diào)用 .net standard的類庫。

下面是審計日志的列表展示界面,和我之前的Winform框架一樣的布局,因此我重用了Winform框架里面公用類庫項目、基礎界面封裝項目、分頁控件等內(nèi)容,因此整個界面看起來還是很一致的。

由于審計日志主要供底層記錄,因此在界面不能增加增刪改的操作,我們只需要分頁查詢,和導出記錄即可,如下窗體界面所示。

image

而明細內(nèi)容,可以通過雙擊或者右鍵選擇菜單打開即可彈出新的展示界面,主要展示審計日志里面的各項信息。

image

而對于用戶登錄日志來說,處理方式差不多,也是通過在列表中查詢展示,并在列表中整合右鍵菜單或者雙擊處理,可以查看登錄明細內(nèi)容。

image

通過雙擊或者右鍵選擇菜單打開即可彈出新的展示界面,主要展示登錄日志里面的各項信息。

image

2、Winform界面代碼實現(xiàn)

上面展示了列表界面和查看明細界面,實際上我們Winform的界面內(nèi)部是如何處理的呢,我們這里對其中的一些關鍵處理進行分析介紹。

列表界面的窗體初始化代碼如下所示

    /// <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;//是否設置為自動調(diào)整寬度,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);

            //關聯(lián)回車鍵進行查詢
            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;
        }

這些是使用分頁控件來初始化一些界面的處理事件,不要一看就抱怨需要編寫這么多代碼,這些基本上都是代碼生成工具生成的,后面會介紹。

其實窗體的加載的時候,主要邏輯是初始化字典列表和展示列表數(shù)據(jù),如下代碼所示。

        /// <summary>
        /// 編寫初始化窗體的實現(xiàn),可以用于刷新
        /// </summary>
        public override async void  FormOnLoad()
        {   
            await InitDictItem();
            await BindData();
        }

其中這里都是使用async和await 配對實現(xiàn)的異步處理操作。我們對于審計日志列表來說,字典模塊沒有需要字典綁定信息,那么默認為空不用修改。

        /// <summary>
        /// 初始化字典列表內(nèi)容
        /// </summary>
        private async Task InitDictItem()
        {
            //初始化代碼
            //await this.txtCategory.BindDictItems("報銷類型");

            await Task.FromResult(0);
        }

那么我們主要處理的就是BindData的數(shù)據(jù)綁定操作了。

        /// <summary>
        /// 綁定列表數(shù)據(jù)
        /// </summary>
        private async Task BindData()
        {
            this.winGridViewPager1.DisplayColumns = "Id,BrowserInfo,ClientIpAddress,ClientName,CreationTime,Result,UserId,UserNameOrEmailAddress";
            this.winGridViewPager1.ColumnNameAlias = await UserLoginAttemptApiCaller.Instance.GetColumnNameAlias();//字段列顯示名稱轉(zhuǎn)義

            //獲取分頁數(shù)據(jù)列表
            var result = await GetData();

            //設置所有記錄數(shù)和列表數(shù)據(jù)源
            this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的賦值,更新分頁信息
            this.winGridViewPager1.DataSource = result.Items;

            this.winGridViewPager1.PrintTitle = "用戶登錄日志報表";
        }

其中我們通過 調(diào)用服務端接口 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", "結(jié)果");
//this.winGridViewPager1.AddColumnAlias("UserId", "用戶ID");
//this.winGridViewPager1.AddColumnAlias("UserNameOrEmailAddress", "用戶名或郵件");

#endregion

只是基于服務端更加方便,也減少客戶端的編碼了。

而獲取數(shù)據(jù)主要通過 GetData 函數(shù)進行統(tǒng)一獲取對應的列表和數(shù)據(jù)記錄信息,如下是GetData的函數(shù)實現(xiàn)。

    /// <summary>
    /// 獲取數(shù)據(jù)
    /// </summary>
    /// <returns></returns>
    private async Task<IPagedResult<UserLoginAttemptDto>> GetData()
    {
        //構(gòu)建分頁的條件和查詢條件
        var pagerDto = new UserLoginAttemptPagedDto(this.winGridViewPager1.PagerInfo)
        {
            UserNameOrEmailAddress = this.txtUserNameOrEmailAddress.Text.Trim(),
        };

        //日期和數(shù)值范圍定義
        //時間,需在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;
    }

這個函數(shù)里面,主要是接收列表界面里面的查詢條件,并構(gòu)建對應的分頁查詢條件,這樣根據(jù)條件DTO就可以請求服務器的數(shù)據(jù)了。

前面講了,這個過濾條件并返回對應的數(shù)據(jù),主要就是在Application Service層,設置CreateFilteredQuery的控制邏輯即可,如下所示。

        /// <summary>
        /// 自定義條件處理
        /// </summary>
        /// <param name="input">分頁查詢Dto對象</param>
        /// <returns></returns>
        protected override IQueryable<AuditLog> CreateFilteredQuery(AuditLogPagedDto input)
        {
            //構(gòu)建關聯(lián)查詢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的界面實現(xiàn)邏輯。

上面通過GetData獲取到服務端數(shù)據(jù)后,我們就可以把列表數(shù)據(jù)綁定到分頁控件上面,讓分頁控件調(diào)用GridControl 進行展示出來即可。

//設置所有記錄數(shù)和列表數(shù)據(jù)源
this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount;
this.winGridViewPager1.DataSource = result.Items;

數(shù)據(jù)的導出操作,我們這里也順便提一下,雖然這些代碼是基于代碼生成工具生成的,不過還是提一下邏輯處理。

數(shù)據(jù)的導出操作,主要就是通過GetData獲取到數(shù)據(jù)后,轉(zhuǎn)換為DataTable,并通過Apose.Cell進行寫入Excel文件即可,如下代碼所示。

        /// <summary>
        /// 導出的操作
        /// </summary>        
        private async void ExportData()
        {
            string file = FileDialogHelper.SaveExcel(string.Format("{0}.xls", moduleName));
            if (!string.IsNullOrEmpty(file))
            {
                //獲取分頁數(shù)據(jù)列表
                var result = await GetData();
                var list = result.Items;
                DataTable dtNew = DataTableHelper.CreateTable("序號|int,Id,時間,用戶名,服務,操作,參數(shù),持續(xù)時間,IP地址,客戶端,瀏覽器,自定義數(shù)據(jù),異常,返回值");
                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["自定義數(shù)據(jù)"] = list[i].CustomData;
                    dr["異常"] = list[i].Exception;
                    dr["持續(xù)時間"] = list[i].ExecutionDuration;
                    dr["時間"] = list[i].ExecutionTime;
                    dr["操作"] = list[i].MethodName;
                    dr["參數(shù)"] = 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出現(xiàn)錯誤:{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

它的實現(xiàn)邏輯主要就是獲取單個記錄,然后在界面上逐一綁定控件內(nèi)容顯示即可。

/// <summary>
/// 數(shù)據(jù)顯示的函數(shù)
/// </summary>
public async override void DisplayData()
{
    InitDictItem();//數(shù)據(jù)字典加載(公用)

    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;//轉(zhuǎn)義的用戶名

        }
        #endregion 
    }
    else
    {
    }

    this.btnAdd.Visible = false;
    this.btnOK.Visible = false;
}

當然對于新增或編輯的界面,我們需要處理它的保存或者更新的操作事件,雖然審計日志不需要這些操作,不過生成的編輯窗體界面,依舊保留這些處理邏輯,如下代碼所示。

/// <summary>
/// 新增狀態(tài)下的數(shù)據(jù)保存
/// </summary>
/// <returns></returns>
public async override Task<bool> SaveAddNew()
{
    AuditLogDto info = tempInfo;//必須使用存在的局部變量,因為部分信息可能被附件使用
    SetInfo(info);

    try
    {
        #region 新增數(shù)據(jù)

        tempInfo = await AuditLogApiCaller.Instance.Create(info);
        if (tempInfo != null)
        {
            //可添加其他關聯(lián)操作

            return true;
        }
        #endregion
    }
    catch (Exception ex)
    {
        LogTextHelper.Error(ex);
        MessageDxUtil.ShowError(ex.Message);
    }
    return false;
}

/// <summary>
/// 編輯狀態(tài)下的數(shù)據(jù)保存
/// </summary>
/// <returns></returns>
public async override Task<bool> SaveUpdated()
{
    AuditLogDto info = await AuditLogApiCaller.Instance.Get(ID.ToInt64());
    if (info != null)
    {
        SetInfo(info);

        try
        {
            #region 更新數(shù)據(jù)

            tempInfo = await AuditLogApiCaller.Instance.Update(info);
            if (tempInfo != null)
            {
                //可添加其他關聯(lián)操作

                return true;
            }
            #endregion
        }
        catch (Exception ex)
        {
            LogTextHelper.Error(ex);
            MessageDxUtil.ShowError(ex.Message);
        }
    }
    return false;
}

我們可以根據(jù)實際的需要,對我們業(yè)務對象的窗體進行一定的改造即可。

3、復雜一點的WInform界面處理

例如對于前面的列表界面,一個比較復雜一點的列表展示內(nèi)容,需要在查詢條件中綁定字典列表,并對列表記錄的一些狀態(tài)進行特殊展示等,以及需要考慮增加、導入、導出等功能按鈕,這些默認的列表生成界面就有的。

如下是對于產(chǎn)品信息的一個界面展示,也是基于ABP框架構(gòu)建的服務進行數(shù)據(jù)展示的例子。

image

和前面介紹的例子一樣,也是基于分頁控件進行展示的,我們來看看狀態(tài)的處理吧。

由于狀態(tài)和用戶信息,我們在數(shù)據(jù)庫里面記錄的是整形的數(shù)據(jù)信息,也就是狀態(tài)為0,1的這樣,以及用戶ID等,我們?nèi)绻枰D(zhuǎn)義給客戶端使用,那么我們需要在對應的DTO里面增加一些字段進行承載,如下所示是產(chǎn)品信息的DTO對象,除了本身CreateProductDto必須有的字段外,我們另外增加了兩個屬性,如下代碼所示。

image

然后我們在應用服務接口的ConvertDto轉(zhuǎn)義函數(shù)里面增加自己的處理轉(zhuǎn)義邏輯即可,如下代碼所示。

        /// <summary>
        /// 對記錄進行轉(zhuǎn)義
        /// </summary>
        /// <param name="item">dto數(shù)據(jù)對象</param>
        /// <returns></returns>
        protected override void ConvertDto(ProductDto item)
        {
            //如需要轉(zhuǎn)義,則進行重寫

            #region 參考代碼
            //用戶名稱轉(zhuǎn)義
            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

前面也介紹了,對于產(chǎn)品類型屬性,我們一般是一個字典信息的,因此我們可以集成綁定字典的處理,如下代碼所示。

image

這個BindDictItems是擴展函數(shù),通過擴展函數(shù),我們對控件類型的綁定字典操作進行處理即可,具體的邏輯代碼如下所示。

/// <summary>
/// 擴展函數(shù)封裝
/// </summary>
internal static class ExtensionMethod
{
    /// <summary>
    /// 綁定下拉列表控件為指定的數(shù)據(jù)字典列表
    /// </summary>
    /// <param name="control">下拉列表控件</param>
    /// <param name="dictTypeName">數(shù)據(jù)字典類型名稱</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>
    /// 綁定下拉列表控件為指定的數(shù)據(jù)字典列表
    /// </summary>
    /// <param name="control">下拉列表控件</param>
    /// <param name="dictTypeName">數(shù)據(jù)字典類型名稱</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

新增產(chǎn)品信息界面如下所示。

image

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

這些都是標準的Winform界面模板,因此可以利用代碼生成工具進行快速開發(fā),利用代碼生成工具Database2Sharp快速生成來實現(xiàn)ABP優(yōu)化框架類文件的生成,以及界面代碼的生成,然后進行一定的調(diào)整就是本項目的代碼了。

ABP框架的基礎代碼生成我們就不再這里介紹了,主要介紹下Winform展示界面和編輯界面的快速生成即可。

在生成Abp框架的Winform界面面板中,配置我們查詢條件、列表展示、編輯展示內(nèi)容等信息后,就可以生成對應的界面,然后復制到項目中使用即可,整個過程是比較快速的,這些開發(fā)便利可是花了我很多反復核對和優(yōu)化NVelocity模板的開發(fā)時間的。

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

image

設置好后直接生成,代碼工具就可以依照模板來生成所需要的WInform列表界面和編輯界面的內(nèi)容了,如下是生成的界面代碼。

image

放到VS項目里面,就看到對應的窗體界面效果了。

image

生成界面后,進行一定的布局調(diào)整就可以實際用于生產(chǎn)環(huán)境了,省卻了很多時間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容