//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.UI.WebControls { using System.Collections; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Linq; using System.Web.Util; public partial class Menu { /// The standards-compliant Menu renderer internal class MenuRendererStandards : MenuRenderer { private string _dynamicPopOutUrl; private string _staticPopOutUrl; public MenuRendererStandards(Menu menu) : base(menu) { } private string DynamicPopOutUrl { get { if (_dynamicPopOutUrl == null) { _dynamicPopOutUrl = GetDynamicPopOutImageUrl(); } return _dynamicPopOutUrl; } } protected virtual string SpacerImageUrl { get { return Menu.SpacerImageUrl; } } private string StaticPopOutUrl { get { if (_staticPopOutUrl == null) { _staticPopOutUrl = GetStaticPopOutImageUrl(); } return _staticPopOutUrl; } } private void AddScriptReference() { string key = "_registerMenu_" + Menu.ClientID; string initScript = String.Format(CultureInfo.InvariantCulture, "", Menu.ClientID, Menu.DisappearAfter, Menu.Orientation.ToString().ToLowerInvariant(), Menu.TabIndex, (!Menu.IsEnabled).ToString().ToLowerInvariant()); if (Menu.Page.ScriptManager != null) { Menu.Page.ScriptManager.RegisterClientScriptResource(Menu.Page, typeof(Menu), "MenuStandards.js"); Menu.Page.ScriptManager.RegisterStartupScript(Menu, typeof(MenuRendererStandards), key, initScript, false); } else { Menu.Page.ClientScript.RegisterClientScriptResource(Menu.Page, typeof(Menu), "MenuStandards.js"); Menu.Page.ClientScript.RegisterStartupScript(typeof(MenuRendererStandards), key, initScript); } } private void AddStyleBlock() { if (Menu.IncludeStyleBlock) { Menu.Page.Header.Controls.Add(CreateStyleBlock()); } } private StyleBlock CreateStyleBlock() { StyleBlock styleBlock = new StyleBlock(); Style rootMenuItemStyle = Menu.RootMenuItemStyle; // drop the font and forecolor from the control style, those are applied // to the anchors directly with rootMenuItemStyle. Style menuStyle = null; if (!Menu.ControlStyle.IsEmpty) { menuStyle = new Style(); menuStyle.CopyFrom(Menu.ControlStyle); // Relative sizes should not be multiplied (VSWhidbey 457610) menuStyle.Font.Reset(); menuStyle.ForeColor = Color.Empty; } // Menu surrounding DIV style -- without ForeColor or Font, // those are applied directly to the '#Menu a' selector. styleBlock.AddStyleDefinition("#{0}", Menu.ClientID) .AddStyles(menuStyle); // Image styles styleBlock.AddStyleDefinition("#{0} img.icon", Menu.ClientID) .AddStyle(HtmlTextWriterStyle.BorderStyle, "none") .AddStyle(HtmlTextWriterStyle.VerticalAlign, "middle"); styleBlock.AddStyleDefinition("#{0} img.separator", Menu.ClientID) .AddStyle(HtmlTextWriterStyle.BorderStyle, "none") .AddStyle(HtmlTextWriterStyle.Display, "block"); if (Menu.Orientation == Orientation.Horizontal) { styleBlock.AddStyleDefinition("#{0} img.horizontal-separator", Menu.ClientID) .AddStyle(HtmlTextWriterStyle.BorderStyle, "none") .AddStyle(HtmlTextWriterStyle.VerticalAlign, "middle"); } // Menu styles styleBlock.AddStyleDefinition("#{0} ul", Menu.ClientID) .AddStyle("list-style", "none") .AddStyle(HtmlTextWriterStyle.Margin, "0") .AddStyle(HtmlTextWriterStyle.Padding, "0") .AddStyle(HtmlTextWriterStyle.Width, "auto"); styleBlock.AddStyleDefinition("#{0} ul.static", Menu.ClientID) .AddStyles(Menu._staticMenuStyle); var ulDynamic = styleBlock.AddStyleDefinition("#{0} ul.dynamic", Menu.ClientID) .AddStyles(Menu._dynamicMenuStyle) .AddStyle(HtmlTextWriterStyle.ZIndex, "1"); if (Menu.DynamicHorizontalOffset != 0) { ulDynamic.AddStyle(HtmlTextWriterStyle.MarginLeft, Menu.DynamicHorizontalOffset.ToString(CultureInfo.InvariantCulture) + "px"); } if (Menu.DynamicVerticalOffset != 0) { ulDynamic.AddStyle(HtmlTextWriterStyle.MarginTop, Menu.DynamicVerticalOffset.ToString(CultureInfo.InvariantCulture) + "px"); } if (Menu._levelStyles != null) { int index = 1; foreach (SubMenuStyle style in Menu._levelStyles) { styleBlock.AddStyleDefinition("#{0} ul.level{1}", Menu.ClientID, index++) .AddStyles(style); } } // Menu item styles // MenuItems have a 14px default right padding. // This is necessary to prevent the default (and the typical) popout image from going under // the menu item text when it is one of the longer items in the parent menu. // It is 'px' based since its based on the image size not font size. styleBlock.AddStyleDefinition("#{0} a", Menu.ClientID) .AddStyle(HtmlTextWriterStyle.WhiteSpace, "nowrap") .AddStyle(HtmlTextWriterStyle.Display, "block") .AddStyles(rootMenuItemStyle); var menuItemStatic = styleBlock.AddStyleDefinition("#{0} a.static", Menu.ClientID); if ((Menu.Orientation == Orientation.Horizontal) && ((Menu._staticItemStyle == null) || (Menu._staticItemStyle.HorizontalPadding.IsEmpty))) { menuItemStatic.AddStyle(HtmlTextWriterStyle.PaddingLeft, "0.15em") .AddStyle(HtmlTextWriterStyle.PaddingRight, "0.15em"); } menuItemStatic.AddStyles(Menu._staticItemStyle); if (Menu._staticItemStyle != null) { menuItemStatic.AddStyles(Menu._staticItemStyle.HyperLinkStyle); } if (!String.IsNullOrEmpty(StaticPopOutUrl)) { styleBlock.AddStyleDefinition("#{0} a.popout", Menu.ClientID) .AddStyle("background-image", "url(\"" + Menu.ResolveClientUrl(StaticPopOutUrl).Replace("\"", "\\\"") + "\")") .AddStyle("background-repeat", "no-repeat") .AddStyle("background-position", "right center") .AddStyle(HtmlTextWriterStyle.PaddingRight, "14px"); } if (!String.IsNullOrEmpty(DynamicPopOutUrl)) { // Check if dynamic popout is the same as the static one, so there's no need for a separate rule if (DynamicPopOutUrl != StaticPopOutUrl) { styleBlock.AddStyleDefinition("#{0} a.popout-dynamic", Menu.ClientID) .AddStyle("background", "url(\"" + Menu.ResolveClientUrl(DynamicPopOutUrl).Replace("\"", "\\\"") + "\") no-repeat right center") .AddStyle(HtmlTextWriterStyle.PaddingRight, "14px"); } } var styleBlockStyles = styleBlock.AddStyleDefinition("#{0} a.dynamic", Menu.ClientID) .AddStyles(Menu._dynamicItemStyle); if (Menu._dynamicItemStyle != null) { styleBlockStyles.AddStyles(Menu._dynamicItemStyle.HyperLinkStyle); } if (Menu._levelMenuItemStyles != null || Menu.StaticDisplayLevels > 1) { int lastIndex = Menu.StaticDisplayLevels; if (Menu._levelMenuItemStyles != null) { lastIndex = Math.Max(lastIndex, Menu._levelMenuItemStyles.Count); } for (int index = 0; index < lastIndex; ++index) { var style = styleBlock.AddStyleDefinition("#{0} a.level{1}", Menu.ClientID, index + 1); if (index > 0 && index < Menu.StaticDisplayLevels) { Unit indent = Menu.StaticSubMenuIndent; // The default value of Menu.StaticSubMenuIndent is Unit.Empty, and the effective default value // for list rendering is either 1em (vertical) or empty (horizontal). if (indent.IsEmpty && Menu.Orientation == Orientation.Vertical) { indent = new Unit(1, UnitType.Em); } if (!indent.IsEmpty && indent.Value != 0) { double indentValue = indent.Value * index; if (indentValue < Unit.MaxValue) { indent = new Unit(indentValue, indent.Type); } else { indent = new Unit(Unit.MaxValue, indent.Type); } style.AddStyle(HtmlTextWriterStyle.PaddingLeft, indent.ToString(CultureInfo.InvariantCulture)); } } if (Menu._levelMenuItemStyles != null && index < Menu._levelMenuItemStyles.Count) { var levelItemStyle = Menu._levelMenuItemStyles[index]; style.AddStyles(levelItemStyle).AddStyles(levelItemStyle.HyperLinkStyle); } } } styleBlockStyles = styleBlock.AddStyleDefinition("#{0} a.static.selected", Menu.ClientID) .AddStyles(Menu._staticSelectedStyle); if (Menu._staticSelectedStyle != null) { styleBlockStyles.AddStyles(Menu._staticSelectedStyle.HyperLinkStyle); } styleBlockStyles = styleBlock.AddStyleDefinition("#{0} a.dynamic.selected", Menu.ClientID) .AddStyles(Menu._dynamicSelectedStyle); if (Menu._dynamicSelectedStyle != null) { styleBlockStyles.AddStyles(Menu._dynamicSelectedStyle.HyperLinkStyle); } styleBlock.AddStyleDefinition("#{0} a.static.highlighted", Menu.ClientID) .AddStyles(Menu._staticHoverStyle); styleBlock.AddStyleDefinition("#{0} a.dynamic.highlighted", Menu.ClientID) .AddStyles(Menu._dynamicHoverStyle); if (Menu._levelSelectedStyles != null) { int index = 1; foreach (MenuItemStyle style in Menu._levelSelectedStyles) { styleBlock.AddStyleDefinition("#{0} a.selected.level{1}", Menu.ClientID, index++) .AddStyles(style).AddStyles(style.HyperLinkStyle); } } return styleBlock; } private string GetCssClass(int level, Style staticStyle, Style dynamicStyle, IList levelStyles) { string result = "level" + level; Style style; if (level > Menu.StaticDisplayLevels) { style = dynamicStyle; } else { if (Menu.DesignMode) { result += " static"; if (Menu.Orientation == Orientation.Horizontal) { result += " horizontal"; } } style = staticStyle; } if (style != null && !String.IsNullOrEmpty(style.CssClass)) { result += " " + style.CssClass; } if (levelStyles != null && levelStyles.Count >= level) { Style levelStyle = (Style)levelStyles[level - 1]; if (levelStyle != null && !String.IsNullOrEmpty(levelStyle.CssClass)) { result += " " + levelStyle.CssClass; } } return result; } protected virtual string GetDynamicPopOutImageUrl() { string url = Menu.DynamicPopOutImageUrl; if (String.IsNullOrEmpty(url) && Menu.DynamicEnableDefaultPopOutImage) { url = Menu.GetImageUrl(Menu.PopOutImageIndex); } return url; } protected virtual string GetStaticPopOutImageUrl() { string url = Menu.StaticPopOutImageUrl; if (String.IsNullOrEmpty(url) && Menu.StaticEnableDefaultPopOutImage) { url = Menu.GetImageUrl(Menu.PopOutImageIndex); } return url; } private string GetMenuCssClass(int level) { return GetCssClass(level, Menu.StaticMenuStyle, Menu.DynamicMenuStyle, Menu._levelStyles); } private string GetMenuItemCssClass(MenuItem item, int level) { // give the A the proper popout class string cssClass = null; if (ShouldHavePopOutImage(item)) { if (level > Menu.StaticDisplayLevels) { if (!String.IsNullOrEmpty(DynamicPopOutUrl)) { cssClass = (DynamicPopOutUrl == StaticPopOutUrl) ? "popout" : "popout-dynamic"; } } else if (!String.IsNullOrEmpty(StaticPopOutUrl)) { cssClass = "popout"; } } string levelCssClass = GetCssClass(level, Menu.StaticMenuItemStyle, Menu.DynamicMenuItemStyle, Menu._levelMenuItemStyles); if (!String.IsNullOrEmpty(cssClass)) { return cssClass + " " + levelCssClass; } else { return levelCssClass; } } protected virtual string GetPostBackEventReference(MenuItem item) { return Menu.Page.ClientScript.GetPostBackEventReference(Menu, item.InternalValuePath, true); } private bool IsChildPastMaximumDepth(MenuItem item) { return (item.Depth + 1 >= Menu.MaximumDepth); } private bool IsChildDepthDynamic(MenuItem item) { return (item.Depth + 1 >= Menu.StaticDisplayLevels); } private bool IsDepthDynamic(MenuItem item) { // Depth is 0 based. StaticDisplayLevels is 1 based because it is a counter // (1 means show "one level" statically -- so, show Depth 0 statically but not Depth 1). // Therefore, it is dynamic if the item's depth is greater than or equal to the static levels. // Depth = 2 (3rd level), StaticDisplayLevels = 3 ==> Static // Depth = 2 (3rd level), StaticDisplayLevels = 2 ==> Dynamic return (item.Depth >= Menu.StaticDisplayLevels); } private bool IsDepthStatic(MenuItem item) { return !IsDepthDynamic(item); } public override void PreRender(bool registerScript) { if (Menu.DesignMode || Menu.Page == null) { return; } if (Menu.IncludeStyleBlock && Menu.Page.Header == null) { throw new InvalidOperationException(SR.GetString(SR.NeedHeader, "Menu.IncludeStyleBlock")); } AddScriptReference(); // We always need our script, even if we're disabled, because the script sets our styles AddStyleBlock(); } public override void RenderBeginTag(HtmlTextWriter writer, bool staticOnly) { ControlRenderingHelper.WriteSkipLinkStart(writer, Menu.RenderingCompatibility, Menu.DesignMode, Menu.SkipLinkText, SpacerImageUrl, Menu.ClientID); if (Menu.DesignMode && Menu.IncludeStyleBlock) { // Need to render style block in design mode, since it won't be present CreateStyleBlock().Render(writer); } // Add expando attributes if (Menu.HasAttributes) { foreach (string key in Menu.Attributes.Keys) { writer.AddAttribute(key, Menu.Attributes[key]); } } // CSS class, including disabled class if it's set string cssClass = Menu.CssClass ?? ""; if (!Menu.Enabled) { cssClass = (cssClass + " " + DisabledCssClass).Trim(); } if (!String.IsNullOrEmpty(cssClass)) { writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); } // Need to simulate the float done by Javascript when we're in design mode if (Menu.DesignMode) { writer.AddStyleAttribute("float", "left"); } writer.AddAttribute(HtmlTextWriterAttribute.Id, Menu.ClientID); writer.RenderBeginTag(HtmlTextWriterTag.Div); } public override void RenderContents(HtmlTextWriter writer, bool staticOnly) { RenderItems(writer, staticOnly || Menu.DesignMode || !Menu.Enabled, Menu.Items, 1, !String.IsNullOrEmpty(Menu.AccessKey)); } public override void RenderEndTag(HtmlTextWriter writer, bool staticOnly) { writer.RenderEndTag(); // Need to simulate the clear done by Javascript when we're in design mode if (Menu.DesignMode) { writer.AddAttribute(HtmlTextWriterAttribute.Style, "clear: left"); writer.RenderBeginTag(HtmlTextWriterTag.Div); writer.RenderEndTag(); } ControlRenderingHelper.WriteSkipLinkEnd(writer, Menu.DesignMode, Menu.SkipLinkText, Menu.ClientID); } private bool RenderItem(HtmlTextWriter writer, MenuItem item, int level, string cssClass, bool needsAccessKey) { RenderItemPreSeparator(writer, item); if (Menu.DesignMode && Menu.Orientation == Orientation.Horizontal) { writer.AddStyleAttribute(HtmlTextWriterStyle.WhiteSpace, "nowrap"); } needsAccessKey = RenderItemLinkAttributes(writer, item, level, cssClass, needsAccessKey); writer.RenderBeginTag(HtmlTextWriterTag.A); RenderItemIcon(writer, item); item.RenderText(writer); // popout image is in the A's background css writer.RenderEndTag(); // RenderItemPostSeparator(writer, item); return needsAccessKey; } private void RenderItemIcon(HtmlTextWriter writer, MenuItem item) { if (String.IsNullOrEmpty(item.ImageUrl) || !item.NotTemplated()) { return; } writer.AddAttribute(HtmlTextWriterAttribute.Src, Menu.ResolveClientUrl(item.ImageUrl)); writer.AddAttribute(HtmlTextWriterAttribute.Alt, item.ToolTip); writer.AddAttribute(HtmlTextWriterAttribute.Title, item.ToolTip); writer.AddAttribute(HtmlTextWriterAttribute.Class, "icon"); writer.RenderBeginTag(HtmlTextWriterTag.Img); writer.RenderEndTag(); } private bool RenderItemLinkAttributes(HtmlTextWriter writer, MenuItem item, int level, string cssClass, bool needsAccessKey) { if (!String.IsNullOrEmpty(item.ToolTip)) { writer.AddAttribute(HtmlTextWriterAttribute.Title, item.ToolTip); } // Bail out for for disabled or non-selectable menu items if (!item.Enabled || !Menu.Enabled) { writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass + " " + DisabledCssClass); return needsAccessKey; } if (!item.Selectable) { writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); return needsAccessKey; } // Selected if (item.Selected) { cssClass += " selected"; } writer.AddAttribute(HtmlTextWriterAttribute.Class, cssClass); // Attach the access key to the first link we render if (needsAccessKey) { writer.AddAttribute(HtmlTextWriterAttribute.Accesskey, Menu.AccessKey); } // Postback... if (String.IsNullOrEmpty(item.NavigateUrl)) { writer.AddAttribute(HtmlTextWriterAttribute.Href, "#"); writer.AddAttribute(HtmlTextWriterAttribute.Onclick, GetPostBackEventReference(item)); } // ...or direct link else { writer.AddAttribute(HtmlTextWriterAttribute.Href, Menu.ResolveClientUrl(item.NavigateUrl)); string target = item.Target; if (String.IsNullOrEmpty(target)) { target = Menu.Target; } if (!String.IsNullOrEmpty(target)) { writer.AddAttribute(HtmlTextWriterAttribute.Target, target); } } return false; } private void RenderItemPostSeparator(HtmlTextWriter writer, MenuItem item) { string separatorImageUrl = item.SeparatorImageUrl; if (String.IsNullOrEmpty(separatorImageUrl)) { separatorImageUrl = IsDepthStatic(item) ? Menu.StaticBottomSeparatorImageUrl : Menu.DynamicBottomSeparatorImageUrl; } if (!String.IsNullOrEmpty(separatorImageUrl)) { RenderItemSeparatorImage(writer, item, separatorImageUrl); } } private void RenderItemPreSeparator(HtmlTextWriter writer, MenuItem item) { string separatorImageUrl = IsDepthStatic(item) ? Menu.StaticTopSeparatorImageUrl : Menu.DynamicTopSeparatorImageUrl; if (!String.IsNullOrEmpty(separatorImageUrl)) { RenderItemSeparatorImage(writer, item, separatorImageUrl); } } private void RenderItemSeparatorImage(HtmlTextWriter writer, MenuItem item, string separatorImageUrl) { if (Menu.RenderingCompatibility >= VersionUtil.Framework45) { // Dev10 #867750, Dev11 #436206: We need to be consistent with other controls by calling ResolveClientUrl, // but we need to hide this behavior behind a compat switch so that upgrading to 4.5 doesn't break // customers who have implemented their own manual workaround. separatorImageUrl = Menu.ResolveClientUrl(separatorImageUrl); } writer.AddAttribute(HtmlTextWriterAttribute.Src, separatorImageUrl); writer.AddAttribute(HtmlTextWriterAttribute.Alt, String.Empty); writer.AddAttribute(HtmlTextWriterAttribute.Class, IsDepthStatic(item) && Menu.Orientation == Orientation.Horizontal ? "horizontal-separator" : "separator"); writer.RenderBeginTag(HtmlTextWriterTag.Img); writer.RenderEndTag(); } private void RenderItems(HtmlTextWriter writer, bool staticOnly, MenuItemCollection items, int level, bool needsAccessKey) { if (level == 1 || level > Menu.StaticDisplayLevels) { // Render a