如何使用 StylePlaceHolder 和 Style 控件控制 ASP.NET 主题中的样式表

How to take control of style sheets in ASP.NET Themes with the StylePlaceHolder and Style control

提问人:Egil Hansen 提问时间:11/30/2008 最后编辑:Steve BertanEgil Hansen 更新时间:12/4/2015 访问量:8773

问:

更新:这变成了一篇博客文章,其中包含更新的链接和代码,在我的博客上:https://egilhansen.com/2008/12/01/how-to-take-control-of-style-sheets-in-asp-net-themes-with-the-styleplaceholder-and-style-control/


问题很简单。使用 ASP.NET 主题时,您对样式表如何呈现到页面没有太多发言权。

渲染引擎使用<link href=“...” 按字母顺序添加主题文件夹中的所有样式表表示法。

我们都知道样式表的顺序很重要,幸运的是,可以通过在样式表前面加上 01、02、...、99 来规避 asp.nets 的缺点,从而强制执行所需的顺序(有关更多信息,请参见 Rusty Swayne 关于该技术的博客文章)。

如果您使用重置样式表,这一点尤其重要,我强烈建议这样做;它使跨浏览器以一致的形式设置网站样式变得更加容易(看看 Eric Meyer 的 Reset Reloaded)。

您还错过了指定媒体类型(例如屏幕、打印、投影、盲文、语音)的可能性。如果您更喜欢使用 @import 方法包含样式表,那么您也会被冷落。

另一个缺少的选项是条件注释,如果您使用“ie-fix.css”样式表,这将特别有用。

在我解释 StylePlaceholder 和 Style 控件如何解决上述问题之前,我的解决方案受到 Per Zimmerman 关于该主题的博客文章的启发。

StylePlaceHolder 控件位于母版页或母版页的标题部分。它可以承载一个或多个样式控件,默认情况下将删除呈现引擎添加的样式,并添加自己的样式(它只会删除从当前活动主题添加的样式)。

Style 控件既可以承载其开始标记和结束标记之间的内联样式,也可以通过其 CssUrl 属性承载对外部样式表文件的引用。使用其他属性,可以控制样式表呈现给页面的方式。

让我举个例子。考虑一个简单的网站项目,其中包含母版页和具有三个样式表的主题 - 01reset.css、02style.css、99iefix.css。注意:我使用前面描述的前缀技术来命名它们,因为它可以带来更好的设计时体验。此外,自定义控件的标记前缀为“ass:”。

在母版页的标题部分中,添加:

<ass:StylePlaceHolder ID="StylePlaceHolder1" runat="server" SkinID="ThemeStyles" />

在主题目录中,添加一个皮肤文件(例如 Styles.skin)并添加以下内容:

<ass:StylePlaceHolder1runat="server" SkinId="ThemeStyles">
    <ass:Style CssUrl="~/App_Themes/Default/01reset.css" />
    <ass:Style CssUrl="~/App_Themes/Default/02style.css" />
    <ass:Style CssUrl="~/App_Themes/Default/99iefix.css" ConditionCommentExpression="[if IE]" />
</ass:StylePlaceHolder1>

基本上就是这样。Style 控件上还有更多可用于控制呈现的属性,但这是基本设置。有了这个,您可以轻松地添加另一个主题并替换所有样式,因为您只需要包含不同的皮肤文件。

现在来看看实现这一切的代码。我必须承认,设计时体验有一些怪癖。这可能是因为我不太精通编写自定义控件(实际上,这两个是我第一次尝试),所以我非常希望对以下内容进行输入。在我正在开发的当前基于 WCAB/WCSF 的项目中,我在 Visual Studios 设计视图中看到了这样的错误,我不知道为什么。该网站编译,一切都在线运行。

Visual Studio http://www.egil.dk/wp-content/styleplaceholder-error.jpg 中的设计时错误示例

下面是 StylePlaceHolder 控件的代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;

