wenmo8 发布的文章

一般情况下,我们都倾向于使用一个组织比较好的独立界面来录入或者展示相关的数据,这样处理比较规范,也方便显示比较复杂的数据。不过在一些情况下,我们也可能需要直接在GridView表格上直接录入或者修改数据,这种对于字段比较少,而且内容相对比较简单的情况下,效率是比较高的一种输入方式。本篇随笔主要介绍在DevExpress程序中使用GridView直接录入数据并保存的实现,以及使用Winform分页控件来进行数据直接录入的实现操作。

1、在GridView上展示数据

在GridView上展示数据,只需要绑定数据源给对应的GridControl控件即可,比较简单,可以绑定的数据格式也是很多,可以是DataTable,也可以支持IEnumerable格式的IList<T>集合对象等等,在我的框架中,倾向于使用IList<T>这种实体类集合的方式。

在网上大多数例子,绑定的数据源多数采用的是基于DataTable的方式,这种方式显示的数据受制于不同数据库的特点,字段可能大小写不一样,而且DevExpress操作行列的字段是大小写敏感的,因此如果你要字段名获取值的时候,传入的字段名一定是要大小写一致,因此在数据库不同的时候,如Oracle查询到的字段全部是大写的,SQLServer则是混合模式的(可以是Pascal,也可以是大写、小写)。

采用实体类集合对于框架层次的操作有很多好处,可以强类型,而且不受具体数据库的影响,所有交互均通过实体类或者其集合进行处理,相对来说有很大的优势。

一般情况下,对实体类的数据源,我们可以通过下面的方式进行数据绑定(使用分页控件的数据绑定方式)。

List<DictDataInfo> list = BLLFactory<DictData>.Instance.FindWithPager(condition, this.winGridViewPager1.PagerInfo);this.winGridViewPager1.DataSource = list;

如果需要在GridView上对记录进行新增修改的,也就是需要存储绑定数据的状态的,那么就不能直接使用List<T>集合,而需要使用其BindingList<T>的集合,同样BindingList<T>实现了IList<T>接口,我们可以通过构造函数new BindingList<T>(list)方式进行类型转换,然后重新绑定到数据源即可。

修改下上面的处理,我们可以得到如下的数据绑定方式:

/// <summary>
///绑定GridControl控件的数据源/// </summary>
private voidBindData()
{
//使用BindingList可以进行编辑记录,否则无法存储新增记录内容 var list = BLLFactory<DictData>.Instance.Find(treeConditionSql);this.gridControl1.DataSource = new BindingList<DictDataInfo>(list);
}

默认情况下,如果这样绑定,那么字段列名称会是实体类的属性名称,英文的,显然这样不太符合我们的显示处理,我们需要限定几个字段,而且需要显示中文内容,那么我们应该为GridView构建几个绑定的列字段才可以。

例如我们可以通过函数进行字段列的构建工作,如下所示。

其中的CreateColumn是为了简化处理模式引入的扩展函数,主要就是实现绑定字段的增加的,可以理解为下面代码的处理

这样我们绑定数据源后,就可以显示指定的一些字段内容了,而且是中文列名称,如下效果所示。

2、在GridView上录入数据和保存数据

但是,上面小节只是说明了数据如何绑定显示的,如果需要在GridView上直接录入数据,那么还需要做一些特殊的代码处理的。

在上面我们介绍使用BindingList<T>的数据源方式,确保可以显示并录入实体类集合数据的,如下代码绑定数据源所示。

如果我们需要设置GridView能够直接录入数据,那么需要设置下面几个属性,特别是红色那行。

grv.OptionsBehavior.ReadOnly = false;
grv.OptionsBehavior.Editable
= true;
grv.OptionsView.NewItemRowPosition
= NewItemRowPosition.Bottom;

对于一些常规的GridView初始化,我们可以通过一个扩展函数实现它的属性设置。

grv.InitGridView(GridType.NewItem, false, EditorShowMode.MouseDownFocused);

我们录入新的数据行时候,往往需要设置一些相关的属性,如父ID什么的,那么需要在InitNewRow事件里面进行处理,如下所示,我们希望在新建记录的时候,保留记录这个记录的父ID(来源自左侧树节点的ID信息)

其中代码

grv.SetRowCellValue(i, "DictType_ID", typeId);//存储记录的父ID

需要我们在这个GridView里面有一个隐藏的字段存在(如本例的DictType_ID字段),否则无法存储这个记录内容的。

如果我们需要在录入完成某个字段内容后,其他字段也做相应的改变,如触发关联变化,那么可以在CellValueChanged事件进行处理,如本例我们希望名称增加后,值和名称一致变化,那么代码如下所示。

这样我们在界面编辑的时候,就可以得到下面的效果了。

完成上面步骤,如果我们需要在录入数据后校验录入的信息,并提交数据库处理,如果成功则提示,那么就需要在事件ValidateRow里面进行处理了。

校验记录内容,我们可以通过下面的代码进行处理。

不过一般情况下,我们可以使用扩展函数来封装这些校验的处理,已达到简化代码的目的。

校验通过后提交数据库,我们首先需要做的方式是定位记录集合里面当前的记录,把它转换为具体的实体类对象,然后写入新记录或者更新处理,如下所示。

运行程序,并在GridView中录入记录后,并逐一回车,跳转到新一行的时候,触发数据校验和保存操作,保存成功后提示用户,并可以继续录入新的记录,非常方便,这个就是使用GridView直接录入数据并保存的方便地方。

结合GridControl的右键菜单,我们可以实现数据的删除、打印、导出等常规的功能整合。

3、基于Winform分页控件的数据展示和录入

在数据记录比较少的情况下,直接使用GridView进行展示全部记录,没有什么问题。但是在数据记录比较多的时候,如果用GridView全部展开所有的记录,会比较影响性能,比较好的方式就是对数据进行适当的分页。

在Winform开发中,我们使用封装好的分页控件来展示数据,这个分页的特性已经整合在Winform开发框架、混合式开发框架等框架底层里面了,直接调用相应的框架业务类方法,即可获取对应业务表的分页记录,非常方便。

前面两个小节都是基于原生的GridView控件对象进行数据的展示和录入,分页控件本身也是封装GridView控件的,因此也是可以直接利用它来进行数据的录入处理的。使用分页控件,可以高效提高数据的查询速度,另外可以利用代码生成工具Database2Sharp工具进行快速开发好Winform界面,然后进行一定的修改即可,非常方便。

