MVC4 bundling javascript or css files from a different domain? - c#

In MVC4's Web.Optimization bundling / minimization, is it possible to register a bundle on one site (our static cookieless domain) and then use that bundle on another site (our webapp domain)?
eg static.myapp.com has a BundleConfig.cs with
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/Scripts/static")
.Include("~/Scripts/*.js"));
}
Can that bundle be used in a view on the webapp's domain, eg www.myapp.com has this in Site.Master
<%= Scripts.Render("static.myapp.com/Scripts/static") %>
Can this be done with MVC4 bundling? Serving static files from a cookieless static domain is a well known performance improvement.

Bundling in ASP.net MVC allows you to optimize the deployment of scripts and stylesheets by replacing the placeholder Scripts.Render() at Runtime rather than at Design time. When the page is parsed and pushed to the client, the bundles registered to the calling server are parsed into the output buffer. Therefore the app serving the content must be running the bundler service. If a web app not running bundling encountered the Scripts.Render() element, it would either output null or throw an exception.
you can, however, use CDN references in your RegisterBundles method, like:
bundles.UseCdn = true; //enable CDN support
//add link to jquery on the CDN
var jqueryCdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";
bundles.Add(new ScriptBundle("~/bundles/jquery",
jqueryCdnPath).Include(
"~/Scripts/jquery-{version}.js"));
In the code above, jQuery will be requested from the CDN while in release mode and the debug version of jQuery will be fetched locally in debug mode. When using a CDN, you should have a fallback mechanism in case the CDN request fails.
Edit
You could use ASP.Net MVC to serve as a CDN from static.myapp.com something like
routes.MapRoute(
"CDN",
"cdn",
new { controller = "Webpage", action = "Fetch" }
);
[OutputCache(Duration=300, Location=OutputCacheLocation.Any)]
public ActionResult Fetch()
{
return new FileStreamResult(
GetScriptBundle(Request.QueryString["url"]),
"text/javascript");
}

So I just ran into this requirement and I solved it neatly in the page effectively similar to this;
<%
string jsStore = Context.IsDebuggingEnabled ? "~" : "static.mydomain.com";
string scriptTagFormat = Scripts.DefaultTagFormat.Replace("{0}", Url.Content(jsStore).TrimEnd('/') + "{0}");
%>
and
<%=Scripts.RenderFormat(scriptTagFormat, "~/bundles/allMyJS")%>
Of course you could also use DefaultTagFormat in RegisterBundles() but this offered more flexibility because;
You can read jsStore from alternate web.configs for different environments.
You can modify jsStore at runtime to match different {SERVER_NAME} hosts if you have dev, test sites etc.
You can also use it with static tags like this;
<script src='#Url.Content(jsStore + "/scripts/etc/lt-ie10.min.js")'></script>
The same approach can be used for the CSS.

Yes, see my answer here: How to make bundles unminify and list individual files when using a cookieless static content server?
Not only can you reference a different domain, but if you use a custom VirtualPathProvider like the one I provided, you can also keep the ability to list the files individually while in DEBUG mode.
Besides being easier to debug in the browser, you also won't have to rebuild to update the bundles every time you make a JS or CSS change while developing (just when adding or removing files).

Related

ASP.Net MVC dynamic script bundle does not load and gives 404 error first few times

I am working on an existing ASP.Net MVC web application on .Net 4.5.2 and MVC 5.2. This has dynamic script bundles on 250 odd views which are added to the BundleTable only when the particular view is first accessed. This leads to an issue where the first time any view is hit, the script bundle returns a 404 error. But refreshing the page few times resolves the issue - at which point the bundle loads up fine and then gets cached on the browser.
The code for this looks like:
//View:
#model XYZ
#{ ViewBag.Title = "Title"; }
#section JavaScript {
#Html.RenderScripts("~/Scripts/a.js", "~/Scripts/b.js")
}
// HtmlHelper RenderScripts extension
public static IHtmlString RenderScripts(this HtmlHelper helper, bool isMinified, params string[] filePaths)
{
var path = (helper.ViewDataContainer as WebPageBase).VirtualPath.Replace("~/", "~/Scripts/").Replace(".cshtml", "").Replace("../", "");
var bundle = new ScriptBundle(path).Include(filePaths);
bundle.Orderer = _orderer;
if (!isMinified) bundle.Transforms.Clear();
BundleTable.Bundles.Add(bundle);
return Scripts.Render(path);
}
It appears that the 1st request might be adding the bundle, but there's a lag in generating and serving up the bundle itself which is resulting in the 404 error. So how soon can a bundle be used once it's added - does it need a warm up?
Is this a wrong pattern of using Script bundles? Is there a way to address this so I can continue to declare the view-specific script bundles on the view without moving them to the bundle config?

