平时在开发过程中,通常都会涉及到数据的查看功能,比如查看订票记录,查看资料列表等。而查看这个动作,往往是跟权限有关的。
一种可能的做法是:页面上放一个GridView控件和一个SqlDataSource控件。然后在页面逻辑里加入相应操作的权限判断,然后再展现数据。或者将SqlDataSource换成ObjectDataSource,然后将权限判断封装在ObjectDataSource中。
这样做是可行的。但是存在一些问题:
为了解决上述问题,我们做了如下考虑:
-
将所有的业务逻辑操作视为一个动作,权限按动作,角色,用户分解成不同实体,存放在数据库。这样用户登陆后,可以根据当前用户身份求解到该用户对所有动作的所有权限。
-
将SqlDataSource进行扩展,让其支持“动作”属性,这样就可以在检索数据时,根据动作求解出当前用户需要的权限(即权限因子)。
-
将SqlDataSource进行扩展,让其在检索数据前,将权限因子替换掉Command中关于权限的占位符。
这样SqlDataSource检索出来的数据已经根据权限进行了过滤。
OK,思路就介绍到这里,下面详细介绍一下如何扩展SqlDataSource。
SqlDataSource作为一个服务端控件,和ObjectDataSource一样,继承于DataSourceControl,具有可扩展性。
关键问题
上述思路中,不管是添加权限因子还是动态构造Sql语句,都是在执行查询数据之前,更改SelectCommand的值。所以“
如何更改
SelectCommand
的值”是需要解决的一个问题。
另外,为了支持“权限因子”以及支持用来和SelectParameter中定义的Parameter结合,动态生成Sql的其他属性,需要
给
SqlDataSource
扩展一些属性。
为了提供可扩展性,先实现与权限相关的扩展,然后再实现与动态生成Sql语句相关的扩展
实现SecuritySqlDataSource
和定义服务器端控件和组件一样,首先定义一个继承于Control的类,因为SqlDataSource继承于DataSourceControl,而DataSourceControl继承于Control(并且实现了IDataSource和IListSource),所以改类可以直接继承于SqlDataSource。
public
class
SecuritySqlDataSource : System.Web.UI.WebControls.SqlDataSource
...
{
}
该控件需要一个SelectRefAction的属性(命名不重要)来支持“权限因子,所以在类中加入如下属性声明
protected
string
selectRefAction
=
null
;
/**/
///
/// SqlDataSource查询数据时,需要控制权限的动作权限名称
///
[Category(
"
Security
"
),
Description(
"
查询数据的权限动作名称
"
),
]
public
string
SelectRefAction
...
{
get ...{ return this.selectRefAction; }
set ...{ this.selectRefAction = value.Trim(); }
}
其中,Category Attribute是该属性在VisualStudio可视化设计器中显示的属性分类名称。Description Attrubute是该属性在设计器中对该属性的描述。
为了让可视化编辑器将SelectRefAction设置的值保存为SqlDataSouce的属性,那么需要在类声明中用元数据属性来指示页分析器如何保存属性的值
[ParseChildren(
true
)]
[PersistChildren(
true
)]
public
class
SecuritySqlDataSource : System.Web.UI.WebControls.SqlDataSource
用 ParseChildrenAttribute 类指示页分析器应如何处理页上声明的服务器控件标记中嵌套的内容。以元数据属性 (Attribute) ParseChildren(true) 标记服务器控件将指示分析器把包含在服务器控件标记内的元素解释为属性 (Property)。PersistChildrenAttribute属性指示设计时是否应将 ASP.NET 服务器控件的子控件作为嵌套内部控件保持。
编译后在页面放置一个SecuritySqlDataSource,刚才设置的属性在设计器中是这样效果
保存后在设计器在SecuritySqlDataSource的属性中保存该属性的值
SelectRefAction
="LISTORDER"。LISTORDER即执行该查看动作的定义
然后在SelectCommand中添加权限因子
SelectCommand
=
"
SELECT
[
au_id
]
,
[
au_lname
]
,
[
au_fname
]
,
[
phone
]
,
[
address
]
,
[
city
]
,
[
state
]
,
[
zip
]
,
[
contract
]
FROM
[
authors
]
WHERE
{LISTORDER}
其中where子句LISTORDER即SelectRefAction的取值。
到这里为止,关于权限的作用就通过这些声明性语句描述完了,那么怎样生成真正的权限因子呢?也就是说,如何来替换{LISTORDER}呢?
可能按照惯性思维,认为在SqlDataSource的SqlDataSource_Selecting事件中更改SelectCommand的值来更改就可以了,但是实际上是不正确的。这是因为虽然该事件在检索数据之前触发,但是在该事件中修改后的SelectCommand的值并不会被用来检索数据。因为在该事件触发之前,SelectCommand和SelectParameter已经被用来生成DBCommand,而事件触发之后,更新过的SelectCommand并不用来生成新的DBCommand。所以必须扩展SqlDataSource(真正的情况是,扩展SqlDataSourceView),使其在生成DBcommand之前就更新SelectCommand。这样就解决了如何有效修改SelectCommand的问题。同时,可以也看出:扩展一个组件,了解其本身的时序是很重要的。
详细情况如下:
通过阅读MSDN中自定义DataSourceControl的文章和反编译SqlDataSource的代码,可以发现SqlDataSource_Selecting事件不是由SqlDataSource对象触发的。
public event
SqlDataSourceSelectingEventHandler Selecting
...
{
add...{ this.GetView().Selecting += value; }
remove ...{ this.GetView().Selecting -= value; }
}
然后再根据GetView()方法的定义,可以找到一个和SqlDataSource相关紧密的类SqlDataSourceView类,原来Selecting事件是在SqlDataSourceView的ExcuteSelect方法中触发出来的。
protected override
IEnumerable ExecuteSelect(DataSourceSelectArguments arguments)
...
{
//通过SelectParameter构造DBCommand
DbCommand command1 = this._owner.CreateCommand(this.SelectCommand, connection1);
this.InitializeParameters(command1, this.SelectParameters, null);
command1.CommandType = SqlDataSourceView.GetCommandType(this.SelectCommandType);
//触发Selecting事件
SqlDataSourceSelectingEventArgs args2 = new SqlDataSourceSelectingEventArgs(command1, arguments);
this.OnSelecting(args2);
if (args2.Cancel)
...{
return null;
}
//执行查询
}
所以,更改SelectCommand的正确位置是在SqlDataSourceView执行ExcuteSelect方法之前。
因此,需要一个类来继承SqlDataSourceView
public class SecuritySqlDataSourceView : System.Web.UI.WebControls.SqlDataSourceView
其中获得权限上下文以及获得该权限动作对应的规则由其他类完成,执行结果返回当前用户执行当前动作,比如ListOrder动作,需要的规则。然后替换SelectCommand中的关于权限的占位符。
那么,
SecuritySqlDataSource
类和SecuritySqlDataSourceView类如何关联起来呢?
通过反编译SqlDataSource的代码,找到最佳的位置是在CreateDataSourceView方法中。在这里创建新的SecuritySqlDataSourceView实例,并被SecuritySqlDataSource作为局部变量保存起来,然后通过GetView方法获得。
protected
override
System.Web.UI.WebControls.SqlDataSourceView CreateDataSourceView(
string
viewName)
...
{
return new SecuritySqlDataSourceView((System.Web.UI.WebControls.SqlDataSource)this, viewName, HttpContext.Current);
}
到现在为止,一个支持安全上下文的SqlDataSource就完成了。
在页面上使用该控件时,只需要指定前述属性,避免了在每个页面逻辑中求解安全上下文并拼凑Sql的麻烦。使代码更简洁。
实现支持复杂检索的SqlDataSource
通常,业务逻辑并不只是根据当前用户的身份检索出所有有权限查看的数据,因为数据量太大,不方便查看,所以通常都会缩小检索的范围,也就是对检索条件加以限制,使结果更精确。
例如,在Pubs数据库中检索作者信息
如果不仅需要在输入条件为空时检索出所有记录,而且在输入条件至少一个不为空时能够根据输入的值检索符合条件的记录,那么仅仅通过在页面声明SelectCommand,然后声明一些SelectParameter参数和控件进行绑定时做不到的。因为在这种情况下,SelectCommand已经不是一成不变的了。
那么,我们可能会放弃使用SqlDataSource,而拼凑Sql语句。
但是,拼凑Sql通常是比较繁琐的,而且在不同场景下拼凑的Sql的代码很难复用。因此扩展SqlDataSource使其能够支持这种需要,是很实用的。
那么怎样扩展才能够支持到这种需要呢?
首先,需要能够动态生成SelectCommand,而这个难点已经在SecuritySqlDataSource已经解决了。
然后,考虑到SelectParameter集合已经能够支持6种来源的Parameter(ControlParameter,CookieParameter,FormParameter,QueryStringParameter,ProfileParameter,SessionParameter)能够对不能来源的Paramter提供很好的支持,而且ControlParameter对丰富的页面控件提供很好的支持,能够避免我们通过别的解决方案解决这个问题时碰到的难题(如对TextBox控件的取值和DropDownList控件的取值是不同的),所以决定把这些参数利用起来,并且给这些已有的参数类型提供一些属性。例如:给它添加一个Pattern属性,来描述该参数的值将在SelectCommand种被Format成什么子句,来代替SelectCommand中State=@State这样的子句,简单的,Pattern值类似这样:State={0}.另外,考虑到经常碰到的动态构造子句的问题,如Title为空时,生成1=1,Title不为空时,生成
au_id in (select ta.au_id from titleauthor ta where ta.title_id in (select title_id from titles where {0}))其中{0}将被Pattern的值(title={0})代替.分析这种情况,我们引入Group属性,用来将每个子查询分组。也就是每个查询参数一定属于每个组。然后在每组的查询子句构造完成后抛出一个事件
SearchConditionBuilded,在事件中来构造上面的语句,这样解决了动态构造SelectCommand的问题。
再对上面的解决办法进一步优化,我们把Group属性抽取出来,变成节点,这样就不用在每个查询参数都设置Group属性,而把属于该Group组的查询参数都置于该节点之下。同时,把原来在
SearchConditionBuilded
事件中处理的给子查询做处理的语句抽取出来,变成一个属性,也叫Pattern。在上面举的场景中,Pattern的值就是au_id in (select ta.au_id from titleauthor ta where ta.title_id in (select title_id from titles where {0})),然后给每个组设置一个DefaultCondition,表示这个子查询中如果所有的查询参数都为空时,该子句生成的缺省条件.同时为了区别个组,给组增加一个属性Name.
所以,最后的结果是类似这样
到这里为止,前面提出的问题都已经得到了解决.
下面介绍代码如果实现
首先建立SqlDataSource类,使之继承SecuritySqlDataSource.
public
class
SqlDataSource : SecuritySqlDataSource
再建立SqlDataSourceView,使之继承SecuritySqlDataSourceView
SqlDataSourceView : SecuritySqlDataSourceView
因为需要建立一个Pattern节点,所以在SqlDataSource中增加一个属性Pattern
private
SearchGroups patterns
=
new
SearchGroups();
[Category(
"
Search
"
),
Description(
"
查询模式
"
),
PersistenceMode(PersistenceMode.InnerDefaultProperty),
DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public
SearchGroups Patterns
...
{
get ...{ return this.patterns; }
set ...{ this.patterns = value; }
}
至于每组里的查询参数节点怎样实现,以及组节点的属性如果增加进来,由于属性较多,这里不一一解释,详细实现请参看代码
扩展中遇到的问题:
-
如果自定义ServerControl的类在App_Code文件夹下,则注册时不能使用Assembly,否则会出错.这是因为App_Code文件夹编译后生成的Assembly命名是随机的.正确的做法是将类单独放在一个工程里,然后引用.或者注册时使用namespace注册,不要使用Assembly
-
自定义SeverControl在注册时不要使用强命名,这样会使该ServerControl中的复杂节点(即包含子节点的节点)注册不正确(原因不明).可行的做法是:使用Assembly的名称即可,去掉强命名
-
在SqlDataSource中,如果在其定义的事件(比如Selecting,Selected)中,更改SelectCommand是无效的.这是因为真正用来取数据的DBCommand在这些事件之前已经构造完成了,而更改后的SelectCommand不会用来重新生成DBCommand.
-
如果这定义的ServerControl包含集合属性,那么需要扩展该集合属性的编辑器,并在集合属性上指明该编辑器Editor(typeof(EditorName)),否则编辑完成后,编辑的值不会被持久化.并且扩展的编辑器完成后,如果更改了集合的字段,那么需要重新启动IDE,设计器才会正确显示修改会的字段.
-
在SqlDataSource中ParameterCollection是如何求解的呢?ParameterCollection有个GetValues方法,可以求解出所有Parameter的值,结果是IOrderedDictionary类型.求解出来的结果用来构造DBCommand.