其中,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 然后重写ExcuteSelect方法: Override Method #region Override Method protected override System.Collections.IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) ...{ //替换查询动作安全上下文 ProcessSelectCommand(); return base.ExecuteSelect(arguments); } #endregion Virtual Method #region Virtual Method /**//// <summary> /// 如果用户没有此权限动作的授权,则系统会抛出异常 /// </summary> protected virtual void ProcessSelectCommand() ...{ // 如果设置了查询数据的动作权限名称 if (!string.IsNullOrEmpty(this.owner.SelectRefAction)) ...{ // 获得权限上下文 PlatSecurityContext security = (PlatSecurityContext)PlatSecurityContext.Current; // 获得该权限动作对应的规则 string rule = security.GetRuleByRefAction(this.owner.SelectRefAction); this.SelectCommand = this.SelectCommand .Replace(string.Format("{{{0}}}",this.owner.SelectRefAction), rule); } } #endregion 其中获得权限上下文以及获得该权限动作对应的规则由其他类完成,执行结果返回当前用户执行当前动作,比如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 Override CreateDataSourceView 方法,使SqlDataSource类和SqlDataSourceView类关联起来 protected override System.Web.UI.WebControls.SqlDataSourceView CreateDataSourceView( string viewName) ... { SqlDataSourceView sqlDataSourceView = new SqlDataSourceView((System.Web.UI.WebControls.SqlDataSource)this, viewName, HttpContext.Current); return sqlDataSourceView; } 因为需要建立一个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; } } 而Pattern属性是一个复杂节点,Pattern的属性需要被持久化成子节点,所以SqlDataSource类需要增加这两个属性 [ParseChildren( true )] [PersistChildren( false )] public class SqlDataSource : SecuritySqlDataSource 上面使用的SearchGroup中有一个重要的属性,它用来表示Pattern包含的组 [ParseChildren( true , " Groups " )] [TypeConverter( typeof (ExpandableObjectConverter))] public class SearchGroups ... { Private Property#region Private Property private ArrayList groups; #endregion Designer Exposed InnerProperty#region Designer Exposed InnerProperty [ Category("Behavior"), Description("查询组"), NotifyParentProperty(true), PersistenceMode(PersistenceMode.InnerDefaultProperty), Editor(typeof(SearchGroupEditor), typeof(UITypeEditor)), DesignerSerializationVisibility(DesignerSerializationVisibility.Content) ] public ArrayList Groups ...{ get ...{ if (groups == null) ...{ groups = new ArrayList(); } return groups; } } #endregion}
至于每组里的查询参数节点怎样实现,以及组节点的属性如果增加进来,由于属性较多,这里不一一解释,详细实现请参看代码