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

ASP.NET 横向菜单控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (39投票s)

2009年12月15日

CPOL

5分钟阅读

viewsIcon

558539

downloadIcon

10583

一篇关于如何使用 ASP.NET 构建带访问键和目标窗口支持的横向菜单控件的文章。

引言

几周前,我正在开发一个 ASP.NET Web 应用程序,需要一个带有子菜单的简单横向菜单。我决定使用 ASP.NET 的 Menu 控件,然后将其拖放到页面上。这很简单,但该控件不为菜单项提供访问键和目标窗口支持。我编写了一个教程,介绍如何

  1. 包含 accesskey 属性
  2. 包含 target 属性
  3. 包含站点地图路径
图 1

Sample results

入门

这是我的项目结构。欢迎下载演示。

图 2

Project Structure

整合所有内容

首先,为网站项目添加一个站点地图。打开 web.sitemap 文件并用您的导航数据和结构填充它。要加下划线菜单标题中的某个字符,我们可以使用 HTML 下划线标签(<u></u>)。为了完美地解析 XML,我们必须将小于号 (<) 替换为 & lt;(无空格)。然后,在每个 siteMapNode 中包含 accesskeytarget 属性及其值。请参阅下面的示例。

站点地图

列表 1
<siteMapNode>
      <siteMapNode url="Default.aspx" 
         title="& lt;u>H& lt;/u>ome" 
         description="Home" 
         accesskey="H" />
      <siteMapNode url="~/Views/Menu1.aspx" 
                   title="& lt;u>M& lt;/u>enu1"  
                   description="Menu1" accesskey="M" />
        <siteMapNode url="~/Views/Menu2.aspx" 
                     title="M<u>e</u>nu2" 
                     description="Menu2" accesskey="E" />
    
    <siteMapNode url="~/Views/Menu3.aspx" 
                 title="Me<u>n</u>u3" 
                 description="Menu3" 
                 accesskey="N" target="_blank" />
        
    <siteMapNode url="~/Views/Menu4.aspx" 
                 title="Men<u>u</u>4" 
                 description="Menu4" accesskey="U">
      <siteMapNode url="~/Views/Menu4Sub1.aspx" 
                   title="Menu4<u>S</u>ub1" 
                   description="Menu4Sub1" 
                   accesskey="S" />
      <siteMapNode url="~/Views/Menu4Sub2.aspx" 
                   title="Menu4Su<u>b</u>2" 
                   description="Menu4Sub2" 
                   target="_blank" accesskey="B" />
    </siteMapNode>
……
….
  </siteMapNode>
</siteMap>

主页模板

为网站项目添加一个母版页。将 SiteMapDataSource 控件拖放到页面上,然后将 Menu 控件拖放到页面上,并将 Menu 控件包装在 div 标签中。您可以在 此处找到每个菜单属性的详细说明。设置 staticdisplaylevels ="2"orientation="Horizontal" 以水平模式显示菜单控件。我们可以使用内联样式表,或者将 CSS 样式放在外部文件中。在本教程中,CSS 样式位于 style.css 文件中。请参阅列表 2。

列表 2
<asp:SiteMapDataSource id="MenuSource" runat="server" />
<div class="background">
  <asp:menu id="NavigationMenu" CssClass="NavigationMenu"  
        staticdisplaylevels="2" DynamicHorizontalOffset="1"
        staticsubmenuindent="1px" MaximumDynamicDisplayLevels="4"
        orientation="Horizontal"   
        DynamicPopOutImageUrl="~/Images/right-arrow.gif" 
        StaticPopOutImageUrl="~/Images/drop-arrow.gif"
        datasourceid="MenuSource"    
        runat="server" Height="30px">

        <staticmenuitemstyle ItemSpacing="10" 
                    CssClass="staticMenuItemStyle"/>
        <statichoverstyle CssClass="staticHoverStyle" />
       <StaticSelectedStyle CssClass="staticMenuItemSelectedStyle"/> 
        <DynamicMenuItemStyle CssClass="dynamicMenuItemStyle" />      
        <dynamichoverstyle CssClass="menuItemMouseOver" />
        <DynamicMenuStyle CssClass="menuItem" />
       <DynamicSelectedStyle CssClass="menuItemSelected" />
     
       <DataBindings>        
             <asp:MenuItemBinding DataMember="siteMapNode" 
                    NavigateUrlField="url" TextField="title"  
                    ToolTipField="description" />
        </DataBindings>

      </asp:menu>
</div>

SiteMapPath 控件拖放到页面上。此控件的目的是显示一个导航路径,向用户显示当前页面的位置。请参阅列表 3。

