65.9K
CodeProject 正在变化。 阅读更多。
Home

灵活的框架,用于将项目数量 [Count] 附加到菜单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (4投票s)

2014 年 7 月 20 日

CPOL

10分钟阅读

viewsIcon

14666

downloadIcon

570

本文将演示如何开发通用的框架来创建类似 Outlook 的菜单项自动刷新计数。

介绍 

在最近的开发活动中,我们遇到了一个RFC,客户要求在每个菜单项旁边显示计数,并且几乎是实时更新。内部讨论促使我们建立某种框架,以便轻松快速地管理更改,即:

  • 将计数附加到新菜单不应需要更改所有应用程序层
  • 从菜单中分离计数不应需要任何努力
  • 此区域的错误修复不应需要完整的发布
  • 计数逻辑的更改应在不修改任何现有层的情况下独立完成

我们搜索过谷歌,但没有找到太多帮助,这导致我们决定构建自己的框架,并通过撰写本文来帮助其他人,以防他们希望开发类似的功能。

解决方案

基于上述要求,我们草拟了一个可以完成此任务的框架。伪解决方案可能是:

  1. 应用程序应每隔n分钟向服务器发出AJAX请求
  2. 服务器应返回某种XML,其中包含每个菜单项的数字
  3. 应用程序通过迭代节点处理xml。如果它找到一个具有计数值的节点,它将更新树节点

如果我们仔细研究上述方法,我们可以观察到解决方案有三个层次。让我们剖析每个层次。

数据库层

我们认为数据库应该掌握这个框架的关键。由于标准业务应用程序菜单大多从权限表渲染,其中存储了每个用户的权限,并且在登录时加载用户的权限并动态通过菜单渲染。所以我们看到了这种结构的明显变化,如果我们可以以某种方式将一些代码附加到每个权限/菜单,我们的问题将得到解决。

假设我们有以下存储权限的表结构

FunctionID FunctionParentID MenuDisplayText
1 NULL Area
2 1 模块
3 2 Functon

 

   

 

在数据库层,运行的最细粒度的代码是函数。如果我们将一个函数附加到每个菜单,那么我们的大部分问题都解决了。SQL函数是隔离的独立组件,具有高度可维护性,因此我们决定使用函数。修改后的表结构将如下所示:

FunctionID FunctionParentID MenuDisplayText CountFunction
1 NULL Area Dbo.get_GetAreaCount({0}, {1}, {2})
2 1 模块 Dbo.get_GetModuleCount({0}, {1}, {2})
3 2 函数 Dbo.get_GetFunctionCount({0}, {1}, {2})

这给我们带来了巨大的好处,以满足我们的要求。现在我们可以:

  • 为每个项目编写不同的业务逻辑来计算计数
  • 隔离错误,因为我们知道它必须是计算计数的函数
  • 错误修复很容易,我们只需要修补函数并仅部署该函数
  • 同样,每个单独菜单上的更改也很容易实现
  • 性能不会受到影响,因为我们使用的是编译性质的SQL函数
  • 从菜单项中分离计数现在是小菜一碟
  • 现在将计数附加到新菜单变得非常容易,因为我们只需要创建新函数并将其与菜单关联。此任务甚至可以在生产站点完成。

这些函数的参数至关重要,因为这些参数将作为这些函数的上下文。它们将帮助它们决定它们是从哪里调用的,它们是如何调用的,以及/或者它们是否需要任何特殊逻辑。例如,LoginId在这里可能很棘手,因为计数可能取决于单个用户的业务逻辑。如果一个函数被决定为所有菜单项的银弹,那么在这种情况下,函数ID将是函数决定它从哪里调用的关键参数。

我们现在只需要一个机制来加载单个用户的菜单项,执行与菜单关联的所有函数,将结果存储在XML中并传递给应用程序,我们就大功告成了。

XML

我们选择以XML格式返回结果,而不是表格格式。原因是与其它格式相比,XML能更好地表示分层结构(例如菜单)。其次,在客户端迭代XML会很方便,因为它几乎表示相同的树结构。我们决定在数据库层生成XML,因为SQL Server对XML有良好的支持,但这只是一个选择,也可以在业务层生成相同的XML。

<r>
   <n t="0_1" v="24">
      <n t="0_2" v="58">
         <n t="0_3" v="74"/>
     </n>
  </n>
</r>