我们来看看使用分页控件做的数据展示和录入界面效果图。

利用分页控件的方式和前面的直接使用GridView的方式大同小异,只是一些细节处理上进行修改即可。

分页控件,在利用代码生成工具Database2Sharp的Winform界面生成的时候,自动带有下面的分页控件初始化代码,我们屏蔽其中的新增和编辑处理事件,因为我们使用直接录入的方式,不需要弹出对话框进行处理。

代码如下所示。

由于我们使用分页控件,其在数据绑定的时候,列是自动创建的,因此我们不需要为GridView指定数据列,我们使用分页控件后的InitGridView的代码如下所示。

/// <summary>
///初始化GridView/// </summary>
private voidInitGridView()
{
var grd = this.winGridViewPager1.gridControl1;var grv = this.winGridViewPager1.gridView1;#region GridView控件初始化grv.InitGridView(GridType.NewItem,false, EditorShowMode.MouseDownFocused, "");
grv.InitNewRow
+= delegate(objectsender, InitNewRowEventArgs e)
{
//数据记录初始化的时候,设置ID和父节点的ID if (this.tree.FocusedNode != null)
{
var i =e.RowHandle;var typeId = this.tree.FocusedNode.GetValue("ID");
grv.SetRowCellValue(i,
"ID", Guid.NewGuid().ToString());
grv.SetRowCellValue(i,
"DictType_ID", typeId);//存储记录的父ID }
};
grv.CellValueChanged
+= delegate(objectsender, DevExpress.XtraGrid.Views.Base.CellValueChangedEventArgs e)
{
//如果字段的值发生改变,触发其他值改变则在此处理 if (e.Column.FieldName == "Name")
{
string value = string.Concat(grv.GetRowCellValue(e.RowHandle, "Value"));if (string.IsNullOrEmpty(value))
{
grv.SetRowCellValue(e.RowHandle,
"Value", e.Value);
}
}
};


grv.ValidateRow
+= delegate(objectsender, ValidateRowEventArgs e)
{
var result = grd.ValidateRowNull(e, new string[]
{
"Name"});//校验通过后提交数据库 if(result)
{
//获取当前操作的记录对象 var info = grv.GetFocusedRow() asDictDataInfo;//如果记录对象非空,则可以写入或者更新 if (info != null)
{
result
= BLLFactory<DictData>.Instance.InsertUpdate(info, info.ID);//如果不能写入,提示用户 if (!result)
{
e.Valid
= false;
e.ErrorText
= string.Format("写入数据出错");
}
else{base.ShowMessageAutoHide();
}
}
}
};
#endregion}

绑定数据源的方法BindData和直接生成的代码没有太大的差别,只是把GridView设置为可编辑即可,代码如下所示。

这样我们就可以实现基于Winform分页控件的基础上进行数据的直接录入操作了,处理方式就是先利用代码生成工具Database2Sharp进行Winform界面生成,然后进行微调代码即可,非常方便就可以实现这种快捷的数据录入了。

4、基于主从表数据录入的处理

在前面的记录保存和显示里面,都是采用一个视图的方式进行数据的展示和录入的,如果对于主从表的记录同时录入,那么就需要主从表两个GridView来进行展示和数据录入的了,对于主从表录入相对复杂一些,具体如何操作呢?

这里我们依旧采用分页控件来进行数据的分页及直接录入数据操作,而且增加了主从表数据同时在一个GridControl界面上进行处理。

这样主表记录为字典类型,从表为字典明细项目,得到的数据展示界面效果如下所示。

当然我们可直接在底部进行数据的录入,包括主表记录和从表的明细记录,都可以一气呵成的录入并进行保存处理的,界面效果如下所示。

GridView的主从关系需要设置好集合的映射关系,我们需要通过设置GridLevelNode集合实现主从表关系的处理的。

初始化从表的GridView2和主从表关系的代码如下所示

通过上面的初始化代码,指定了主从表的关系后,我们还需要对绑定的数据源进行一定的处理,才能够在GridControl控件上显示主从表关系的记录。

首先需要定义一个业务对象,用来存储主从关系的记录对象。

然后在BindData绑定数据的时候,代码处理如下即可。

这样就可以得到开始介绍的主从表界面效果了。

数据保存的代码和前面的操作类似,我们需要分别对GridView1和GridView2的数据保存操作进行处理,如下代码所示。

GridView2的字典项目明细保存操作如下所示。

主从表的记录删除这里需要顺带介绍一下,由于主从表公用一个右键菜单的删除操作。

那么处理的时候,我们需要判断是操作从表还是主表记录,对它们要分开处理,然后提示是否操作成功,如果成功,我们可以移除这行即可,避免重新更新数据导致的焦点丢失。

以上就是介绍各种在GridView界面上直接录入数据并保存的处理操作,虽然一般情况下面,我们建议通过独立的弹出界面进行内容的展示和录入,但是对于一些字段较少,或者喜欢直接录入记录的用户来说,这种方式也是一种非常不错的体验效果,也可以达到快速录入的目的,可以把这种方式作为我开发框架数据非常规录入的补充。

在我上篇随笔《
在DevExpress程序中使用Winform分页控件直接录入数据并保存
》中介绍了在GridView以及在其封装的分页控件上做数据的直接录入的处理,介绍情况下数据的保存和校验等操作,不过还没有涉及到数据列表选择的这种方式,而这种在项目应用也是比较广泛的一种输入方式。本篇随笔继续探讨在GridView上直接录入数据,并增加字典选择列表的功能。

1、GridView直接录入数据回顾

在之前整合的数据录入案例里面,我们可以看到可以在列表里面直接录入速度的便捷性,如下所示。

1)直接在GridView上录入并保存

2)基于WInform分页控件的直接录入和保存

这种方式就是利用在Griview上对数据校验后进行保存。

校验通过后提交数据库,我们首先需要做的方式是定位记录集合里面当前的记录,把它转换为具体的实体类对象,然后写入新记录或者更新处理,如下所示。

2、基于数据字典的下拉列表选择输入

我们下面介绍的内容,作为数据直接录入的补充,提供基于数据字典的下拉列表输入方式。

首先我们来看看整体的效果,然后在一步步分析其中的奥秘。

例如对于性别的选择方式。

以及基于可以搜索的下拉列表