列表 3
<div id="e">
       <asp:SiteMapPath ID="SiteMapPath1" runat="server" 
                RenderCurrentNodeAsLink="true" 
                CssClass="currentNodeStyle"
            PathSeparator=" >> ">
            <PathSeparatorStyle ForeColor="#5D7B9D" CssClass="currentNodeStyle" />
            <CurrentNodeStyle ForeColor="#333333" CssClass="currentNodeStyle" />
            <NodeStyle ForeColor="#7C6F57"  CssClass="currentNodeStyle"  />
            <RootNodeStyle  ForeColor="#5D7B9D" CssClass="currentNodeStyle"  />
    </asp:SiteMapPath> 
</div>

母版页代码隐藏

Page_Load 事件中包含 MenuItemDataBoundSiteMapResolve 事件处理程序。前一个事件的目的是在菜单项被渲染或显示在 Menu 控件中之前插入 target 属性值并创建菜单项的访问键。后一个事件用于修改 SiteMapPath 控件显示的文本。

列表 4
NavigationMenu.MenuItemDataBound += 
     new MenuEventHandler(NavigationMenu_MenuItemDataBound);
SiteMap.SiteMapResolve += 
     new SiteMapResolveEventHandler(SiteMap_SiteMapResolve);

下面是 NavigationMenu_MenuItemDataBound 方法的实现。当 Menu 控件中的菜单项绑定到数据时,会发生 MenuItemDataBound 事件。也就是说,它将遍历每个 siteMapNode 并查找 accesskeytarget 属性。菜单项有一个关联的 target 属性,我们可以使用 target 属性值来设置其目标窗口。请参阅列表 5。

列表 5
void NavigationMenu_MenuItemDataBound(object sender, MenuEventArgs e)
{
    SiteMapNode node = (SiteMapNode)e.Item.DataItem;
   
    //set the target of the navigation menu item (blank, self, etc...)
    if (node["target"] != null)
    {
        e.Item.Target = node["target"];
    }
    //create access key button
    if (node["accesskey"] != null)
    {
        CreateAccessKeyButton(node["accesskey"] as string, node.Url);
    }
}

为了使访问键生效,请在母版页中添加一个 Panel 控件,并添加一个 JavaScript 函数以将网页重定向到指定的页面。请参阅下文。

列表 6
<asp:Panel ID="AccessKeyPanel" runat="server" />
<script type="text/javascript">
 function navigateTo(url) {
    window.location = url;
 }
</script>

下面是 CreateAccessKeyButton 方法的实现。动态创建一个 HtmlButton 控件并为其附加一个 onclick 事件。将 style.left 属性设置为 -2555px 以隐藏该控件。可在 此处找到不同浏览器中访问键的完整列表。

列表 7
//create access key button
void CreateAccessKeyButton(string ak, string url)
{
    HtmlButton inputBtn = new HtmlButton();
    inputBtn.Style.Add("width", "1px");
    inputBtn.Style.Add("height", "1px");
    inputBtn.Style.Add("position", "absolute");
    inputBtn.Style.Add("left", "-2555px");
    inputBtn.Style.Add("z-index", "-1");
    inputBtn.Attributes.Add("type", "button");
    inputBtn.Attributes.Add("value", "");
    inputBtn.Attributes.Add("accesskey", ak);
    inputBtn.Attributes.Add("onclick", "navigateTo('" + url + "');");

    AccessKeyPanel.Controls.Add(inputBtn);
}

当访问 CurrentNode 属性时,会触发 SiteMap.SiteMapResolve 事件。它将递归调用 ReplaceNodeText 方法并替换 HTML 下划线标签。请参阅列表 8。

列表 8
SiteMapNode SiteMap_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
{
   if (SiteMap.CurrentNode != null)
        {
            SiteMapNode currentNode = SiteMap.CurrentNode.Clone(true);
            SiteMapNode tempNode = currentNode;
            tempNode = ReplaceNodeText(tempNode);

            return currentNode;
        }

        return null;
}

//remove <u></u> tag recursively
internal SiteMapNode ReplaceNodeText(SiteMapNode smn)
{
    //current node
    if (smn != null && smn.Title.Contains("<u>"))
    {
        smn.Title = smn.Title.Replace("<u>", 
                       "").Replace("</u>", "");
    }

    //parent node
    if (smn.ParentNode != null)
    {
        if (smn.ParentNode.Title.Contains("<u>"))
        {
            SiteMapNode gpn = smn.ParentNode;
            smn.ParentNode.Title = smn.ParentNode.Title.Replace(
              "<u>", "").Replace("</u>", "");
            smn = ReplaceNodeText(gpn);
        }
    }
    return smn;
}

使用代码

由于菜单位于母版页中,右键单击网站项目,选择“添加新项”,选择“Web 窗体”,然后选中“选择母版页”复选框。

关注点