如果我们仔细观察XML,可以看到为减少XML大小所做的努力。这样做是为了减少每N分钟从服务器流向浏览器的数据大小。这里的n表示一个菜单节点。一个内部的n表示菜单中的菜单,以此类推。t表示菜单键,将用于精确识别树菜单节点。v表示将与菜单树节点关联的值。空v表示没有值,自然意味着不应更新。

动态SQL

我们认为将SQL函数附加到单个菜单项是一个优雅的想法,因为它提供了巨大的维护灵活性,修改、新增、删除、错误修复、业务更改都可以非常快速地完成,而不会影响整个系统。但这里的挑战是如何开发函数执行机制,而不会造成显著的性能影响。一个想法是编写一些迭代逻辑,依次执行函数并将结果存储在某个地方。我们通过消除迭代并构建如下所示的动态SQL改进了这一想法:

SELECT <HardcodeValue1>, <HardcodedValue2>, dbo.get_xxxx(HardcodeValue1, HardcodeValue2,..)

UNION ALL

SELECT <HardcodeValue1>, <HardcodedValue2>, dbo.get_xxxx(HardcodeValue1, HardcodeValue2,..)

....

上述方法将给我们一个只执行一次并以表格结构返回结果的SQL语句。现在这个表格结构为每个菜单项提供了数字。一个简单的临时表现在可以保存这些值,如下所示。这个临时表现在非常有用,通过它可以一次性更新每个菜单项的计数值,即不需要任何迭代循环。

分层XML生成

是的,SQL Server 拥有强大的原生 XML 支持。但在我们的案例中,存在一个挑战。由于我们的菜单结构是分层的,并且我们所说的 XML 本身也是嵌套的,因此我们需要一种机制来在 XML 中包含嵌套元素,而 SQL SERVER 不直接支持这一点。这要求我们专门为此工作构建一个函数。我们设计了以下机制来在 XML 中生成嵌套元素:

SELECT 
   isnull(Unique_id,'') as 't',
   isnull(n.value,'') as 'v',
   CASE WHEN IsNull(ParentFunctionId,0)=@FunctionId 
        THEN dbo.DocGet_XMLNode(@MENUTBL, FunctionId) 
   END
FROM     @MENUTBL as n 
WHERE IsNull(ParentFunctionId,0)=@FunctionId
FOR XML AUTO, TYPE

业务层

所提供的示例不需要独立的业务层。由于我们在示例中使用了 ASP.NET MVC,因此控制器可以被视为业务层。此层现在的职责是调用数据库组件,获取所需的 XML 并将结果返回给应用程序。所有这些都是使用控制器操作完成的。同样,为了说明目的,数据库访问使用了简单的 ADO.NET。我们想在这里提及的一件事是,此层也可以用于生成所需的 XML(目前是在数据库层完成的)。这现在是一个选择问题。我们使用数据库是因为它对 XML 的强大支持。SELECT FROM FOR XML AUTO 是一种强大的方法,可以快速生成 XML,由于机制已经存在,因此我们没有重新设计它。

表示层

ASP.NET MVC 在构建出色的 Web 应用程序方面表现出色,它与 jQuery 紧密集成并轻松支持第三方工具。我们使用了 DEVEXPRESS 树形控件 MVC 扩展来生成菜单。jQuery 用于 AJAX 调用和客户端树形更新。

使用代码

如上所述,整个框架的关键在于数据库层。`DocGet_MenuXML` 存储过程是核心,因为它包含所有菜单结构,为执行关联函数构建动态 SQL,并最终以所需格式生成 XML。现在让我们剖析 `DocGet_MenuXML` 存储过程。

DocGet_MenuXML

存储过程以声明变量开始。其中一个重要变量是 ` @MENUTBL `,因为此表将保存要返回的所需菜单结构以及存储与菜单项关联的数字的值字段。我们需要记住,菜单结构取决于用户角色和权限。因此,有必要只获取用户有权限的菜单项。此外,随着时间的推移,菜单项也会过时并变为非活动状态。我们还需要过滤掉这些。这个 ` @MENUTBL ` 完成所有这些工作。

SP 的以下摘录显示了所有活动函数是如何检索并存储在 ` @MNUTBL ` 中的。