[assembly: TagPrefix("Assimilated.Extensions.Web.Controls", "ass")]
namespace Assimilated.WebControls.Stylesheet
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [DefaultProperty("SkinID")]
    [ToolboxData("<{0}:StylePlaceHolder runat=\"server\" SkinID=\"ThemeStyles\"></{0}:StylePlaceHolder>")]
    [ParseChildren(true, "Styles")]
    [Themeable(true)]
    [PersistChildren(false)]
    public class StylePlaceHolder : Control
    {
        private List<Style> _styles;

        [Browsable(true)]
        [Category("Behavior")]
        [DefaultValue("ThemeStyles")]
        public override string SkinID { get; set; }

        [Browsable(false)]
        public List<Style> Styles
        {
            get
            {
                if (_styles == null)
                    _styles = new List<Style>();
                return _styles;
            }
        }

        protected override void CreateChildControls()
        {
            if (_styles == null)
                return;

            // add child controls
            Styles.ForEach(Controls.Add);
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            // get notified when page has finished its load stage
            Page.LoadComplete += Page_LoadComplete;
        }

        void Page_LoadComplete(object sender, EventArgs e)
        {
            // only remove if the page is actually using themes
            if (!string.IsNullOrEmpty(Page.StyleSheetTheme) || !string.IsNullOrEmpty(Page.Theme))
            {
                // Make sure only to remove style sheets from the added by
                // the runtime form the current theme.
                var themePath = string.Format("~/App_Themes/{0}",
                                              !string.IsNullOrEmpty(Page.StyleSheetTheme)
                                                  ? Page.StyleSheetTheme
                                                  : Page.Theme);

                // find all existing stylesheets in header
                var removeCandidate = Page.Header.Controls.OfType<HtmlLink>()
                    .Where(link => link.Href.StartsWith(themePath)).ToList();

                // remove the automatically added style sheets
                removeCandidate.ForEach(Page.Header.Controls.Remove);
            }
        }

        protected override void AddParsedSubObject(object obj)
        {
            // only add Style controls
            if (obj is Style)
                base.AddParsedSubObject(obj);
        }

    }
}

以及 Style 控件的代码:

using System.ComponentModel;
using System.Security.Permissions;
using System.Web;
using System.Web.UI;

[assembly: TagPrefix("Assimilated.Extensions.Web.Controls", "ass")]
namespace Assimilated.WebControls.Stylesheet
{
    [AspNetHostingPermission(SecurityAction.Demand, Level = AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
    [DefaultProperty("CssUrl")]
    [ParseChildren(true, "InlineStyle")]
    [PersistChildren(false)]
    [ToolboxData("<{0}:Style runat=\"server\"></{0}:Style>")]
    [Themeable(true)]
    public class Style : Control
    {
        public Style()
        {
            // set default value... for some reason the DefaultValue attribute do
            // not set this as I would have expected.
            TargetMedia = "All";
        }

        #region Properties

        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue("")]
        [Description("The url to the style sheet.")]
        [UrlProperty("*.css")]
        public string CssUrl
        {
            get; set;
        }

        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue("All")]
        [Description("The target media(s) of the style sheet. See http://www.w3.org/TR/REC-CSS2/media.html for more information.")]
        public string TargetMedia
        {
            get; set;
        }

        [Browsable(true)]
        [Category("Style sheet")]
        [DefaultValue(EmbedType.Link)]
        [Description("Specify how to embed the style sheet on the page.")]
        public EmbedType Type
        {
            get; set;
        }

        [Browsable(false)]
        [PersistenceMode(PersistenceMode.InnerDefaultProperty)]
        public string InlineStyle
        {
            get; set;
        }

        [Browsable(true)]
        [Category("Conditional comment")]
        [DefaultValue("")]
        [Description("Specifies a conditional comment expression to wrap the style sheet in. See http://msdn.microsoft.com/en-us/library/ms537512.aspx")]
        public string ConditionalCommentExpression
        {
            get; set;
        }

        [Browsable(true)]
        [Category("Conditional comment")]
        [DefaultValue(CommentType.DownlevelHidden)]
        [Description("Whether to reveal the conditional comment expression to downlevel browsers. Default is to hide. See http://msdn.microsoft.com/en-us/library/ms537512.aspx")]
        public CommentType ConditionalCommentType
        {
            get; set;
        }

        [Browsable(true)]
        [Category("Behavior")]
        public override string SkinID { get; set; }

        #endregion

        protected override void Render(HtmlTextWriter writer)
        {            
            // add empty line to make output pretty
            writer.WriteLine();

            // prints out begin condition comment tag
            if (!string.IsNullOrEmpty(ConditionalCommentExpression))
                writer.WriteLine(ConditionalCommentType == CommentType.DownlevelRevealed ? "<!{0}>" : "<!--{0}>",
                                 ConditionalCommentExpression);

            if (!string.IsNullOrEmpty(CssUrl))
            {               
                // add shared attribute
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css");

                // render either import or link tag
                if (Type == EmbedType.Link)
                {
                    // <link href=\"{0}\" type=\"text/css\" rel=\"stylesheet\" media=\"{1}\" />
                    writer.AddAttribute(HtmlTextWriterAttribute.Href, ResolveUrl(CssUrl));
                    writer.AddAttribute(HtmlTextWriterAttribute.Rel, "stylesheet");
                    writer.AddAttribute("media", TargetMedia);
                    writer.RenderBeginTag(HtmlTextWriterTag.Link);
                    writer.RenderEndTag();
                }
                else
                {
                    // <style type="text/css">@import "modern.css" screen;</style>
                    writer.RenderBeginTag(HtmlTextWriterTag.Style);
                    writer.Write("@import \"{0}\" {1};", ResolveUrl(CssUrl), TargetMedia);
                    writer.RenderEndTag();
                }
            }

            if(!string.IsNullOrEmpty(InlineStyle))
            {
                // <style type="text/css">... inline style ... </style>
                writer.AddAttribute(HtmlTextWriterAttribute.Type, "text/css");
                writer.RenderBeginTag(HtmlTextWriterTag.Style);
                writer.Write(InlineStyle);
                writer.RenderEndTag();
            }

            // prints out end condition comment tag
            if (!string.IsNullOrEmpty(ConditionalCommentExpression))
            {
                // add empty line to make output pretty
                writer.WriteLine();
                writer.WriteLine(ConditionalCommentType == CommentType.DownlevelRevealed ? "<![endif]>" : "<![endif]-->");
            }
        }
    }