以及多选框的数据显示处理

或者基于按钮选择对话框的实现

这些操作能够给列表录入提供多样化的选择,也丰富了用户的输入模式。

那么我们如何基于GridView的基础上实现这些功能呢?

首先我们基于模型构建数据库表,数据库表设计如下所示。

然后基于Database2Sharp代码生成工具生成框架底层的代码,以及生成WInform界面代码,生成的界面代码其中绑定数据部分如下所示。

/// <summary>
///绑定列表数据/// </summary>
private voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns = "Name,Sex,Nationality,BirthDate,Height,Weight,City,Area,State,Favorites,Introduction,Creator,CreateTime";this.winGridViewPager1.ColumnNameAlias = BLLFactory<Test>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();var list = BLLFactory<Test>.Instance.FindWithPager(where, this.winGridViewPager1.PagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<TestInfo>(list);this.winGridViewPager1.PrintTitle = "人员测试信息报表";
}

我们为了添加对应的直接录入方式,我们需要设置其中的字典绑定,处理方式如下所示,我们通过一个函数SetRepositoryItems来封装所需处理。

        /// <summary>
        ///设置GridControl对应的下拉类别内容,方便转义/// </summary>
        private voidSetRepositoryItems(GridView gridview)
{
var sexList = new List<CListItem>(){new CListItem("", "1"), new CListItem("", "2"), new CListItem("未知", "0")};
gridview.Columns.ColumnByFieldName(
"Sex").CreateLookUpEdit().BindDictItems(sexList, false);
gridview.Columns.ColumnByFieldName(
"City").CreateLookUpEdit().BindDictItems("城市");
gridview.Columns.ColumnByFieldName(
"Nationality").CreateSearchLookUpEdit().BindDictItems("民族");
gridview.Columns.ColumnByFieldName(
"Area").CreateLookUpEdit().BindDictItems("市场分区");
gridview.Columns.ColumnByFieldName(
"State").CreateLookUpEdit().BindDictItems("处理状态");
gridview.Columns.ColumnByFieldName(
"Favorites").CreateCheckedComboBoxEdit().BindDictItems("兴趣爱好");
gridview.Columns.ColumnByFieldName(
"Introduction").CreateMemoEdit();
gridview.Columns.ColumnByFieldName(
"Creator").CreateButtonEdit().ButtonClick += (object sender, ButtonPressedEventArgs e) =>{
FrmSelectCustomer dlg
= newFrmSelectCustomer();if(dlg.ShowDialog() ==System.Windows.Forms.DialogResult.OK)
{
if(gridview.GetFocusedRow() == null)
{
gridview.AddNewRow();
//如果首次则增加一行 }

gridview.SetFocusedRowCellValue(
"Creator", dlg.CustomerName);
}
};

gridview.OptionsBehavior.ReadOnly
= false;
gridview.OptionsBehavior.Editable
= true;
}

然后在上面的BindData函数里面加入这个方法调用即可。

SetRepositoryItems(this.winGridViewPager1.gridView1);

其中的CreateLookUpEdit、CreateSearchLookUpEdit、CreateCheckedComboBoxEdit、CreateButtonEdit等方法是框架底层进行的内容显示控件的处理,为了方便作为扩展函数直接使用的,其规则类似

代码类似下面的处理方式。

        /// <summary>
        ///创建GridView的列编辑为SearchLookUpEdit/// </summary>
        /// <param name="gridColumn">GridColumn列对象</param>
        /// <returns></returns>
        public static RepositoryItemSearchLookUpEdit CreateSearchLookUpEdit(thisGridColumn gridColumn)
{
RepositoryItemSearchLookUpEdit repositoryItem
= newRepositoryItemSearchLookUpEdit
{
AutoHeight
= false,
NullText
= ""};
gridColumn.View.GridControl.RepositoryItems.Add(repositoryItem);
gridColumn.ColumnEdit
=repositoryItem;returnrepositoryItem;
}

当然我们还需要注册响应的处理事件,代码如下所示。

private voidRegisterEvent()
{
var grd = this.winGridViewPager1.gridControl1;var grv = this.winGridViewPager1.GridView1;

grv.InitGridView(GridType.NewItem,
false);#region 列表处理事件grv.InitNewRow+= delegate(objectsender, InitNewRowEventArgs e)
{
GridView gridView
= grd.FocusedView asGridView;
gridView.SetFocusedRowCellValue(
"ID", Guid.NewGuid().ToString());
gridView.SetFocusedRowCellValue(
"Creator", LoginUserInfo.Name);
gridView.SetFocusedRowCellValue(
"CreateTime", DateTime.Now);
};

grv.ValidateRow
+= delegate(objectsender, ValidateRowEventArgs e)
{
var result = grd.ValidateRowNull(e, new string[]
{
"Name"});//校验通过后提交数据库 GridView gridView = grd.FocusedView asGridView;if(result)
{
var newInfo = grv.GetFocusedRow() asTestInfo;if (newInfo != null)
{
result
= BLLFactory<Test>.Instance.InsertUpdate(newInfo, newInfo.ID);if (!result)
{
e.Valid
= false;
e.ErrorText
= string.Format("写入数据出错");
}
else{base.ShowMessageAutoHide();
}
}
}
};
#endregion}

然后在窗体初始化的时候,调用上面的注册事件即可。

/// <summary>
///人员测试信息/// </summary>    
public partial classFrmTest : BaseDock
{
publicFrmTest()
{
InitializeComponent();

InitDictItem();
this.winGridViewPager1.OnPageChanged += newEventHandler(winGridViewPager1_OnPageChanged);this.winGridViewPager1.OnStartExport += newEventHandler(winGridViewPager1_OnStartExport);this.winGridViewPager1.OnEditSelected += newEventHandler(winGridViewPager1_OnEditSelected);this.winGridViewPager1.OnAddNew += newEventHandler(winGridViewPager1_OnAddNew);this.winGridViewPager1.OnDeleteSelected += newEventHandler(winGridViewPager1_OnDeleteSelected);this.winGridViewPager1.OnRefresh += newEventHandler(winGridViewPager1_OnRefresh);this.winGridViewPager1.AppendedMenu = this.contextMenuStrip1;this.winGridViewPager1.ShowLineNumber = true;this.winGridViewPager1.BestFitColumnWith = false;//是否设置为自动调整宽度,false为不设置 this.winGridViewPager1.gridView1.DataSourceChanged += newEventHandler(gridView1_DataSourceChanged);this.winGridViewPager1.gridView1.CustomColumnDisplayText += newDevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventHandler(gridView1_CustomColumnDisplayText);this.winGridViewPager1.gridView1.RowCellStyle += newDevExpress.XtraGrid.Views.Grid.RowCellStyleEventHandler(gridView1_RowCellStyle);//关联回车键进行查询 foreach (Control control in this.layoutControl1.Controls)
{
control.KeyUp
+= new System.Windows.Forms.KeyEventHandler(this.SearchControl_KeyUp);
}
//注册对应的GridView处理事件 RegisterEvent();
}

我们在数据源变化的时候,设置好各个列的宽度,方便正常显示内容就很完美了。

/// <summary>
///绑定数据后,分配各列的宽度/// </summary>
private void gridView1_DataSourceChanged(objectsender, EventArgs e)
{
if (this.winGridViewPager1.gridView1.Columns.Count > 0 && this.winGridViewPager1.gridView1.RowCount > 0)
{
//统一设置100宽度 foreach (DevExpress.XtraGrid.Columns.GridColumn column in this.winGridViewPager1.gridView1.Columns)
{
column.Width
= 100;
}
//Name,Sex,BirthDate,Height,Weight,City,Area,State,Favorites,Introduction,Creator,CreateTime//可特殊设置特别的宽度 SetGridColumWidth("BirthDate", 120);
SetGridColumWidth(
"CreateTime", 120);
SetGridColumWidth(
"Introduction", 200);
SetGridColumWidth(
"Favorites", 200);
}
}

这样,基于开发框架基础上就完成了这种直接录入数据的处理实现了,非常方便,当然如果直接利用没有封装的GridView处理,基本上也是没有太多变化,思路一样的。

在我的很多Winform开发项目中(包括混合框架的项目),统一采用了权限管理模块来进行各种权限的控制,包括常规的功能权限(按钮、菜单权限)、数据权限(记录的权限),另外还可以进行字段级别的字段权限控制,字段权限是我们在一些对权限要求比较严格的系统里面涉及到的,可以对部分用户隐藏一些敏感的信息。本篇主要介绍字段权限的控制思路及实现机制,以便大家对这个字段权限的控制有一个直观的了解。

如果需要对权限系统的功能进行一定的了解,可以先回顾下我前面的文章《
Winform开发框架之权限管理系统功能介绍
》、《
如何在应用系统中实现数据权限的控制功能
》、《
如何在应用系统中实现数据权限的控制功能(2)
》,以及《
Winform开发框架之权限管理系统改进的经验总结(1)-TreeListLookupEdit控件的使用
》、《
Winform开发框架之权限管理系统改进的经验总结(2)-用户选择界面的设计
》、《
Winform开发框架之权限管理系统改进的经验总结(4)--用户分级管理
》等文章。

1、字段权限的设计

字段的权限控制,一般就是控制对应角色人员的对某个表的一些敏感字段的可访问性:包括可见、可编辑性等处理。

在设计字段权限的时候,我们需要了解这些还是基于RBAC的概念,基于角色进行授权的,而且我们的字段列表是属于具体的业务对象列表的,这里的业务对象是指一些我们具体的业务模块,如客户基础信息、人员基础信息、报价单等等,我们就是基于这些业务进行字段的控制的。

如下界面所示,我们在权限系统里面也可以对其字段进行权限控制,如下图所示。先选择左边的具体角色,然后添加一些业务对象,并设置它们的权限即可。

首次业务对象需要用户加入,这里以程序集中的实体类进行字段信息的标识处理,如下所示可以加载对应业务信息。

我们在业务对象列表的【显示设置】处可以单击旁边的按钮,在弹出的界面上进行条件的设置,如下界面效果所示。

这样我们就完成了对某个业务对象的各个字段进行配置了,具体的字段控制在业务模块里面添加部分代码即可实现了。

例如我们以系统黑名单为例介绍,通过上面的方式进行设置,隐藏起始和结束IP地址的字段,那么列表界面得到的效果如下所示。

同时,如果系统界面有新增或者编辑界面,那么我们也需要隐藏才可以达到效果,如下是其的编辑界面效果(隐藏显示那两个字段了)。

以上就是整个字段权限控制的设计思路和实现了,但是具体我们是如何在业务模块里面整合这些权限控制呢?下面我们进行介绍。

2、字段权限的列表控制处理

前面我们介绍了在权限系统中进行业务对象的字段权限的设置流程,以及以其中的【登陆系统黑白名单】的业务模块进行的演示,那么我们如何才能在自己的业务模块里面进行控制处理的呢?

首先我们需要在业务列表绑定的时候,需要获取我们当前用户能够访问的字段列表,默认是全部可见,但是如果用户设置了条件,那么就需要获取对应的权限列表进行控制了,具体的控制代码如下所示。

//根据业务对象获取对应的显示字段,如果没有设置,那么根据FieldPermit表的配置获取字段权限列表
var permitDict = BLLFactory<FieldPermit>.Instance.GetColumnsPermit(typeof(BlackIPInfo).FullName, Portal.gc.UserInfo.ID);var displayColumns = BLLFactory<BlackIP>.Instance.GetDisplayColumns();
displayColumns
= string.IsNullOrEmpty(displayColumns) ? string.Join(",", permitDict.Keys) : displayColumns;this.winGridViewPager1.DisplayColumns = displayColumns;

然后在设置字段的中文映射显示

//设置字段的中文显示
this.winGridViewPager1.ColumnNameAlias = BLLFactory<BlackIP>.Instance.GetColumnNameAlias();//字段列显示名称转义

在对列表进行数据绑定后,我们统一设置各个字段的权限的可读写、可见或隐藏值权限即可,如下代码所示。

//获取字段显示权限,并设置
this.winGridViewPager1.gridView1.SetColumnsPermit(permitDict);

整个数据绑定的代码如下所示,
这些代码可以利用代码生成工具Database2Sharp进行界面代码统一生成

/// <summary>
///绑定列表数据/// </summary>
private voidBindData()
{
//entity//根据业务对象获取对应的显示字段,如果没有设置,那么根据FieldPermit表的配置获取字段权限列表 var permitDict = BLLFactory<FieldPermit>.Instance.GetColumnsPermit(typeof(BlackIPInfo).FullName, Portal.gc.UserInfo.ID);var displayColumns = BLLFactory<BlackIP>.Instance.GetDisplayColumns();
displayColumns
= string.IsNullOrEmpty(displayColumns) ? string.Join(",", permitDict.Keys) : displayColumns;this.winGridViewPager1.DisplayColumns =displayColumns;//设置字段的中文显示 this.winGridViewPager1.ColumnNameAlias = BLLFactory<BlackIP>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();
List
<BlackIPInfo> list = BLLFactory<BlackIP>.Instance.Find(where);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<BlackIPInfo>(list);this.winGridViewPager1.PrintTitle = "登陆系统的黑白名单列表报表";//获取字段显示权限,并设置 this.winGridViewPager1.gridView1.SetColumnsPermit(permitDict);
}

对于DevExpress的GridControl列表控件,我们一般在处理过程中需要设置字段的DisplayText转义,那么这种设置后,通过上面代码处理的权限就会失效,我们可以利用对Tag的标识的判断进行处理,如下所示,这样就避免了权限控制无效的情况。

void gridView1_CustomColumnDisplayText(objectsender, DevExpress.XtraGrid.Views.Base.CustomColumnDisplayTextEventArgs e)
{
//如果字段权限不够,那么字段的标签设置为*的 if (string.Concat(e.Column.Tag) != "*")
{
if (e.Column.ColumnType == typeof(DateTime))
{
string columnName =e.Column.FieldName;if (e.Value != null)
{
if (Convert.ToDateTime(e.Value) <= Convert.ToDateTime("1900-1-1"))
{
e.DisplayText
= "";
}
else{
e.DisplayText
= Convert.ToDateTime(e.Value).ToString("yyyy-MM-dd HH:mm");//yyyy-MM-dd }
}
}
else if (e.Column.FieldName == "AuthorizeType")
{
if (e.Value != null)
{
e.DisplayText
=((AuthrizeType)e.Value).ToString();
}
}
}
}

3、字段权限的显示窗体控制处理

如果在开发Winform界面的时候,把列表的展示统一放在GridControl里面进行展示,不再独立设计展示窗体,那么上面列表控制就已经达到了字段权限的控制目的了:可见或不可见、可编辑或只读、显示或隐藏值等处理。

在我的Winform框架中,我一般倾向于设计一个界面来展示业务对象的内容,一般新增,查看或者编辑都放在这个窗体上展示信息,比较直观,那么这种对字段权限的控制也需要延伸到这个显示窗体上;

对于普通的编辑控件,我们只能控制控件的可读写、可见与否的处理。

首先我们设计一个函数,用来设置控件的权限的,如下所示。

/// <summary>
///设置控件字段的权限显示或者隐藏/// </summary>
private voidSetPermit()
{
#region 设置控件和字段的对应关系 this.txtName.Tag = "Name";this.txtAuthorizeType.Tag = "AuthorizeType";this.txtForbid.Tag = "Forbid";this.txtIPStart.Tag = "IPStart";this.txtIPEnd.Tag = "IPEnd";this.txtNote.Tag = "Note";this.txtCreator.Tag = "Creator";this.txtCreateTime.Tag = "CreateTime";#endregion //获取列表权限的列表 var permitDict = CallerFactory<IFieldPermitService>.Instance.GetColumnsPermit(typeof(BlackIPInfo).FullName, Portal.gc.UserInfo.ID);this.SetControlPermit(permitDict, this.layoutControl1);
}

在上面,我们的逻辑就是先为每个控件绑定一个字段的标识,最后通过获取用户的字段权限列表,对控件的权限进行统一的控制处理即可。

为了开发的省时省力,
这些代码可以利用代码生成工具Database2Sharp进行界面代码统一生成

完成上面SetPermit函数的处理,我们在窗体界面的显示内容上,最后统一设置控件的权限即可,如下代码所示。

/// <summary>
///数据显示的函数/// </summary>
public override voidDisplayData()
{
InitDictItem();
//数据字典加载(公用) if (!string.IsNullOrEmpty(ID))
{
#region 显示信息BlackIPInfo info= CallerFactory<IBlackIPService>.Instance.FindByID(ID);if (info != null)
{
tempInfo
= info;//重新给临时对象赋值,使之指向存在的记录对象 txtName.Text=info.Name;
txtAuthorizeType.SetComboBoxItem(info.AuthorizeType.ToString());
txtForbid.Checked
=info.Forbid;
txtIPStart.Text
=info.IPStart;
txtIPEnd.Text
=info.IPEnd;
txtNote.Text
=info.Note;
txtCreator.Text
=info.Creator;
txtCreateTime.SetDateTime(info.CreateTime);
}
#endregion //this.btnOK.Enabled = Portal.gc.HasFunction("BlackIP/Edit"); }else{
txtCreator.Text
= Portal.gc.UserInfo.FullName;//默认为当前登录用户 txtCreateTime.DateTime = DateTime.Now; //默认当前时间//this.btnOK.Enabled = Portal.gc.HasFunction("BlackIP/Add"); }

RefreshUsers();

SetPermit();
}

以上就是字段权限的设计思路,实现控制过程,这样我们在权限里面实现了
功能权限、菜单权限、数据记录权限、字段权限的综合控制
,基本上能够满足大多数业务规则的要求了,从而提高了权限管理系统在整个应用开发中的通用性、便利性,一致性。

PostSharp是一种Aspect Oriented Programming 面向切面(或面向方面)的组件框架,适用在.NET开发中,本篇主要介绍Postsharp在.NET开发中的相关知识,以及一些如日志、缓存、事务处理、异常处理等常用的切面处理操作。

AOP(Aspect-Oriented Programming)是一种将函数的辅助性功能与业务逻辑相分离的编程泛型(programming paradigm),其目的是将横切关注点(cross-cutting concerns)分离出来,使得程序具有更高的模块化特性。AOP是面向方面软件开发(Aspect-Oriented Software Development)在编码实现层面上的具体表现。

我们知道,解耦是程序员编码开发过程中一直追求的,AOP也是为了解耦所诞生。引入AOP技术,能很大程度上简化我们编码,减少复制的代码量,也便于统一维护统一的部分代码,如日志、缓存、事务处理、异常处理等常用的处理。

1、AOP框架的介绍

1)AOP技术介绍

AOP技术利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

2)AOP使用场景

AOP用来封装横切关注点,具体可以在下面的场景中使用:

Authentication 权限

Caching 缓存

Context passing 内容传递

Error handling 错误处理

Lazy loading 懒加载

Debugging  调试

logging, tracing, profiling and monitoring 记录跟踪 优化 校准

Performance optimization 性能优化

Persistence  持久化

Resource pooling 资源池

Synchronization 同步

Transactions 事务

3)PostSharp框架

PostSharp是一个用于在.NET平台上实现AOP的框架,是比较常用的一个AOP框架,官方网站为
http://www.sharpcrafters.com
。目前最新版本为4.X,但是是收费的AOP软件。

PostSharp使用静态织入方式实现AOP,其连接点非常丰富,使用简单,而且相对其它一些.NET平台上的AOP框架来说,PostSharp较为轻量级,但是功能却一点也不逊色。

总体来说,使用PostSharp,将会带来如下优点:

  • 横切关注点单独分离出来,提高了代码的清晰性和可维护性。
  • 只要在Aspect中编写辅助性功能代码,在一定程度上减少了工作量和冗余代码。

当然,使用PostSharp也会存在一些缺点,主要缺点有如下两方面:

  • 增加了调试的难度。
  • 相比于不用AOP的代码,运行效率有所降低。

不过瑕不掩瑜,相对于这些缺点问题,使用PostSharp可以极大提高开发效率,减少重复代码,从而提高代码的可读性、可维护性。

另外在GitHub上还有一些开源的AOP组件,例如排头位的是KingAOP(https://github.com/AntyaDev/KingAOP),不过由于它采用了Dynamic的方式来实现,如它的构造对象如下所示。

dynamic helloWorld = newHelloWorld();
helloWorld.HelloWorldCall();

因此虽然比较方便,而且号称和PostSharp使用习惯类似,但是改变了对象的创建方式,对一般项目的类对象处理并不太适合。因此我还是比较倾向于使用PostSharp来进行AOP的编程开发。

2、PostSharp框架的使用

1)准备PostSharp的编译环境

PostSharp目前版本是4.x,我在官网下载了进行使用,不过经常发生"Error connecting to the pipe server. See previous warnings for details.",后来干脆使用了3.x版本的,反而能够正常使用,非常不错,呵呵。

PostSharp是一个可以安装在VS上的插件,安装后在VS的菜单栏目里面增加了一个PostSharp的菜单项,如下所示。

一般项目如果需要使用PostSharp特性的,在项目属性的【PostSharp】选项页中,使用【Add PostSharp to this project】把PostSharp加入到项目里面进行使用。

添加后,会弹出PostSharp的插件提示对话框,提示将加入相应的PostSharp包等内容,如下所示。

完成后就可以在项目中使用PostSharp的相关类了。

2)增加PostSharp的AOP切面处理

一般约定每个Aspect类的命名必须为“XXXAttribute”的形式。其中“XXX”就是这个Aspect的名字。PostSharp中提供了丰富的内置“Base Aspect”以便我们继承,其中这里我们继承“OnMethodBoundaryAspect ”,这个Aspect提供了进入、退出函数等连接点方法。另外,Aspect上必须设置“[Serializable] ”,这与PostSharp内部对Aspect的生命周期管理有关。

日志的Aspect类的代码如下所示。

[Serializable]public classLogAttribute : OnMethodBoundaryAspect
{
public override voidOnEntry(MethodExecutionArgs args)
{
Console.WriteLine(Environment.NewLine);

Console.WriteLine(
"Entering [ {0} ] ...", args.Method);base.OnEntry(args);
}
public override voidOnExit(MethodExecutionArgs args)
{
Console.WriteLine(
"Leaving [ {0} ] ...", args.Method);base.OnExit(args);
}
}

异常处理的类代码如下所示。

[Serializable]public classExceptionAttribute : OnExceptionAspect
{
public override voidOnException(MethodExecutionArgs args)
{
Console.WriteLine(String.Format(
"Exception in :[{0}] , Message:[{1}]", args.Method, args.Exception.Message));
args.FlowBehavior
=FlowBehavior.Continue;base.OnException(args);
}
}

计时处理的Aspect类代码如下所示。

[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method)]
public classTimingAttribute : PostSharp.Aspects.OnMethodBoundaryAspect
{
[NonSerialized]
Stopwatch _StopWatch;
public override voidOnEntry(PostSharp.Aspects.MethodExecutionArgs args)
{
_StopWatch
=Stopwatch.StartNew();base.OnEntry(args);
}
public override voidOnExit(PostSharp.Aspects.MethodExecutionArgs args)
{
Console.WriteLine(
string.Format("[{0}] took {1}ms to execute",new StackTrace().GetFrame(1).GetMethod().Name,
_StopWatch.ElapsedMilliseconds));
base.OnExit(args);
}
}