WITH MENUHIRARCHY (FunctionParentId, FunctionId, MenuDisplayText, Level, countfunction)
AS
(
    SELECT e.functionparentid, e.FunctionId, e.MenuDisplayText, e.Level, e.countfunction
    FROM  SEC_FUNCTION AS e
    WHERE 1=1
    and     e.functionparentid is null
    UNION ALL
    SELECT e.functionparentid, e.FunctionId, e.MenuDisplayText,e.Level, e.countfunction
    FROM SEC_FUNCTION AS e
    INNER JOIN MENUHIRARCHY M on M.functionId = e.functionparentId
    WHERE 1=1
    AND     e.functionIsActive=1
    AND     e.functionIsActive=1
)
INSERT INTO @MENUTBL
(
        UNIQUE_ID,
        PARENTFUNCTIONID,
        FUNCTIONID,
        Level,
        MenuDisplayText,
        countfunction
)
SELECT  DISTINCT '0_'+CAST(FunctionId as varchar(10)),FunctionParentId, FunctionId, Level, MenuDisplayText, countfunction
FROM    MENUHIRARCHY;

正如您所看到的,通过在 functionid 中预置 0 来生成唯一的 Id。这只是为了向您展示也可以以这种方式生成自定义的唯一 Id。

下一步是动态构建 SQL 查询,如上文简要说明。SP 的以下摘录显示了动态 SQL 查询是如何构建的。这里要理解的一个重要事情是 WHERE 子句。请理解,并非每个菜单项都必须关联一个计数函数,因为业务可能只要求特定菜单项的计数。WHERE 子句过滤掉所有没有关联计数函数的菜单项。还要仔细查看我们如何将参数传递给函数。这里的一个限制是参数集对所有函数都是常量的,这个限制可以通过将每个函数的所有参数存储在一个表中以及它们的解析机制(我们从哪里获取这些参数的值)来避免,但这在这里是题外话。

SELECT @tsql = COALESCE(@tsql + '  UNION ALL ', 'INSERT INTO ##LocalTempMenuTable(ProjectID, FunctionID, value) ') + 'SELECT ' + cast(ProjectID as varchar(10)) + ',' + cast(FunctionID as varchar(10)) + ',' + Replace(Replace(Replace(CountFunction, '{0}', ProjectID), '{1}', FUNCTIONID), '{2}', @p_ContactID)
FROM   @MENUTBL
WHERE CountFunction IS NOT NULL;

@TSQL 的执行步骤很简单。SQL 将被执行,结果将存储在临时表中。

EXEC SP_EXECUTESQL @TSQL;

现在我们需要更新我们的值->`@MENUTBL`,这现在是小菜一碟,因为我们只需要一个连接来将所有函数值更新到它们关联的菜单项。请看我们是如何做到的:

UPDATE    @MENUTBL
SET        value=CAST(m.value as varchar(20))
FROM    @MENUTBL a
INNER JOIN ##LocalTempMenuTable m on a.functionId = m.functionId and a.ProjectId = m.ProjectId;

最后一步是生成XML,我们使用FOR XML AUTO, TYPE以所需格式生成XML。DocGet_XMLNode用于创建嵌套元素,因为我们的XML是嵌套的。

SET @xml =  
(
    SELECT
    IsNull(Unique_id,'') as 't',
    Isnull(n.value,'') as 'v',
    dbo.DocGet_XMLNode(@MENUTBL, FunctionId)
    FROM   @MENUTBL n
    where    1=1
    AND        Level = 1
    FOR XML AUTO, TYPE
);
    
SET @p_Outstring = '<r>'+CONVERT(VARCHAR(max), @xml, 1)+'</r>';

DocGet_MenuXML 到此结束。现在让我们看看我们是如何创建 ` get_GetAreaCount ` 函数的,它返回 Area 菜单项的计数。该函数只是一个示例,实际上并没有做任何事情,它只是向您展示如何自己构建复杂的函数。` get_GetAreaCount ` 这里只是简单地返回一个随机值。

ALTER FUNCTION [dbo].[get_GetAreaCount](@p_Param1 int, @p_Param2 int,  @p_Param3 int)
RETURNS INT
AS
BEGIN

    declare @Count INT = NULL  
    
 --   SELECT  @Count = COUNT(DISTINCT FUNCTIONID)
    --FROM    SEC_FUNCTION 
 --   WHERE    Functionid = @p_Param2
    SELECT @Count = cast(rndResult * 100 as int)
    FROM RandomView
    
    return  @Count
END

HomeController->GetServerResponse

