在前面隨筆介紹的《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項目里面,就看到對應的窗體界面效果了。
生成界面后,進行一定的布局調整就可以實際用于生產環境了,省卻了很多時間。