1. 程式人生 > >ABP開發框架前後端開發系列---(8)ABP框架之Winform介面的開發過程

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框架裡面公用類庫專案、基礎介面封裝專案、分頁控制元件等內容,因此整個介面看起來還是很一致的。

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

而明細內容,可以通過雙擊或者右鍵選擇選單開啟即可彈出新的展示介面,主要展示審計日誌裡面的各項資訊。

而對於使用者登入日誌來說,處理方式差不多,也是通過在列表中查詢展示,並在列表中整合右鍵選單或者雙擊處理,可以檢視登入明細內容。

通過雙擊或者右鍵選擇選單開啟即可彈出新的展示介面,主要展示登入日誌裡面的各項資訊。

  

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

 而對於編輯或者檢視介面,如下所示。

它的實現邏輯主要就是獲取單個記錄,然後在介面上逐一繫結控制元件內容顯示即可。

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

......

 

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

 新增產品資訊介面如下所示。

 

4、基於程式碼工具的Winform介面快速生成

這些都是標準的Winform介面模板,因此可以利用程式碼生成工具進行快速開發,利用程式碼生成工具Database2Sharp快速生成來實現ABP優化框架類檔案的生成,以及介面程式碼的生成,然後進行一定的調整就是本專案的程式碼了。

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

在生成Abp框架的Winform介面面板中,配置我們查詢條件、列表展示、編輯展示內容等資訊後,就可以生成對應的介面,然後複製到專案中使用即可,整個過程是比較快速的,這些開發便利可是花了我很多反覆核對和優化NVelocity模板的開發時間的。

如下是程式碼生成工具Database2Sharp關於ABP框架的Winform介面配置。

設定好後直接生成,程式碼工具就可以依照模板來生成所需要的WInform列表介面和編輯介面的內容了,如下是生成的介面程式碼。

放到VS專案裡面,就看到對應的窗體介面效果了。

生成介面後,進行一定的佈局調整就可以實際用於生產環境了,省卻了很多時