事务处理的Aspect类代码如下所示。

[Serializable]
[AspectTypeDependency(AspectDependencyAction.Order, AspectDependencyPosition.After,
typeof(LogAttribute))]public classRunInTransactionAttribute : OnMethodBoundaryAspect
{
[NonSerialized]
TransactionScope TransactionScope;
public override voidOnEntry(MethodExecutionArgs args)
{
this.TransactionScope = newTransactionScope(TransactionScopeOption.RequiresNew);
}
public override voidOnSuccess(MethodExecutionArgs args)
{
this.TransactionScope.Complete();
}
public override voidOnException(MethodExecutionArgs args)
{
args.FlowBehavior
=FlowBehavior.Continue;
Transaction.Current.Rollback();
Console.WriteLine(
"Transaction Was Unsuccessful!");
}
public override voidOnExit(MethodExecutionArgs args)
{
this.TransactionScope.Dispose();
}
}

下面是几个Aspect类的切面处理代码,如下所示。

[Exception]
[Log]
static voidCalc()
{
throw new DivideByZeroException("A Math Error Occured...");
}

[Log, Timing]
static voidLongRunningCalc()
{
//wait for 1000 miliseconds Thread.Sleep(1000);
}

从上面我们可以看到,常规的异常处理、日志处理都已经通过Attribute的方式进行处理了,在函数体里面都只是剩下具体的业务逻辑代码了,这样极大提高了代码的可读性,简洁明了。