悬停菜单似乎在移动设备上不起作用。为了解决这个问题,我包含了一个 TreeView 控件并将其 Visible 属性设置为 false。此控件默认展开其所有节点。这将解决上述问题。在代码隐藏中,如果请求的浏览器是移动设备,则隐藏 Menu 控件并显示 TreeView 控件。请参阅列表 9。

列表 9
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.Browser.IsMobileDevice)
    {
        NavigationMenu.Visible = false;
        NavigationTreeView.Visible = true;
    }
}

当我尝试在 IE 8 中测试菜单时,悬停菜单未正确渲染。为了解决这个问题,我将 DynamicMenuStyle 的 z-index 设置为 200,请参阅 style.css。子菜单在 Google Chrome 中不起作用。经过一番研究,我找到了解决方案。请参阅列表 10。

列表 10
protected void Page_Load(object sender, EventArgs e)
{
    if (Request.UserAgent.IndexOf("AppleWebKit") > 0)
    {
        Request.Browser.Adapters.Clear();
        NavigationMenu.DynamicMenuStyle.Width = Unit.Pixel(120);
    }
}

新更新

我收到读者关于菜单控件在 Safari 和 Google Chrome 浏览器中显示不正确的几条投诉。不知何故,菜单项堆叠在一起,子菜单的宽度出现了间隔。经过一些研究,我在 此处找到了答案,请参阅列表 11。要修复子菜单宽度,请从 CSS 文件中的 dynamicMenuItemStyle 中删除 display:block

列表 11
protected override void AddedControl(Control control, int index)
{
    if (Request.ServerVariables["http_user_agent"].IndexOf("Safari",
        StringComparison.CurrentCultureIgnoreCase) != -1)
        this.Page.ClientTarget = "uplevel";

    base.AddedControl(control, index);
}

我还使用 Vincent Van Zyl 的代码重写了检测移动浏览器的逻辑。请参阅列表 12。

列表 12
public static readonly string[] mobiles =
      new[]
            {
                "midp", "j2me", "avant", "docomo", 
                "novarra", "palmos", "palmsource", 
                "240x320", "opwv", "chtml",
                "pda", "windows ce", "mmp/", 
                "blackberry", "mib/", "symbian", 
                "wireless", "nokia", "hand", "mobi",
                "phone", "cdm", "up.b", "audio", 
                "SIE-", "SEC-", "samsung", "HTC", 
                "mot-", "mitsu", "sagem", "sony"
                , "alcatel", "lg", "eric", "vx", 
                "NEC", "philips", "mmm", "xx", 
                "panasonic", "sharp", "wap", "sch",
                "rover", "pocket", "benq", "java", 
                "pt", "pg", "vox", "amoi", 
                "bird", "compal", "kg", "voda",
                "sany", "kdd", "dbt", "sendo", 
                "sgh", "gradi", "jb", "dddi", 
                "moto", "iphone"
            };

public static bool isMobileBrowser()
{
    //GETS THE CURRENT USER CONTEXT
    HttpContext context = HttpContext.Current;

    //FIRST TRY BUILT IN ASP.NT CHECK
    if (context.Request.Browser.IsMobileDevice)
    {
        return true;
    }
    //THEN TRY CHECKING FOR THE HTTP_X_WAP_PROFILE HEADER
    if (context.Request.ServerVariables["HTTP_X_WAP_PROFILE"] != null)
    {
        return true;
    }
    //THEN TRY CHECKING THAT HTTP_ACCEPT EXISTS AND CONTAINS WAP
    if (context.Request.ServerVariables["HTTP_ACCEPT"] != null &&
        context.Request.ServerVariables["HTTP_ACCEPT"].ToLower().Contains("wap"))
    {
        return true;
    }
    //AND FINALLY CHECK THE HTTP_USER_AGENT 
    //HEADER VARIABLE FOR ANY ONE OF THE FOLLOWING
    if (context.Request.ServerVariables["HTTP_USER_AGENT"] != null)
    {
        for (int i = 0; i < mobiles.Length; i++)
        {
            if (context.Request.ServerVariables["HTTP_USER_AGENT"].
                                                ToLower().Contains(
                                                    mobiles[i].ToLower()))
            {
                return true;
            }
        }
    }

    return false;
}

结论

如果您发现任何错误或不同意内容,请给我留言,我将与您一起纠正。

已在 IE 6.0/7.0/8.0、Google Chrome、Safari 和 Firefox 上测试。

历史

  • 2010/02/25 - 根据 The Code Project 会员 kentex2000 的建议,移除了根节点之前的 SiteMapPathPathSeparator。在 SiteMap_SiteMapResolve 方法中添加了 SiteMap.CurrentNode != null
  • 2010/02/03 - 修复了 Safari 和 Google Chrome 浏览器中的菜单显示问题,并添加了检测移动浏览器的新逻辑。

资源

© . All rights reserved.