应用程序向 HomeController->GetServerResponse action 发起 AJAX 调用。以下是代码片段的摘录,代码不言自明。它只是简单地创建一个数据库连接,创建一个命令对象,执行 DocGet_MenuXML 存储过程,并将内容返回给应用程序。

        public ActionResult GetServerResponse()
        {
            SqlConnection connection = null;
            try
            {
                string connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["DefaultConnection"].ConnectionString;
                string outXMLString = string.Empty;

                connection = new SqlConnection(connectionString);

                using (SqlCommand cmd = new SqlCommand("DocGet_MenuXML", connection))
                {
                    cmd.CommandType = System.Data.CommandType.StoredProcedure;
                    cmd.Parameters.Add(new SqlParameter("@p_ContactID", System.Data.SqlDbType.Int));
                    cmd.Parameters["@p_ContactID"].Direction = System.Data.ParameterDirection.Input;
                    cmd.Parameters["@p_ContactID"].Value = 1;
                    SqlParameter outparam = cmd.Parameters.Add(new SqlParameter("@p_Outstring", System.Data.SqlDbType.VarChar, 8000));
                    outparam.Direction = System.Data.ParameterDirection.Output;

                    connection.Open();
                    cmd.ExecuteNonQuery();
                    connection.Close();

                    outXMLString = Convert.ToString(cmd.Parameters["@p_Outstring"].Value);
                }

                ViewBag.MenuXML = outXMLString;
                return Content(outXMLString);
            }
            catch (System.Data.DataException e)
            {
                if (connection != null)
                    if (connection.State != System.Data.ConnectionState.Closed)
                        connection.Close();
                throw new Exception(e.Message);
            }
        }

_Layout.cshtml

这是表示层的核心部分,它首先为每N秒/分钟设置一个定时器钩子,并调用 ` getCountStatus() ` 函数。

    window.setInterval(function () {
        getCountStatus();
    }, 1000 * 10 * 1.25);          //where X is your every X minutes

getCountFunction 调用 AJAX,接收 menuXML 字符串,将 xml 字符串解析为 XML 对象。成功解析后,它获取 XML 的根节点(如果记得是“r”),并将其传递给 MenuIterator 函数。

    var actionUrlGetServerResponse = '@Url.Action("GetServerResponse", "Home")';
    function getCountStatus() {
        $.get(actionUrlGetServerResponse, function (projectTransmittalStatus) {
            var menuXML = '@ViewBag.MenuXML';

            $xmlDoc = $($.parseXML(projectTransmittalStatus));
            var rootNode = $xmlDoc.find("*").eq(0);
            MenuIterator($(rootNode));
        });
    }

MenuIterator 本质上是一个迭代器。它在 XML 中查找所有 n 个节点。对于每个 XML 节点,它通过查看 t 属性获取键。使用此键,它搜索 DevExpress 树形控件并找到相应的节点。成功识别节点后,应用程序使用正则表达式识别树形节点文本中是否已存在任何数字。这很棘手,因为处理不当可能导致每次 AJAX 调用时都出现“()”的连接。我们使用正则表达式来识别菜单文本是否已更新。如果已更新,我们只更新菜单文本中的数字。如果未更新,则将文本更新为“区域 (9)”之类的形式。

    function MenuIterator($currentXMLNode) {
        if ($currentXMLNode != null) {

            $currentXMLNode.find('n').each(function (childXMLNode) { //'n'
                if (childXMLNode != null) {
                    var menuText = $(this).attr('t');
                    var node = tvFunction.GetNodeByName(menuText);
                    if (node != null) {
                        //console.log("INFO", "node Value..." + node.GetText);
                        var nodeTotal = $(this).attr('v');
                        console.log("INFO", "nodeTotal" + nodeTotal);
                        if (nodeTotal != '') {
                            var re = new RegExp(/\d+/g);
                            var nodeText = node.GetText();
                            console.log("INFO", "nodeText = " + nodeText);
                            var m = re.exec(nodeText);
                            if (m != null) {
                                nodeText = nodeText.replace(re, nodeTotal);
                                console.log("INFO", "Matched" + nodeText);
                            }
                            else {
                                nodeText = nodeText + '(' + nodeTotal + ')';
                                console.log("INFO", "Not Matched Value..." + nodeText);
                            }
                            node.SetText(nodeText);
                        }
                    }
                    else {
                        //console.log("INFO", "node Value NULL " + menuText);
                    }

                }
            });
        }
    }


结论

本文中提出的方法不仅有助于构建具有类似要求的应用程序,还可用于需要动态绑定代码的其他领域

历史

第一版 - 0.1 - 2014年7月20日

© . All rights reserved.