运行上面的代码函数的调用,我们可以在输出日志里面看到具体的结果内容。

Entering [ Void Calc() ] ...
“System.DivideByZeroException”类型的第一次机会异常在 PostSharpExample.exe 中发生
Exception
in:[Void Calc()] , Message:[A Math Error Occured...]
Leaving [ Void Calc() ] ...


Entering [ Void LongRunningCalc() ] ...
Leaving [ Void LongRunningCalc() ] ...
[LongRunningCalc] took 1002ms to execute

这样,通过声明的方式,就实现了常规日志 、异常的处理,当然实际项目上使用日志、异常处理的这些代码肯定会更加复杂一些,不过小例子已经实现了切面逻辑的分离处理了,尘归尘、土归土,一切都是那么的简洁安静了。

在之前一篇随笔《
在.NET项目中使用PostSharp,实现AOP面向切面编程处理
》介绍了PostSharp框架的使用,试用PostSharp能给我带来很多便利和优势,减少代码冗余,提高可读性,并且可以更加优雅的实现常规的日志、异常、缓存、事务等业务场景的处理。本篇主要介绍使用MemoryCache实现缓存的处理。

1、MemoryCache的介绍回顾

上篇没有提及缓存的处理,一般情况下,缓存的处理我们可以利用微软的分布式缓存组件MemoryCache进行缓存的处理操作。MemoryCache的使用网上介绍的不多,不过这个是.NET4.0新引入的缓存对象,主要是替换原来企业库的缓存模块,使得.NET的缓存可以无处不在,而不用基于特定的Windows版本上使用。

