44

I'm just trying out ASP.NET 4.5 bundling and minification, and ran into an issue.

I've got around 10 css files, of which 2 were originally referenced in the layout using the attribute media="screen".

Since the syntax for adding a css to the bundle does not let you specify that such attribute should be added (makes sense, since the attribute would apply for the whole bundle), I was hoping to see an overload of @Styles.Render that would allow me to specify html attributes, like in other Html helpers, but there is none.

There is an ugly solution, in which since I know the url of the bundle created, i could just craft the tag myself, but I'd lose the caching mechanism that is handled by ASP.NET by allowing it to render the tag itself.

Is there a way to do this, am I missing something? Or is this just an oversight of the design team?

Erik Philips
  • 53,428
  • 11
  • 128
  • 150
GR7
  • 5,083
  • 8
  • 48
  • 66

8 Answers8

77

I've found a more elegant solution.

I'm using the Styles.RenderFormat(format, bundle).

I have a BundlesFormats class with a property called PRINT and I use it like so:

public class BundlesFormats
{
    public const string PRINT = @"<link href=""{0}"" rel=""stylesheet"" type=""text/css"" media=""print"" />";
}

And in the cshtml:

@Styles.RenderFormat(BundlesFormats.PRINT, "~/bundles/Content/print")
Adam Tal
  • 5,911
  • 4
  • 29
  • 49
14

Well, it's an ugly hack, but hopefully the team will add a built-in way to do it in the next release.

This is how I solved it, maintaining the caching string and still being able to add the media attribute to the tag.

@{
    var cssMediaBundleUrl = BundleTable.Bundles.ResolveBundleUrl("~/stylesheets/mediacss", true);
}
<link href="@cssMediaBundleUrl" rel="stylesheet" type="text/css" media="screen" />

Guess I can turn this into an Html helper, will do that later and edit.

GR7
  • 5,083
  • 8
  • 48
  • 66
  • 29
    You can just do this: < link href="@Styles.Url("~/stylehseets/mediacss")" rel="stylesheet" type="text/css" media="screen" /> – Hao Kung Aug 20 '12 at 23:11
  • @GR7 After upgrading to VS 2013 Express For Web this start to produce 404 error. I posted it in http://stackoverflow.com/questions/20052278/how-to-specify-media-type-for-css-bundle-in-vs-express-2013-for-web How to fix ? – Andrus Nov 18 '13 at 17:04
  • @Andrus, have you tried any of the alternatives that other people have posted? I haven't revisited this code yet, but will certainly do soon, hopefully. – GR7 Nov 19 '13 at 06:16
  • No, I havent. Please confirm that your/Hao solution does not work in VS2013 then I start to look into other ways. In this case this should unmarked as answer – Andrus Nov 19 '13 at 07:53
6

Another option to solve this issue, without compromising the debug ability, could be:

public static IHtmlString Render(string path, IDictionary<string, object> htmlAttributes)
{
    var attributes = BuildHtmlStringFrom(htmlAttributes);

#if DEBUG
    var originalHtml = Styles.Render(path).ToHtmlString();
    string tagsWithAttributes = originalHtml.Replace("/>", attributes + "/>");
    return MvcHtmlString.Create(tagsWithAttributes);
#endif

    string tagWithAttribute = string.Format(
        "<link rel=\"stylesheet\" href=\"{0}\" type=\"text/css\"{1} />", 
        Styles.Url(path), attributes);

    return MvcHtmlString.Create(tagWithAttribute);
}

What I'm doing is just appending the given html attributes to the end of the tags (on debug mode) or to the end of the only link tag (when minification/bundling are enabled).

The usage in views:

@Bundles.Render("~/css/print", new { media = "print" })

The rest of the code:

public static IHtmlString Render(string path, object htmlAttributes)
{
    return Render(path, new RouteValueDictionary(htmlAttributes));
}

private static string BuildHtmlStringFrom(IEnumerable<KeyValuePair<string, object>> htmlAttributes)
{
    var builder = new StringBuilder();

    foreach (var attribute in htmlAttributes)
    {
        builder.AppendFormat(" {0}=\"{1}\"", attribute.Key, attribute.Value);
    }

    return builder.ToString();
}

