在前面隨筆介紹的《ABP開發(fā)框架前后端開發(fā)系列---(7)系統(tǒng)審計日志和登錄日志的管理》里面,介紹了如何改進和完善審計日志和登錄日志的應(yīng)用服務(wù)端和Winform客戶端,由于篇幅限制,沒有進一步詳細介紹Winform界面的開發(fā)過程,本篇隨筆介紹這部分內(nèi)容,并進一步擴展Winform界面的各種情況處理,力求讓它進入一個新的開發(fā)里程碑。
1、回顧審計日志和登陸日志管理界面
前面介紹了如何擴展審計日志應(yīng)用服務(wù)層(Application Service層)和ApiCaller層(API客戶端調(diào)用封裝層),同時也展示審計日志和登錄日志在Winform界面的展示,由于整個ABP框架目前我還是采用了.net core的開發(fā)路線,所有的封裝項目都是基于.net core基礎(chǔ)上進行的。不過由于目前Winform還沒有能夠以 .net core進行開發(fā),所以界面端還是用.net framework的方式開發(fā),不過可以調(diào)用 .net standard的類庫。
下面是審計日志的列表展示界面,和我之前的Winform框架一樣的布局,因此我重用了Winform框架里面公用類庫項目、基礎(chǔ)界面封裝項目、分頁控件等內(nèi)容,因此整個界面看起來還是很一致的。
由于審計日志主要供底層記錄,因此在界面不能增加增刪改的操作,我們只需要分頁查詢,和導(dǎo)出記錄即可,如下窗體界面所示。
而明細內(nèi)容,可以通過雙擊或者右鍵選擇菜單打開即可彈出新的展示界面,主要展示審計日志里面的各項信息。
而對于用戶登錄日志來說,處理方式差不多,也是通過在列表中查詢展示,并在列表中整合右鍵菜單或者雙擊處理,可以查看登錄明細內(nèi)容。
通過雙擊或者右鍵選擇菜單打開即可彈出新的展示界面,主要展示登錄日志里面的各項信息。
2、Winform界面代碼實現(xiàn)
上面展示了列表界面和查看明細界面,實際上我們Winform的界面內(nèi)部是如何處理的呢,我們這里對其中的一些關(guān)鍵處理進行分析介紹。
列表界面的窗體初始化代碼如下所示
/// <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;//是否設(shè)置為自動調(diào)整寬度,false為不設(shè)置
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);
//關(guān)聯(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ù)和列表數(shù)據(jù)源
this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount; //需先于DataSource的賦值,更新分頁信息
this.winGridViewPager1.DataSource = result.Items;
this.winGridViewPager1.PrintTitle = "用戶登錄日志報表";
}
其中我們通過 調(diào)用服務(wù)端接口 GetColumnNameAlias 來獲取對應(yīng)的別名,其實我們也可以在Winform客戶端設(shè)置對等的別名處理,如下代碼所示。
#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
只是基于服務(wù)端更加方便,也減少客戶端的編碼了。
而獲取數(shù)據(jù)主要通過 GetData 函數(shù)進行統(tǒng)一獲取對應(yī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)建對應(yīng)的分頁查詢條件,這樣根據(jù)條件DTO就可以請求服務(wù)器的數(shù)據(jù)了。
前面講了,這個過濾條件并返回對應(yīng)的數(shù)據(jù),主要就是在Application Service層,設(shè)置CreateFilteredQuery的控制邏輯即可,如下所示。
/// <summary>
/// 自定義條件處理
/// </summary>
/// <param name="input">分頁查詢Dto對象</param>
/// <returns></returns>
protected override IQueryable<AuditLog> CreateFilteredQuery(AuditLogPagedDto input)
{
//構(gòu)建關(guān)聯(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);
}
這里就不在贅述服務(wù)層的邏輯代碼,主要關(guān)注我們本篇的主題,Winform的界面實現(xiàn)邏輯。
上面通過GetData獲取到服務(wù)端數(shù)據(jù)后,我們就可以把列表數(shù)據(jù)綁定到分頁控件上面,讓分頁控件調(diào)用GridControl 進行展示出來即可。
//設(shè)置所有記錄數(shù)和列表數(shù)據(jù)源
this.winGridViewPager1.PagerInfo.RecordCount = result.TotalCount;
this.winGridViewPager1.DataSource = result.Items;
數(shù)據(jù)的導(dǎo)出操作,我們這里也順便提一下,雖然這些代碼是基于代碼生成工具生成的,不過還是提一下邏輯處理。
數(shù)據(jù)的導(dǎo)出操作,主要就是通過GetData獲取到數(shù)據(jù)后,轉(zhuǎn)換為DataTable,并通過Apose.Cell進行寫入Excel文件即可,如下代碼所示。
/// <summary>
/// 導(dǎo)出的操作
/// </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,時間,用戶名,服務(wù),操作,參數(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["服務(wù)"] = 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("導(dǎo)出Excel出現(xiàn)錯誤:{0}", error));
}
else
{
if (MessageDxUtil.ShowYesNoAndTips("導(dǎo)出成功,是否打開文件?") == System.Windows.Forms.DialogResult.Yes)
{
System.Diagnostics.Process.Start(file);
}
}
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}
而對于編輯或者查看界面,如下所示。
它的實現(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)
{
//可添加其他關(guān)聯(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)
{
//可添加其他關(guān)聯(lián)操作
return true;
}
#endregion
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
return false;
}
我們可以根據(jù)實際的需要,對我們業(yè)務(wù)對象的窗體進行一定的改造即可。
3、復(fù)雜一點的WInform界面處理
例如對于前面的列表界面,一個比較復(fù)雜一點的列表展示內(nèi)容,需要在查詢條件中綁定字典列表,并對列表記錄的一些狀態(tài)進行特殊展示等,以及需要考慮增加、導(dǎo)入、導(dǎo)出等功能按鈕,這些默認的列表生成界面就有的。
如下是對于產(chǎn)品信息的一個界面展示,也是基于ABP框架構(gòu)建的服務(wù)進行數(shù)據(jù)展示的例子。
和前面介紹的例子一樣,也是基于分頁控件進行展示的,我們來看看狀態(tài)的處理吧。
由于狀態(tài)和用戶信息,我們在數(shù)據(jù)庫里面記錄的是整形的數(shù)據(jù)信息,也就是狀態(tài)為0,1的這樣,以及用戶ID等,我們?nèi)绻枰D(zhuǎn)義給客戶端使用,那么我們需要在對應(yīng)的DTO里面增加一些字段進行承載,如下所示是產(chǎn)品信息的DTO對象,除了本身CreateProductDto必須有的字段外,我們另外增加了兩個屬性,如下代碼所示。
然后我們在應(yīng)用服務(wù)接口的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
}
這樣客戶端就可以采用這兩個屬性展示信息了。
前面也介紹了,對于產(chǎn)品類型屬性,我們一般是一個字典信息的,因此我們可以集成綁定字典的處理,如下代碼所示。
這個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);
}
......
最后我們可以看到,字典列表的效果如下所示。
新增產(chǎn)品信息界面如下所示。
4、基于代碼工具的Winform界面快速生成
這些都是標準的Winform界面模板,因此可以利用代碼生成工具進行快速開發(fā),利用代碼生成工具Database2Sharp快速生成來實現(xiàn)ABP優(yōu)化框架類文件的生成,以及界面代碼的生成,然后進行一定的調(diào)整就是本項目的代碼了。
ABP框架的基礎(chǔ)代碼生成我們就不再這里介紹了,主要介紹下Winform展示界面和編輯界面的快速生成即可。
在生成Abp框架的Winform界面面板中,配置我們查詢條件、列表展示、編輯展示內(nèi)容等信息后,就可以生成對應(yīng)的界面,然后復(fù)制到項目中使用即可,整個過程是比較快速的,這些開發(fā)便利可是花了我很多反復(fù)核對和優(yōu)化NVelocity模板的開發(fā)時間的。
如下是代碼生成工具Database2Sharp關(guān)于ABP框架的Winform界面配置。
設(shè)置好后直接生成,代碼工具就可以依照模板來生成所需要的WInform列表界面和編輯界面的內(nèi)容了,如下是生成的界面代碼。
放到VS項目里面,就看到對應(yīng)的窗體界面效果了。
生成界面后,進行一定的布局調(diào)整就可以實際用于生產(chǎn)環(huán)境了,省卻了很多時間。