2023年2月

在我们一般的程序中,为一般的界面元素添加一定的图标展示,有助于提升界面的整体的美观。结合排版布局,以及固定场景的图标,往往给用户非常好的直观感受:统一、美观、易理解。因此在一般的程序界面中,都尽量在略显单调的界面增加一些图标进行点缀,能够达到较好的整体效果。本文介绍在Winform程序界面中,可以使用自己的图标库,也可以引入DevExpress程序中使用内置的图标,从而实现给开发的程序润色。

1、使用自己的图标库

不管在Web还是在WInform项目里面,开发者积累一定的图标库,包括256,128,64像素等大型的图标,或者常规32,16像素规格的图标都是比较不错的,有时候可以选择符合主题和场景的图标,给开发的程序增添更美丽的元素。

不过即使自己存储不够,也不要紧,可以通过网站搜索一些符合要求的图标,网站上也有很多免费的图标可以下载,如下面几个网址:

http://www.iconfinder.com/

http://www.iconpng.com/

http://findicons.com/

http://www.iconseeker.com/

http://mricons.com/

http://www.iconarchive.com/

从中总能找到自己所需要主题的图标的。

如下面是我WInform开发框架里面的程序主界面风格,使用了很多自己的图标。

2、使用DevExpress内置的图标库

在一些常规的界面里面,如编辑界面和查询界面里面的按钮,我倾向于使用内置的DevExpress图标,这样程序看起来比较统一,例如我在界面设计的基类窗体里面,图标应用效果如下所示。

添加图标方式非常简单,在按钮的右上角小图标上单击一下进入编辑界面,如下所示。

然后选择Image按钮,进入图标选择界面,选择内置的DevExpress图标库即可,基本上,只要是DevExpress的原生控件,那么就可以通过这种内置图标的对话框进行图标选择,非常方便。

大图标选择32*32的,小图标则可以选择16*16的尺寸。

这样我们设计一些常规的列表界面操作按钮的时候,可以选择同一的图标,形成较为标准的图标。

以上就是我WInform开发框架、混合式开发框架里面,WInform界面的的图表设计,当然很多界面如树列表、右键菜单、工具条等都可以采用一定的图标,把界面装饰的更加美观大方,易于理解。

在很多情况下,我们需要通过树列表进行数据的展示,如一些有层次关系的数据,通过有层级的展示,能够使用户更加直观查看和管理相关的数据。在一般Winform开发的情况下,可以使用微软的TreeView控件,也可以使用DevExpress的TreeList控件进行数据的展示,本篇随笔主要介绍基于DevExpress的TreeList控件使用以及使用SearchControl对节点进行查询的操作。

1、 使用微软的TreeView控件的实现效果和思路

在很多情况下,我们也倾向于使用TreeView控件作为数据的展示,相对于TreeList控件,这种控件的处理,需要自己管理树节点的层次关系,不过使用也比较简单,呈现的效果两者都差别不大。

如在我开发框架中,在字典管理模块里面,就是采用这个控件进行数据的展示的,整体效果也还不错。

在树形列表里面,我们获取数据后,统一根据层级的关系构建树节点即可,如下代码所示。

/// <summary>
///初始化树信息/// </summary>
private voidInitTreeView()
{
this.treeView1.Nodes.Clear();this.treeView1.BeginUpdate();
List
<DictTypeNodeInfo> typeNodeList = BLLFactory<DictType>.Instance.GetTree();foreach (DictTypeNodeInfo info intypeNodeList)
{
AddTree(
null, info);
}
this.treeView1.EndUpdate();this.treeView1.ExpandAll();
}
/// <summary> ///根据节点数据,递归构建该层级以下的树节点/// </summary> /// <param name="pNode">父树节点</param> /// <param name="info">字典类型数据</param> private voidAddTree(TreeNode pNode, DictTypeNodeInfo info)
{
TreeNode node
= null;if (info.PID == "-1")
{
node
= new TreeNode(info.Name, 0, 0);
node.Tag
=info.ID;this.treeView1.Nodes.Add(node);
}
else{
node
= new TreeNode(info.Name, 1, 1);
node.Tag
=info.ID;
pNode.Nodes.Add(node);
}
foreach (DictTypeNodeInfo subInfo ininfo.Children)
{
AddTree(node, subInfo);
}
}

还有我们在鼠标选择某个节点的时候,触发AfterSelect事件,这样我们就可以处理鼠标节点的事件了

/// <summary>
///单击节点事件处理/// </summary>
private void treeView1_AfterSelect(objectsender, TreeViewEventArgs e)
{
if (e.Node.Tag != null)
{
this.lblDictType.Text =e.Node.Text;this.lblDictType.Tag =e.Node.Tag;

BindData();
}
}

以上就是使用TreeView控件来处理数据的展示,从上面代码可以看到,整体的内容,主要是通过递归的关系来构建TreeNode的处理,但是使用的代码也不算复杂,因此大多数可以采用这种方式来自定义树形节点的展示。