缓存在很多情况下需要用到,合理利用缓存可以一方面可以提高程序的响应速度,同时可以减少对特定资源访问的压力。本文主要针对自己在Winform方面的缓存使用做一个引导性的介绍,希望大家能够从中了解一些缓存的使用场景和使用方法。缓存是一个中大型系统所必须考虑的问题。为了避免每次请求都去访问后台的资源(例如数据库),我们一般会考虑将一些更新不是很频繁的,可以重用的数据,通过一定的方式临时地保存起来,后续的请求根据情况可以直接访问这些保存起来的数据。这种机制就是所谓的缓存机制。

.NET 4.0的缓存功能主要由三部分组成:System.Runtime.Caching,System.Web.Caching.Cache和Output Cache。

System.Runtime.Caching这是在.NET 4.0中新增的缓存框架,主要是使用MemoryCache对象,该对象存在于程序集System.Runtime.Caching.dll。

System.Web.Caching.Cache这个则是在.NET2.0开始就一直存在的缓存对象,一般主要用在Web中,当然也可以用于Winform里面,不过要引用System.Web.dll。

Output Cache则是Asp.NET里面使用的,在ASP.NET 4.0之前的版本都是直接使用System.Web.Caching.Cache来缓存HTML片段。在ASP.NET 4.0中对它进行了重新设计,提供了一个OutputCacheProvider供开发人员进行扩展,但是它默认情况下,仍然使用System.Web.Caching.Cache来做做缓存

