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閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容