2、使用DevExpress的TreeList控件的效果和实现代码

而使用DevExpress的TeeList控件,可以通过KeyFieldName和ParentFieldName指定他们的层级关系,使用就更加简单化,提供的数据源会自动进行层次的关系处理,非常方便。

我们先来看看通过DevExpress的TreeList控件展示的字典类型和字典数据的界面效果。

这里面的效果是如何通过代码实现的呢?

首先我们使用代码获取字典类型数据并进行树列表控件的初始化操作,如下所示。

//添加显示列
this.tree.Columns.Add(new TreeListColumn{ FieldName= "Name", Caption= "字典类型名称", Width=160, VisibleIndex =0});//设置树控件的层次关系及属性
tree.KeyFieldName = "ID";
tree.ParentFieldName
= "PID";this.tree.OptionsBehavior.Editable = false;this.tree.OptionsView.EnableAppearanceOddRow = true;this.tree.OptionsView.EnableAppearanceEvenRow = true;

上面的代码我们还可以通过扩展函数对树列表的处理进行封装,已达到简化代码的目的,如下是处理后的实现代码:

//控件扩展函数封装处理
this.tree.CreateColumn("Name", "字典类型名称", 160, true);this.tree.InitTree("ID", "PID", "-1", false, false);

通过添加TreeListColumn对象给TreeList控件就可以实现字段列的显示了,同时指定数据源里面的KeyFieldName和ParentFieldName来设定层级关系即可,非常简单。

而绑定数据源,则可以通过一个函数进行处理,如下所示。

/// <summary>
///绑定树的数据源/// </summary>
private voidBindTree()
{
this.tree.DataSource = BLLFactory<DictType>.Instance.GetAll();this.tree.ExpandAll();
}

从上面代码我们可以看到,我们返回的数据源,不需要在实体类对象层级具有上下级的关系,如通过TreeView实现的时候,我们使用了DictTypeNodeInfo 对象是具有上下层级关系的。

这里只需要使用普通的DictTypeInfo 对象集合即可,通过KeyFieldName和ParentFieldName来设定层级关系即可。

为了指定树形节点的图标,我们可以通过代码进行自定义图标的处理,如下代码所示,这样每个层级的图标都不一样,自动实现获取设置的处理。

//设置树的图标集合及逐级图标
this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e)=>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};

实现树节点选中的事件处理,则需要实现FocusedNodeChanged事件即可。

    //初始化树节点选择事件
    this.tree.FocusedNodeChanged += delegate(objectsender, FocusedNodeChangedEventArgs e)
{
this.FocusedNodeChanged();
};
}
private voidFocusedNodeChanged()
{
if (this.tree.FocusedNode != null)
{
var PID = string.Concat(this.tree.FocusedNode.GetValue("ID"));
treeConditionSql
= string.Format("DictType_ID = '{0}'", PID);
}
else{
treeConditionSql
= "";
}
BindData();
}

最后初始化树列表的代码如下所示。

private voidInitTree()
{
this.tree.Columns.Clear();//控件扩展函数封装处理 this.tree.CreateColumn("Name", "字典类型名称", 160, true);this.tree.InitTree("ID", "PID", "-1", false, false);//设置树的图标集合及逐级图标 this.tree.SelectImageList = this.imageCollection1;this.tree.CustomDrawNodeImages += (object sender, CustomDrawNodeImagesEventArgs e)=>{int maxCount = this.imageCollection1.Images.Count;var index = e.Node.Level < maxCount ? e.Node.Level : 0;
e.SelectImageIndex
=index;
};
}

3、基于SearchControl控件对节点进行查询的操作

上面的处理就是树列表的一般性展示,如果需要在树节点上面增加一个查询过滤的操作,那么可以使用SearchControl控件进行过滤处理,只需要设置SearchControl控件的Client属性,以及实现树控件的FilterNode事件即可。

/// <summary>
///实现树节点的过滤查询/// </summary>
private voidInitSearchControl()
{
this.searchControl1.Client = this.tree;this.tree.FilterNode += (object sender, DevExpress.XtraTreeList.FilterNodeEventArgs e) =>{if (tree.DataSource == null)return;string nodeText = e.Node.GetDisplayText("Name");//参数填写FieldName if (string.IsNullOrWhiteSpace(nodeText))return;bool isExist = nodeText.IndexOf(searchControl1.Text, StringComparison.OrdinalIgnoreCase) >= 0;if(isExist)
{
var node =e.Node.ParentNode;while (node != null)
{
if (!node.Visible)
{
node.Visible
= true;
node
=node.ParentNode;
}
else break;
}
}
e.Node.Visible
=isExist;
e.Handled
= true;
};
}

实现效果如下所示, 对于符合记录的查询,那么会有高亮的颜色进行重点标注。

一般情况下,我们都倾向于使用一个组织比较好的独立界面来录入或者展示相关的数据,这样处理比较规范,也方便显示比较复杂的数据。不过在一些情况下,我们也可能需要直接在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();
}

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