让菜单充满活力:ASP.NET根据角色动态分配菜单+权限

   这次做图书馆维护系统,首先要解决的问题就是角色权限动态分配,权限分配直接体现就是菜单的动态分配。在此和大家分享一下心得。

   大多数系统,都有多种类型的用户,不同的用户权限不同,某一个功能,A类用户是可见的,但是B类用户没有必要或者不应该看见这个功能,这就要涉及到功能的动态分配。要解决这个问题,当然要从数据下手,在学姐的指导下,有了如下的UML设计图:

 

解释一下:

MemberType表是用户类型表。

SystemFunction表是系统所有功能表,记录了功能的名称和对应的页面URL,思想是一个功能即一个页面。

Tab表是菜单表,也就是顶级菜单,SystemFunction表中的功能将被归类到这个菜单中。

MemberFunction表是用户功能表,这个表负责连接MemberType表和Tab表,通过这个表可以得知何种用户有哪些菜单。

TabFunction是菜单功能表,负责连接Tab表和SystemFunction表,通过这个表可以得知何种菜单有哪些功能。

   这种设计遵守了三范式设计原则,使用起来非常方便。假如我们要给某种类型的用户增加一种菜单(增加一种权限),只需要在MemberFunction表中建立一个连接即可:添加一条记录,字段值分别是该类型用户的id和对应菜单的ID。给某个菜单添加某个功能也是如此。这样一来,管理起来非常方便,只需要添加或删除MemberFunction表和TabFunction表中的记录,就可以达到灵活分配用户拥有的菜单、灵活分配菜单中的功能。

   结合ASP.NET,我们还需要把这种数据库表示转换成界面表示。在D层,必须借助于下边两个存储过程:

 

GO
/*-----------------------------用户身份(类型)对应顶级菜单表存储过程-------------------------------*/
/*选取某种用户顶级菜单*/
CREATE PROCEDURE proc_MemberFunction_SelectByTypeID
	@memberTypeID bigint
AS
BEGIN
	select t_MemberFunction.*,t_Tab.[name] from t_MemberFunction 
	join t_Tab on t_Tab.id=t_MemberFunction.tabID
	where memberTypeID=@memberTypeID
END

 

GO
/*-----------------------------顶级菜单(选项卡)功能表存储过程-------------------------------*/
/*选取某种顶级菜单的下属功能*/
CREATE PROCEDURE proc_TabFunction_SelectTabFunction
	@tabID bigint
AS
BEGIN
	select t_TabFunction.*,t_SystemFunction.[name],t_SystemFunction.pageURL from t_TabFunction
	join t_SystemFunction on t_SystemFunction.id=t_TabFunction.systemFunctionID
	where
	tabID=@tabID
END

 

   有了这两个存储过程,就可以读出所有的菜单数据。接下来就要在界面上显示,一般情况下,界面上的菜单都是用ul和li标签,然后用javascript加以控制,在这一级菜单就可以满足我们的需求,类似下边这个结构:

<ul>
	<li class="menu">
		<a href="#">个人管理</a>
		<ul>
			<li><a href='#'>查看信息</a></li>    
		</ul>
		<ul>
			<li><a href='#'>修改密码</a></li>    
		</ul>
	</li>
</ul>

   不难看出,个人管理的位置就是顶级菜单,查看信息、修改密码的位置是具体功能,很明显的一个嵌套结构(把上边的代码保存成html文件,打开看看就知道是啥样的结构了)。在界面上绑定数据,轻量级的repeater控件是非常不错的选择,具体怎么用就不赘述了。要用repeater控件显示出上边提到的结构,就必须进行repeater控件的嵌套。那么如何在ASP.NET中嵌套repeater控件呢?注:以下代码都是针对于本文的数据库,如果您想用,要改一改,起码要改改读取的字段。。。

aspx前台文件代码:

<ul>
            <!--读取顶级菜单-->
            <asp:Repeater ID="menuRepeater" runat="server" 
                onitemdatabound="menuRepeater_ItemDataBound">
                <ItemTemplate>
                    <li class="menu">
        	            <a href="#"><%# Eval("name") %></a>
                        <ul>
                            <!--读取二级菜单-->
                            <asp:Repeater ID="functionRepeater" runat="server">
                                <ItemTemplate>
                                    <li><a href="#" onclick='javascript:changeSrc("<%# setSession(Eval("pageURL").ToString) %