Jquery all ajax calls not picking relative url - ASP.NET MVC

$(document).ready(function () {
getDefaultPDF();
loadPDF();
});
function loadPDF() {
$('#reportsDiv').load("/Review/DisplayPdfPartial");
}
Our site is hosted under the Default website, within a folder. So the url should be
http://servername/foldername/Review/DisplayPdfPartial
but the following code tries to fetch
http://servername/Review/DisplayPdfPartial - doesn't add the foldername and fails obviously.
This doesn't happen on local, only when deployed under default website.
What am I missing?
As you have mentioned you are using Asp.Net MVC then in that case instead of specifying url's this way, a more efficient way is to use #Url.Action() helper method as shown :-
$(document).ready(function () {
getDefaultPDF();
loadPDF();
});
function loadPDF() {
//$('#reportsDiv').load("/Review/DisplayPdfPartial");
$('#reportsDiv').load('#Url.Action("DisplayPdfPartial","Review")');
}
You can use ResolveClientUrl
A fully qualified URL to the specified resource suitable for use on
the browser.
Use the ResolveClientUrl method to return a URL string suitable for
use by the client to access resources on the Web server, such as image
files, links to additional pages, and so on
.

CSS Bundles with Controller Route Source

I created a controller path to return a css file, which works, and returns Response.ContentType = "text/css".
Now I'm trying to put that URL in my bundles file, like this:
bundles.Add(new StyleBundle("~/Content/custom").Include(
"~/CSS/Custom/1"
));
NOTE: /CSS/Custom/1 is a route that returns a text/css file.
In my view I have:
#Styles.Render("~/Content/custom")
When I build the project, the bundler returns this in my HTML:
<link href="/Content/custom?v=" rel="stylesheet"/>
When I view the files source, It's empty.
How do I get this to work?
Not sure why you'd want to do this
After taking a look at it in reflector, during the bundling process it will take the virtual path to the item and check to make sure the file exists. (Code below) So bundling without CDN absolutely requires a file, not an application route.
if ((this.VirtualPathProvider == null) || this.VirtualPathProvider.FileExists(virtualPath))
{
base.Add(new BundleItem(virtualPath, transforms));
}
CDNs never hit this line, they take a different path.
bundles.UseCdn = true;
bundles.Add(new StyleBundle("~/Content/custom", "/CSS/Custom/1"));
BundleTable.EnableOptimizations = true;
Out of the box, you can only bundle physical files. If you need the CSS to be dynamically generated for some reason, you might consider creating a custom bundle. But if it were me, I'd just leave this as a separate download.

Using CDN in ASP.NET MVC bundles

I read the article about bundling and monification, specially about using CDN, but there are some things unclear to me.
Having the example :
public static void RegisterBundles(BundleCollection bundles)
{
//bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
// "~/Scripts/jquery-{version}.js"));
bundles.UseCdn = true; //enable CDN support
//add link to jquery on the CDN
var jqueryCdnPath =
"http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";
bundles.Add(new ScriptBundle("~/bundles/jquery",
jqueryCdnPath).Include(
"~/Scripts/jquery-{version}.js"));
// Code removed for clarity.
}
Is there a possibility to use the {version} format of CDN references, like for the "local" ones?
What is the point of including in the bundles the already minified version of the script, like jquery-1.7.1.min.js? What if it does not exist? Should it not search if the .min file exist and/or generate it respectively?
using System.Web;
using System.Web.Optimization;
namespace MvcApp
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery", "https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js").Include("~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap","https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js").Include("~/Scripts/bootstrap.js"));
bundles.Add(new StyleBundle("~/Content/css", "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css").Include("~/Content/bootstrap.css"));
BundleTable.EnableOptimizations = true;
bundles.UseCdn = true;
}
}
}
What a lot of developers don't realized is that there is an overload for class constructor of ScriptBundle and StyleBundle, which takes two string parameters, for example for the ScriptBundle it would be ScriptBundle(string, string) and for the StyleBundle it would be StyleBundle(string, string). The first parameter is the virtual path and the second parameter is the cdnPath.
We might be asking yourself, if it takes two parameters, how does MVC know which one to use? Well, the cdn location is used only when the BundleTable.EnableOptimizations property is set to true.
Setting the EnableOptimization property to true tells MVC to use the use the minified version of the file instead of the regular version.
When this property is set to true, and the cdn path is present MVC will use the cdn path instead of the local virtual path.
There is one more property you have to set to true and that is the bundles.UseCdn.
This tells MVC to use the cdn location instead of the local version. If the BundleTable.EnableOptimization is set to false, then the local version is used automatically as a fall back because the cdn version is the minified version.
Read this blog its clear about your think:
http://www.techjunkieblog.com/2015/06/aspnet-mvc-5-configure-bundleconfig.html
You can't to my knowledge. But you can keep a table of cdns and populate when the bundles are loaded. When a new version comes out you wish to use, add/replace entry in the db.
//get from db
List<string> cdns = new List<string>();
foreach (string cdn in cdns)
{
bundles.Add(new ScriptBundle("~/bundles/jquery",cdn).Include("~/Scripts/jquery-{version}.js"));
}
I agree on the min part. For the doesn't exist part of the question, scroll down and read about "Using a CDN". There's an example to show how to check. You essentially need to have a local copy as backup of you can reference another cdn I suppose.
Is there a possibility to use the {version} format of CDN references,
like for the "local" ones?
The {version} placeholder is primarily for saving time typing the explicit number so that the build could look for files on local disk. Since the same search could not be done on a remote server, you will need to specify an exact URL explicitly.
What is the point of including in the bundles the already minified
version of the script, like jquery-1.7.1.min.js? What if it does not
exist?
The key benefit to go with this bundling syntax is to conditionally switch between different URLs for script and style tags in the final HTML.
When a requested file does not exist, the bundling process will skip it.
Should it not search if the .min file exist and/or generate it
respectively?
Yes, it applies minification before bundling as you can see:

Dynamic URL route

This app has several routes configured in RouteConfig.cs. For instance, I have the two following routes defined:
routes.MapRoute(
name: "MyPage-Demo",
url: "pages/page-title/demo",
defaults: new { controller = "Root", action = "PageDemo" }
);
routes.MapRoute(
name: "MyPage",
url: "pages/page-title/{resource}",
defaults: new { controller = "Root", action = "Page", resource = UrlParameter.Optional }
);
Each page someone visits has a link to a "demo". A page could be accessed by visiting http://localhost/pages/page-title. This works fine.
When a user clicks the "demo" link, they are redirected to a page located at http://localhost/pages/page-title/demo. This works fine.
My problem is the demo page may reference a complex nested structure. The structure consists of JavaScript, css, images, etc. Content used for the purpose of the demo. None of these nested resources can be found. However, I'm not sure how to setup my routing to account for these nested files.
I'm confident I'm going to need to update my controller's PageDemo action. However, I'm not sure
a) how to do so in a way that will allow for differing structures and
b) how to update my route configuration to account for these nested structures.
Is there a way to do this? In reality, I'm going to have multiple pages and multiple demos. For that reason, I want to have something a little more reusable than a hard-coded approach.
If you just need to serve files physically stored in a path, you should be able to just ignore the route, e.g.:
routes.IgnoreRoute("pages/page-title/demo/resources/{*resource}");
That will bypass MVC trying to route the request to a controller.
Or you could go by file extension:
routes.IgnoreRoute("{file}.js");
routes.IgnoreRoute("{file}.css");
(Code is untested, but it looks like you're trying to do something similar here :)
https://stackoverflow.com/a/3112192/486620
IF I understand:
The problem seems to be that your MyPage-Demo route:
routes.MapRoute(
name: "MyPage-Demo",
url: "pages/page-title/demo",
defaults: new { controller = "Root", action = "PageDemo" }
);
is NOT {resource} specific, while your MyPage route IS.
If you change your route to take a {resource}
routes.MapRoute(
name: "MyPage-Demo",
url: "pages/page-title/demo/{resource}",
defaults: new { controller = "Root",
action = "PageDemo", resource = UrlParameter.Optional });
Then your action method can
return specific Views with proper resource settings
set a Viewbag property with path to your specific resource
If this is inline with your intent, these routes can be consolidated into
routes.MapRoute(
name: "MyPage-Demo",
url: "pages/{action}/{resource}",
defaults: new { controller = "Root",
action = "PageDemo", resource = UrlParameter.Optional });
/pages/PageDemo/{resource} resolves to Controller=pages, action = PageDemo
/pages/demo/{resource} resolves to Controller=pages, action = demo.
This convention allows you flexibility to create more {resource} dependant links
In the Browser, Right Click Demo page => Choose View Page Source.
Here, you have the link for the CSS and Js files in your Demo page. Click on those js/css file links. Check if there are redirecting you to the correct/expected location. Otherwise you could make the Css/Js file URL accordingly Because, as per the demo page each PageDemo will have its own unique structure of JS/Images/css, etc
How are you referencing your JS and CSS files ?
If you use the tilde character like : ~/Content/Styles/Site.css you won't have any problem no matter where you are in your virtual path.
Also not 100% sure I am directly answering your question, but making the assumption that the resources you are trying to access are nested in a folder structure that mirrors the page structure - and the issue you are having is how to ignore the routes to these without having to know what they might be in advance?
This does a good job of explaining that: https://stackoverflow.com/a/30551/1803682
I would ask:
As #PKKG notes in his answer - do the links in the page source match what you expect?
How is this per-demo content served: e.g. by a service and not a static file?
this answer contains two approaches. the second one may be more suitable for your scenario. the first may be more suitable for a general mvc project
approach one
i suggest creating a organized structure in your content folder to store the scripts and css files, ie
/Content/Demos/Page-Title-1/
/Content/Demos/Page-Title-2/
/Content/Demos/Page-Title-3/
and
/Content/Demos/Common/
and then make a bundle to render the scripts and css files for each page title
ie.
bundles.Add(new StyleBundle("~/Demo/page-title/css").Include(
"~/Content/Demos/Page-Title-1/csscontent1.css",
"~/Content/Demos/Page-Title-1/csscontent2.css",
"~/Content/Demos/Page-Title-1/csscontent3.css",
"~/Content/Demos/Page-Title-1/csscontent4.css"));
bundles.Add(new StyleBundle("~/Demo/page-title/js").Include(
"~/Content/Demos/Page-Title-1/jscontent1.css",
"~/Content/Demos/Page-Title-1/jscontent2.css",
"~/Content/Demos/Page-Title-1/jscontent3.css",
"~/Content/Demos/Page-Title-1/jscontent4.css"));
this will allow you to render the scripts on the demo page using a few line approach, ie.
#Styles.Render("~/Demo/page-title/css");
#Scripts.Render("~/Demo/page-title/jss");
#Styles.Render("~/Demo/common/css");
#Scripts.Render("~/Demo/common/css");
you will have to update the files in global .asax as you change the files in your /Content/Demos/Page-Title/ folder.
there is the benefit that if you choose, you may bundle and minify the files to save bandwidth and load time for the first page load.
approach two.
(still use the following folder structure
/Content/Demos/Common/
and
/Content/Demos/Page-Title-1/
/Content/Demos/Page-Title-2/
/Content/Demos/Page-Title-3/)
make an html helper to reference all the scripts & contents in a folder
its usage would be
#Asset.RenderAssets( '~/folderdirectory')
and the helper would do something like
#helper RenderAssets (stirng directory){
#* scrape the directory for all script files*
var scripts = find all scripts in the directory
#* include the script files *#
for each script
<script src=" ... .js"></script>
#* scrape the directory for all cssfiles*
var styles = all css in the directory
#* include the css files *#
for each style
<link rel='stylesheet' type="text/css" href=" ... .css">
}
this would be a few line usage in each demo view
#Asset.RenderAssets( '~/Content/Demos/Common')
#Asset.RenderAssets( '~/Content/Demos/Page-Title')
you may or may not need to pair this with an extra few line or two in your global.asax or RouteConfig.cs file (see source 3)
routes.IgnoreRoute("/Content/Demos/{page}/{script}.js");
routes.IgnoreRoute("/Content/Demos/{page}/{style}.css");
relevant sources
to create html helpers see
http://weblogs.asp.net/scottgu/archive/2011/05/12/asp-net-mvc-3-and-the-helper-syntax-within-razor.aspx
to use bundling and minifcation (the scripts.render approach) see
http://www.asp.net/mvc/tutorials/mvc-4/bundling-and-minification
phill haakk says may not need to pair this with an ignore route!
https://stackoverflow.com/a/30551/1778606
commentary and edits are encouraged.
All static content (.js, .css, .html, .png) is not seen by MVC (unless modules/runAllManagedModulesForAllRequests is set to true in web.config). Static content extensions are defined in IIS configuration "module mapping", and is using the StaticFileHandler module (and not the .NET module).
So static content must be referenced by its physical path relative to the current path (the path of the current html page).
The best solution is to use absolute link from the root of the website. Like /content/demo1/demo1.html, put all js,css in /content/demo1/, and in demo1.html use path relative to the /content/demo1/ folder (where the .html is). Ie: with demo1.css being in the same folder.
The link to demo1.html would be demo 1

Categories