    public enum EmbedType
    {        
        Link = 0,
        Import = 1,
    }

    public enum CommentType
    {
        DownlevelHidden = 0,
        DownlevelRevealed = 1
    }
}

那么你们怎么看呢?这是解决 asp.net 主题问题的好方法吗?那么代码呢?我真的很想对此提出一些意见,尤其是在设计时体验方面。

我上传了包含该项目的 Visual Studio 解决方案的压缩版本,以防有人感兴趣。

最好的问候,埃吉尔。

asp.net css 主题 自定义服务器控件

评论


答:

2赞 Egil Hansen 12/6/2008 #1

找到了我自己问题的答案。

我在设计模式下出现渲染错误的原因是 Visual Studio SP1 中的一个明显错误,Microsoft 尚未修复。

因此,上述代码在设计模式下也能按预期工作,只要您只将自定义控件包含在预编译的程序集中,而不是通过同一解决方案中的另一个项目。

有关如何以及原因的更详细说明,请参阅上面的链接。

0赞 Simon_Weaver 1/21/2009 #2

工作非常顺利。

对于像我这样从不记得 <% 标记语法的人,您需要将以下内容添加到母版页定义和外观文件的顶部以注册命名空间。

<%@ Register TagPrefix="ass" Namespace="Assimilated.WebControls.Stylesheet" %>

我不确定我是否希望在我的代码中包含那么多“屁股”,但除此之外我喜欢它。

哦,如果这真的是你的第一个自定义控件,那就太棒了。我知道它受到其他人代码的启发,但它至少看起来具有所有正确的属性和接口。

评论

0赞 Egil Hansen 1/27/2009
谢谢西蒙的客气话。这确实是我的第一个自定义控件,但现在已经重构了几次,所以第一个自定义控件,而不是第一次尝试:)
0赞 Simon_Weaver 1/28/2009
@egil有趣的是,第二天我就切换到了 MVC——部分原因是我厌倦了不得不去寻找像你这样聪明的解决方案来解决简单的问题。幸运的是,我正在开发一个新网站,所以我被允许奢侈地玩 MVC,我真的很喜欢它
0赞 Rosco 12/21/2009 #3

回复:使用特定的媒体CSS文件,可以使用@media CSS语句,工作正常。