>");'><%# Eval("name") %></a></li>
                                </ItemTemplate>
                            </asp:Repeater>  	            
                        </ul>
                    </li>
                </ItemTemplate>
            </asp:Repeater>
</ul>

 

aspx.cs后台文件代码:

//外层repeater数据绑定
DataTable dt = new DataTable();
dt = menumanager.getMemberFunction(Convert.ToInt64(Request.QueryString["memberTypeID"].ToString()));
menuRepeater.DataSource = dt;
menuRepeater.DataBind();
//内层repeater数据绑定
protected void menuRepeater_ItemDataBound(object sender, System.Web.UI.WebControls.RepeaterItemEventArgs e)
{
    DataTable dt = new DataTable();
    Repeater functionRepeater = (Repeater)e.Item.FindControl("functionRepeater"); //找到内层的repeater控件
    DataRowView rowv = (DataRowView)e.Item.DataItem;
    dt = menumanager.getTabFunction(Convert.ToInt64(rowv["tabID"]));  //读取上一层repeater控件中保存的菜单id,并且根据该id去读取菜单下的功能
    //绑定数据
    functionRepeater.DataSource = dt; 
    functionRepeater.DataBind();
}

 

   repeater嵌套就是这么简单,需要注意的是,在外层repeater上注册的是onitemdatabound事件,也就是itemtemplate模版数据绑定事件,千万不要理解成是repeater的绑定事件。然后在用onitemdatabound注册的menuRepeater_ItemDataBound事件中,去绑定内层repeater控件的数据就可以了。

   细心的读者可能会发现在aspx前台代码中调用了一个setSession函数,这个函数就是就是分配权限用的。函数内容:

 

public string setSession(string pageName) 
{
    Session["PagePermissions"] = Session["PagePermissions"].ToString() + "|" + pageName.Split(new char[1]{'.'})[0];
    return pageName;
}

 

   这么简单的一个小函数,是如何做到分配权限的呢?地球人都知道,即使我们没有给X类型的用户显示某个功能页面,但这个页面是确确实实存在的,只是没有让X看到而已,假如X用户手动访问这个页面,如果显示出来了,不就乱了吗?通过这个函数我们可以获取所有的页面名称,把他们拼接成一个字符串,保存到session中,然后在每个页面的pageLoad事件中都检查这个session,看看这个session中有没有自己的名称,如果没有,就跳转到错误页面,如果有,就显示。这样一来,菜单分配和权限分配就一块搞定了,方便简洁!

   至此,一个ASP.NET根据角色动态分配菜单+权限的例子就讲完了,但是做完这个工程之后我发现这样还不是很好,经过仔细的分析,这样的数据库设计可以用下边这张图表示:

 

 


 

 

   可以看出,假如我们要增加一级菜单,就要额外增加两个表:一个菜单表一个连接表。这在实际应用中并不合理。用过wordpress的朋友都知道,它的菜单可以通过拖拉的方式进行排布,假如是上边这种结构,要来来回回的去删表、建表,这几乎是不敢想象的工作量。经过思考,无论是几级菜单,都放到一个表中(抽象成一个表),然后把所有的连接表也抽象成为一个表,有了如下结构:



 

其中表内的结构如下(表名:字段名1,字段名2…):

系统菜单(t_SystemMenu):id,level,menuName

菜单衔接(t_MenuLink):id,menuID,belongToID

数据库中读取菜单语句:

遍历分级(确定共有多少级):

SELECT * FROM t_SystemMenu WHERE level=@level

选出下属菜单(选出每一级下属的菜单或功能):

SELECT * FROM t_MenuLink WHERE belongToID=@id

   这样设计在数据库读取方面没有问题,但是界面显示就比较困难了,因为我们无法确定repeater控件的数量,有兴趣的可以google搜“动态创建repeater控件”,由于这种技术与平台有很大关系,在此我只抛砖引玉,具体的就留给读者思考了。

   PS:这种变态的设计很可能不符合数据库设计三范式,具体情况具体分析吧,有时候没必要迷信于什么范式!关系型数据库有时候还不好使呢!

posted @ 2012-05-20 19:06  杨元  阅读(1362)  评论(0编辑  收藏  举报