我在之前的一篇随笔《
Winform里面的缓存使用
》曾经介绍了MemoryCache辅助类的处理,用来方便实现缓存的数据操作。它的辅助类主要代码如下所示。

    /// <summary>
    ///基于MemoryCache的缓存辅助类/// </summary>
    public static classMemoryCacheHelper
{
private static readonly Object locker = new object();/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="slidingExpiration">失效的时间范围</param> /// <param name="absoluteExpiration">失效的绝对时间</param> /// <returns></returns> public static T GetCacheItem<T>(String key, Func<T> cachePopulate, TimeSpan? slidingExpiration = null, DateTime? absoluteExpiration = null)
{
if(String.IsNullOrWhiteSpace(key)) throw new ArgumentException("Invalid cache key");if(cachePopulate == null) throw new ArgumentNullException("cachePopulate");if(slidingExpiration == null && absoluteExpiration == null) throw new ArgumentException("Either a sliding expiration or absolute must be provided");if(MemoryCache.Default[key] == null)
{
lock(locker)
{
if(MemoryCache.Default[key] == null)
{
var item = newCacheItem(key, cachePopulate());var policy =CreatePolicy(slidingExpiration, absoluteExpiration);

MemoryCache.Default.Add(item, policy);
}
}
}
return(T)MemoryCache.Default[key];
}
private static CacheItemPolicy CreatePolicy(TimeSpan? slidingExpiration, DateTime?absoluteExpiration)
{
var policy = newCacheItemPolicy();if(absoluteExpiration.HasValue)
{
policy.AbsoluteExpiration
=absoluteExpiration.Value;
}
else if(slidingExpiration.HasValue)
{
policy.SlidingExpiration
=slidingExpiration.Value;
}

policy.Priority
=CacheItemPriority.Default;returnpolicy;
}
/// <summary> ///清空缓存/// </summary> public static voidClearCache()
{
List
<string> cacheKeys = MemoryCache.Default.Select(kvp =>kvp.Key).ToList();foreach (string cacheKey incacheKeys)
{
MemoryCache.Default.Remove(cacheKey);
}
}

...//省略部分代码

}

而我们在程序中,如果需要使用缓存,那么调用这个辅助类来解决,也算是比较方便的,实现缓存的代码如下所示。

    public static classUserCacheService
{
/// <summary> ///获取用户全部简单对象信息,并放到缓存里面/// </summary> /// <returns></returns> public static List<SimpleUserInfo>GetSimpleUsers()
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}", method.DeclaringType.FullName, method.Name);return MemoryCacheHelper.GetCacheItem<List<SimpleUserInfo>>(key,delegate() {//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for(int i = 0; i< 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
},
new TimeSpan(0, 10, 0));//10分钟过期 }/// <summary> ///根据用户的ID,获取用户的登陆名称,并放到缓存里面/// </summary> /// <param name="userId">用户的ID</param> /// <returns></returns> public static string GetNameByID(stringuserId)
{
string result = "";if (!string.IsNullOrEmpty(userId))
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}-{2}", method.DeclaringType.FullName, method.Name, userId);

result
= MemoryCacheHelper.GetCacheItem<string>(key,delegate() {//return CallerFactory<IUserService>.Instance.GetNameByID(userId.ToInt32()); return string.Concat("Name:", userId);
},
new TimeSpan(0, 30, 0));//30分钟过期 }returnresult;
}

上面案例我模拟构造数据库数据返回,否则一般使用BLLFactory<T>、或者混合框架客户端里面使用CallerFactory<T>进行调用接口了,相当于需要对它们进行进一步的函数封装处理才能达到目的。