I've wrote a blog post about this subject: http://danielcorreia.net/blog/quick-start-to-mvc4-bundling/

3

Unfortunately there isn't a great way to hook into how the tags are rendered currently, we thought about adding a hook so you could add your own method to render each script/style tag. It sounds like we do need to do that. Should be pretty simple to add, I'll create a work item to enable this scenario...

As a temporary workaround, if you are willing to lose the debug/release functionality that Styles.Render gives you, you can render a reference to the bundle using Styles.Url which would give you just the bundle url, you can embed that inside your own tag.

Hao Kung
  • 28,040
  • 6
  • 84
  • 93
  • Hao, I'm guessing you're a member of the ASP.NET team? It's weird that other Html Helpers do have overloads to allow the developer to set html attributes and the Bundle renderer methods dont. I'm thinking of hacking the generated tag (which would include the caching string) and just add the media attribute myself, that way i'd still have the caching and i can add it..i just thought it's really weird that this escaped the team. – GR7 Aug 20 '12 at 19:25
  • found a way to do it through the framework, although it's not nice. I'll post it in a bit – GR7 Aug 20 '12 at 19:34
  • Yes, Hao is the lead dev at MSFT for the optimization framework. – RickAndMSFT Aug 26 '12 at 17:47
2

Why not just use @media print? Check out http://www.phpied.com/5-years-later-print-css-still-sucks/

WizxX20
  • 311
  • 2
  • 6
0

Web Forms Solution

In BundleConfig.cs:

//Print css must be a separate bundle since we are going to render it with a media=print
Bundles.Add(new StyleBundle("~/bundles/printCSS").Include("~/Content/Print.css"));

Master Page:

<asp:Literal runat="server" ID="litCssPrint" />

Master Page Code File:

litCssPrint.Text = Styles.RenderFormat(@"<link href=""{0}"" rel=""stylesheet"" type=""text/css"" media=""print"" />", "~/bundles/printCSS").ToHtmlString();
Bolo
  • 1,494
  • 1
  • 19
  • 19
0

I took Adam Tal's suggestion a little further.

I'm probably over-coding it, but for readability, I created a static class to kind of mimic the Styles.Render format.

public static class StyleExtensions
{
    public enum Format
    {
        Async,
        Preload,
    }

    public static IHtmlString Render(string contentPath, Format format)
    {
        switch (format)
        {
            case Format.Async:
            return contentPath.ToAsyncFormat();
            case Format.Preload:
            return contentPath.ToPreloadFormat();
            default:
            return new HtmlString(string.Empty);
        }
    }

    public static IHtmlString RenderAsync(string contentPath)
    {
        return contentPath.ToAsyncFormat();
    }

    public static IHtmlString RenderPreload(string contentPath)
    {
        return contentPath.ToPreloadFormat();
    }

    public static IHtmlString ToAsyncFormat(this string contentPath)
    {
        return Styles.RenderFormat("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\" media=\"print\" onload=\"this.media='all';this.onload=null;\">", contentPath);
    }

    public static IHtmlString ToPreloadFormat(this string contentPath)
    {
        return Styles.RenderFormat("<link rel=\"preload\" href=\"{0}\" as=\"style\" onload=\"this.rel='stylesheet';this.onload=null;\">", contentPath);
    }
}

I would probably erase either the direct constructor or the enum constructor, and you could really put the string extension inside the method too, depending on whatever makes more sense to you, but you'd call it either of these ways accordingly:

@StyleExtensions.Render("~/Content/bundle-bootstrap", StyleExtensions.Format.Preload)
@StyleExtensions.Render("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&display=swap", StyleExtensions.Format.Preload)
@StyleExtensions.Render(Url.Content("~/Content/Styles/Primary.min.css"), StyleExtensions.Format.Async)

or

@StyleExtensions.RenderPreload("~/Content/bundle-bootstrap")
@StyleExtensions.RenderPreload("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&display=swap")
@StyleExtensions.RenderAsync(Url.Content("~/Content/Styles/Primary.min.css"))
-1

So complicated, why not to use:

bundles.Add<StylesheetBundle>("~/Css/site.css", b => b.Media = "screen");

?

KeT4yn
  • 277
  • 3
  • 2