案例中可以设置失效缓存时间,并且失效后,自动通过Func<T> cachePopulate的函数重新获取缓存内容,在实际情况下,也是非常智能的一种处理方式。

2、结合PostSharp和MemoryCache实现缓存

上面的案例使用MemoryCache辅助类来实现缓存的处理,能够解决实际的问题,不过同时问题也来了,每次缓存处理,都需要写一段额外的代码进行处理,代码的冗余就非常多了,而且一旦很多地方采用缓存,那么维护这些代码就很成问题。

我们希望引入PostSharp技术,来减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。这种AOP的代码织入技术能够很好分离横切面和业务处理,从而实现简化代码的目的。

就上面的代码问题,我们来看看,引入PostSharp后,我们的代码是如何实现缓存处理的。

    /// <summary>
    ///使用PostSharp,结合MemoryCache实现缓存的处理类/// </summary>
    public classCacheService
{
/// <summary> ///获取用户全部简单对象信息,并放到缓存里面/// </summary> /// <returns></returns> [Cache(ExpirationPeriod = 30)]public static List<SimpleUserInfo> GetSimpleUsers(intuserid)
{
//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for (int i = 0; i < 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
}
/// <summary> ///根据用户的ID,获取用户的登陆名称,并放到缓存里面/// </summary> /// <param name="userId">用户的ID</param> /// <returns></returns> [Cache]public static string GetNameByID(stringuserId)
{
//return CallerFactory<IUserService>.Instance.GetNameByID(userId.ToInt32()); return string.Concat("Name:", userId);
}
}

我们注意到了上面的函数代码,除了调用业务逻辑(这里构造数据演示)外,其实是没有多余的其他代码的。不过我们是在函数开始进行了一个特性的标识:

[Cache(ExpirationPeriod = 30)]

或者

[Cache]

这个就是我们声明使用缓存处理的函数,如此而已,是不是非常简单了。

我们来看看生成后的代码反编译得到的结果,如下所示。

这个和我们实际的代码是不太一样的,这里整合了PostSharp的织入代码,从而能够实现缓存的处理操作了,但是我们在开发过程中是透明的,只需要维护好自己编写的代码即可。

这个里面需要使用了CacheAttribute来进行标识,这个类的代码就是使用了PostSharp的基类进行处理了

    /// <summary>
    ///方法实现缓存的标识/// </summary>
[Serializable]public classCacheAttribute : MethodInterceptionAspect
{
/// <summary> ///缓存的失效时间设置,默认采用30分钟/// </summary> public int ExpirationPeriod = 30;/// <summary> ///PostSharp的调用处理,实现数据的缓存处理/// </summary> public override voidOnInvoke(MethodInterceptionArgs args)
{
//默认30分钟失效,如果设置过期时间,那么采用设置值 TimeSpan timeSpan = new TimeSpan(0, 0, ExpirationPeriod, 0);var cache =MethodResultCache.GetCache(args.Method, timeSpan);var arguments = args.Arguments.ToList(); var result =cache.GetCachedResult(arguments);if (result != null)
{
args.ReturnValue
=result;return;
}
else{base.OnInvoke(args);//调用后重新更新缓存 cache.CacheCallResult(args.ReturnValue, arguments);
}
}
}

这个CacheAttribute特性类包含一个设置失效的时间间隔(分钟),来指定函数返回结果的失效时间的,通过继承MethodInterceptionAspect基类,我们重写了void OnInvoke(MethodInterceptionArgs args)函数,从而对调用过程的横切面进行介入:

如果调用过程中获得缓存结果,则直接返回,不需要调用函数业务逻辑;否则调用函数获得返回值,并重新设置缓存结果值。

在函数代码里面,通过传入参数(包括方法对象、超时时间等)实现方法缓存对象的构建。

MethodResultCache.GetCache(args.Method, timeSpan);

在MethodResultCache里面,我们就是对方法的缓存进行处理的,首先需要声明一个MemoryCache的对象用于管理缓存(分布式缓存)。

        /// <summary>
        ///初始化缓存管理器/// </summary>
        private voidInitCacheManager()
{
_cache
= newMemoryCache(_methodName);
}

其中通过函数获取方法和参数的键,也就是唯一的键。

        /// <summary>
        ///根据调用方法名称和参数获取缓存键/// </summary>
        /// <param name="arguments">方法的参数列表</param>
        /// <returns></returns>
        private string GetCacheKey(IEnumerable<object>arguments)
{
var key = string.Format("{0}({1})", _methodName,string.Join(",", arguments.Select(x => x != null ? x.ToString() : "<Null>")));returnkey;
}

设置缓存的操作,我们就是调用MemoryCache缓存管理类来实现的键值设置的,如下代码所示。

        /// <summary>
        ///缓存结果内容/// </summary>
        /// <param name="result">待加入缓存的结果</param>
        /// <param name="arguments">方法的参数集合</param>
        public void CacheCallResult(object result, IEnumerable<object>arguments)
{
_cache.Set(GetCacheKey(arguments), result, DateTimeOffset.Now.Add(_expirationPeriod));
}

这样我们就设置了一个键值的缓存,并指定了缓存的失效时间,在这个时间段内,我们每次获取的数据,不需要再次调用外部接口,直接从缓存里面获取,速度提高很多,同时也减轻了分布式构架中的服务器承载的IO压力。

我们可以编写一小段代码进行测试出来的效率,如下代码所示。

            //First test
            DateTime start =DateTime.Now;var list = CacheService.GetSimpleUsers(1);int end = (int)DateTime.Now.Subtract(start).TotalMilliseconds;

Console.WriteLine(
"first:" +end);//Second test start =DateTime.Now;
list
= CacheService.GetSimpleUsers(2);
end
= (int)DateTime.Now.Subtract(start).TotalMilliseconds;
Console.WriteLine(
"Second:" + end);

获得的结果如下所示(分别介绍获得结果的时间)。

 first: 519Second:501first:0Second:0first:0Second:0

从上面代码可以看出,第一次请求数据的有一定的时间差,后面请求毫秒数则是直接0了。

通过上面的 PostSharp和MemoryCache的整合,我们可以极大简化了缓存的处理代码,并且能够利用较为不错的MemoryCache缓存管理类来实现缓存的处理,非常方便和高效了。