I use a jQuery library for Google Maps, and it depends on the Google scripts to be loaded first. I'd like to be able to include both in the bundle as such:
bundles.Add(new ScriptBundle("myfoobundle").Include(
"http://maps.googleapis.com/maps/api/js?sensor=false&libraries=places",
"~/scripts/jquery.fooplugin-{version}.js"
));
This doesn't seem to work (throws an exception complaining about the first string). And one may say that this shouldn't work because that absolute URL is not meant to be minified/bundled.
But the current approach is a hassle, as I need to ensure that the dependencies are correct, and that happens in different places (half the problem in the bundling code, the other half in the view).
Would be nice to have a 1-step solution as above. Do I have any options in this regard?
UPDATE:
To address the comments regarding using a CDN as a solution: if I specify bundles.UseCdn = true it has no effect, and I still get the exception The URL 'http://maps.googleapis.com/maps/api/js?sensor=false&libraries=places' is not valid. Only application relative URLs (~/url) are allowed. Also I'm unsure what the implication of doing that is in the first place, because I already use CDN support for jQuery, etc., so unsure how that would conflict with my use case.
If you are using a version of System.Web.Optimization >= 1.1.2, there is a new convenient way of overriding the url's for Styles and Scripts. In the example below, I am grabbing a CdnBaseUrl from web.config to use as the base url for all scripts and stylesheets:
public class BundleConfig
{
private static readonly string BaseUrl = ConfigurationManager.AppSettings["CdnBaseUrl"];
public static void RegisterBundles(BundleCollection bundles)
{
// This is the new hotness!!
Styles.DefaultTagFormat = "<link href=\"" + BaseUrl + "{0}\" rel=\"stylesheet\"/>";
Scripts.DefaultTagFormat = "<script src=\"" + BaseUrl + "{0}\"></script>";
bundles.Add(new ScriptBundle("~/bundles/js").Include(
"Your scripts here..."
));
bundles.Add(new StyleBundle("~/bundles/css").Include(
"Your css files here..."
));
}
}
More info on static site (CDN) optimization
Currently you would have to include a local copy of the jquery that you are depending on inside of the bundle, or you would have to manage the script tags as you mention. We are aware of this kind of depedency management issue and it falls under the category of asset management which we are tracking with this work item on codeplex
Based on the MVC tutorials, your syntax is incorrect for creating a bundle from a CDN. And as others have said, ensure that you have the bundles.UseCdn = true; property set. Using the example on the MVC site - your code should reflect the following:
public static void RegisterBundles(BundleCollection bundles)
{
bundles.UseCdn = true; //enable CDN support
//add link to jquery on the CDN
var jqueryCdnPath = "http://maps.googleapis.com/maps/api/js?sensor=false&libraries=places";
bundles.Add(new ScriptBundle("myfoobundle", jqueryCdnPath).Include(
"~/Scripts/jquery-{version}.js"));
}
If it is just a matter of getting absolute url in bundle then you can go for this.
public static class Extensions
{
public static IHtmlString RenderScript(this UrlHelper helper, params string[] paths)
{
string scripts = System.Web.Optimization.Scripts.Render(paths).ToHtmlString();
string hostName = HttpContext.Current.Request.Url.Scheme + Uri.SchemeDelimiter + HttpContext.Current.Request.Url.Authority;
string replaced = Regex.Replace(scripts, "src=\"/", "src=\"" + hostName + "/", RegexOptions.Multiline | RegexOptions.IgnoreCase);
return new HtmlString(replaced);
}
}
This will basically take the bahvior from Scripts.Render and then apply absolute urls to it. Then in the view you have to write
#Url.RenderScript("~/bundles/jquery")
instead of
#Scripts.Render("~/bundles/jquery")
Enjoy coding!!...
I tried this as suggested and it didn't work:
string googleMapsApiCDN = "http://maps.google.com/maps/api/js?sensor=false&language=en";
bundles.Add(new ScriptBundle("~/bundles/gmap3", googleMapsApiCDN).Include(
"~/Scripts/GMap3/gmap3.min.js", // GMap3 library
"~/Scripts/GMap3/mygmap3-about.js" // Pops up and configures
GMap3 on About page
));
The mygmap3-about.js script was rendered but the gmap3.min.js and the CDN script from google where both excluded.
Have you tried enabling CDN support and seeing if that allows the absolute URL to work:
bundles.UseCdn = true;
Related
Is it possible to change the rendered output of a ScriptBundle in ASP.NET MVC? When configuring the bundles with EnableOptimizations = false, the output for each script included in the bundle is something like this:
<script src="~/Scripts/path/to/script"></script>
I would like to change this "template" based on the ScriptBundle (for all bundles would also be fine). Is there a way to change this?
Have a look at the below code, which will always give fresh file.
using System.IO;
using System.Web;
using System.Web.Hosting;
using System.Web.Optimization;
namespace TestProj
{
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/common").Include("~/Scripts/CommonScripts.js").WithLastModifiedToken());
BundleTable.EnableOptimizations = false;
}
}
internal static class BundleExtensions
{
public static Bundle WithLastModifiedToken(this Bundle sb)
{
sb.Transforms.Add(new LastModifiedBundleTransform());
return sb;
}
public class LastModifiedBundleTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
foreach (var file in response.Files)
{
var lastWrite = File.GetLastWriteTime(HostingEnvironment.MapPath(file.IncludedVirtualPath)).Ticks.ToString();
file.IncludedVirtualPath = string.Concat(file.IncludedVirtualPath, "?v=", lastWrite);
}
}
}
}
}
The output will be
"/Scripts/CommonScripts.js?v=636180193140000000"
Here i am adding the last modified date of the file as query parameter. So whenever the file changes browser will get the fresh file all the time. Or instead of last updated time you can add version like '1.0.0' in the query parameter.
I wrestled with MVC bundling for a long time too. It was great to begin with, but you start to fight a losing battle with it when you need to do anything out of the ordinary (like your question).
I'm sorry this doesn't directly answer your question, but I moved to WebPack because of issues like this, and have never looked back.
https://webpack.js.org
I am using .LESS variables in my files. I have a LessTransform in my Bundler, which allows all my .less to see the variables. But when I turn bundling off, obviously it no longer works!
Can I see just a single bundle to always be bundled? (even when compilation debug=true)
Unfortunately it's an all or nothing setup (determined very early on by AssetManager.DeterminePathsToRender which, based on EnableOptimizations, either emits a bundle URL or individual script paths).
You could look into using the WebEssentials extension which handles .less (as well as other) files natively. At least then you'll be able to include the compiled version and let you move onto more important matters. Once you've finalized, you can bring bundling back into the equation.
I do not work on/for WebEssentials, I just find the extension very helpful
In the main application that I work with, we use the DotLess compiler directly to serve our stylesheets.
We store custom .LESS variables in the database and combine them with the .less file on the fly.
using System.Web.Mvc;
using dotless.Core;
using System.Web.Helpers;
public class SkinController : Controller
{
private const int TwentyMinutes = 1200;
[OutputCache(Duration = TwentyMinutes, VaryByParam = "*", VaryByContentEncoding = "gzip;deflate", VaryByCustom = "Scheme")]
public ActionResult Index()
{
string variablesFromDatabase = "these came from the database";
string lessFileContents = "this was read from the disk";
string content = Less.Parse(string.Concat(variablesFromDatabase, lessFileContents));
SetEtag(content);
return Content(content, "text/css");
}
private void SetEtag(string content)
{
string acceptEncoding = Request.Headers["Accept-Encoding"];
string value = string.Concat(content, acceptEncoding);
Response.AppendHeader("etag", string.Format("\"{0}\"", Crypto.Hash(value, "md5")));
}
}
I am including a big javascript application into our MVC-based solution. However, the application includes a lot of files due to which I would like to enable bundling and minification on it. In fact, I would like to enable bundling on all 3rd party javascript and CSS files while keeping the files we develop ourselves unminified and unbundled. Until release, of course.
There is way to enable optimizations globally:
public static void RegisterBundles(BundleCollection bundles)
{
Bundle ckScripts = new ScriptBundle("~/scripts/ckeditor")
.IncludeDirectory("~/Areas/CMS/Editor", "*.js", true);
bundles.Add(ckScripts);
BundleTable.EnableOptimizations = true;
}
However, this happens in the top BundleTable level enabling optimizations on all the bundles within the bundle table.
I would need to have something like this:
public static void RegisterBundles(BundleCollection bundles)
{
Bundle ckScripts = new ScriptBundle("~/scripts/ckeditor")
.IncludeDirectory("~/Areas/CMS/Editor", "*.js", true)
.EnableOptimizations();
bundles.Add(ckScripts);
}
Which would effectively enable optimizations only for that particular bundle.
I know, there is currently no Bundle.EnableOptimizations() method and creating such given the fact that the optimization happens in the BundleTable level, which is inherently global by design, creating such method, would prove very difficult.
So, here I in loss of ideas where to look into.
Questions:
Is there an alternative framework somewhere that would support this
Is there a contrib project somewhere that would provide this
Have you encountered such need, possibly have a solution
Provided that there's no existing solution, please post an idea how to start unfolding this challenge.
From what I know, BundleTable is a singleton. Which means there can be only one instance. I had an idea of creating another bundle table but got lost when I started figuring out how to make MVC use it.
Another starting point would be to code a custom renderer. One that mimics the behavior of System.Web.Optimization.Scripts.Render(), but again, I'm getting lost with it trying to figure out in which state the BundleTable comes into picture.
UPDATE
Seems like I can create a new BundleContext and BundleResponse by using a HtmlHelper.
public static IHtmlString RenderBundled<TSource>(this HtmlHelper<TSource> helper, string bundlePath)
{
// Find the bundle in question
var bundle = BundleTable.Bundles.FirstOrDefault(b => b.Path == bundlePath);
// No bundle found, return
if (bundle == null) return MvcHtmlString.Create(String.Empty);
// Add the bundle found into a new collection
BundleCollection coll = new BundleCollection {bundle};
// Create a new BundleContext
BundleContext ctx = new BundleContext(helper.ViewContext.HttpContext, coll, "~/bundles");
// Enable optimizations
ctx.EnableOptimizations = true;
// Create the response (this contains bundled & minified script/styles from bundle)
BundleResponse response = bundle.GenerateBundleResponse(ctx);
// Render the content based on ContentType
if (response.ContentType == "text/css")
return RenderStyle(response.Content);// returns <style>bundled content</style>
if (response.ContentType == "text/javascript")
return RenderScript(response.Content); // returns <script>bundled content</script>
// In any other case return "nothing"
return MvcHtmlString.Create(String.Empty);
}
This probably is not the best approach. Overhead from creating BundleContext on every page request and adding the script/styles payload into the page output without the caching abilities. But it's a start. One thing I noticed was that the bundled content will actually be cached in HttpContext.Cache. So, theoretically I could just put the bundle path into script src or style href and somehow then handle the request from server side.
I am currently experimenting with dynamically loaded areas with ASP.NET MVC 3 RC. I've seen it written in many places that this is not what areas are intended for, and (at least pre-MVC 2) not possible, say here for example.
But still! It should be possible to get it to work, right? I've created a solution, added a MVC 3 project, added an area and some content. All is working well. Now I created a new class library project (in the same solution), added a reference to it from the MVC-project, and started moving over the area-related parts to the library. Changed the output-directory of the library project to the area-folder of the MVC-project, and made sure the Views and their web.config are copied to the output-folder.
After reading so much about how you couldn't have external areas, it was a little surprising that this worked. No problem at all really! The problem starts when I remove the reference between the projects, and instead load the library in code. (Before calling AreaRegistration.RegisterAllAreas().) Now it doesn't work. At all.
I've been poking around a bit in the source for MVC 3, and the problem seems to be with BuildManager.GetReferencedAssemblies() which is used to get the assemblies to look for implementations of AreaRegistration.
Now, I'm not 100% sure about this, but it seems as if this method only looks at assemblies that were present/referenced at compile-time, can someone confirm if this is in fact so?
I have debugged through this, and that method-call does indeed not find the assembly I loaded just before the call to it. It might be because of something else that I've missed perhaps.. Any ideas?
The way things work is a bit complicated.
GetReferencedAssemblies includes referenced assemblies, not loaded assemblies. This includes:
all assemblies referenced in you application's web.config (such as System.Web.Mvc)
everything inherited from root web.config, which includes things like System, System.Web and others that you do not have to add yourself. (You can take a look at the list here: C:\Windows\Microsoft.Net\Framework\v4.0.30319\web.config). It also contains a special * item, which:
includes everything in your site's bin folder
So now take your app v1 (everything in a single app). Everything works because the application code gets compiled into the bin folder which gets automatically included. Also, all of the area views etc are in the application itself so they are accessible.
Now in app v2 (different project with a proj-to-proj reference and a custom build task that copies the views to the right location in your main app) everything still works, because by default a proj-to-proj references means that the class library binary gets copied to your app's bin folder. So by the above rules, the area code still gets loaded correctly. The fact that you've set the library's output path to be some location within your main app's Areas folder does not actually make a difference - you just end up with two copies of the binary.
Now in app v3 (no proj-proj ref, area library assembly loaded manually) your library assembly gets loaded too late. By the time your code runs the set of referenced assemblies has already been locked and can no longer be changed.
There is a way to run code and add items to the list of registered assemblies: you can do it using the AddReferencedAssembly method which must be invoked from a PreApplicationStartMethodAttribute method.
Of course you still have to deal with how you manage your view files. The way you currently have it set up is pretty much the same as having the views in the main application (since they effectively get copied into the right location).
1 - Seperate you Mvc Areas into differrent Mvc Projects to be compiled into their own seperate assemblies
2 - Add this to your AssemblyInfo.cs class, to call a method when the application is loaded
[assembly: PreApplicationStartMethod(typeof(PluginAreaBootstrapper), "Init")]
3 - Here's what the Init method looks like when it's invoked during the load
public class PluginAreaBootstrapper
{
public static readonly List<Assembly> PluginAssemblies = new List<Assembly>();
public static List<string> PluginNames()
{
return PluginAssemblies.Select(
pluginAssembly => pluginAssembly.GetName().Name)
.ToList();
}
public static void Init()
{
var fullPluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Areas");
foreach (var file in Directory.EnumerateFiles(fullPluginPath, "*Plugin*.dll"))
PluginAssemblies.Add(Assembly.LoadFile(file));
PluginAssemblies.ForEach(BuildManager.AddReferencedAssembly);
}
}
4 - Add a custom RazorViewEngine
public class PluginRazorViewEngine : RazorViewEngine
{
public PluginRazorViewEngine()
{
AreaMasterLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaPartialViewLocationFormats = new[]
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
var areaViewAndPartialViewLocationFormats = new List<string>
{
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
var partialViewLocationFormats = new List<string>
{
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
};
var masterLocationFormats = new List<string>
{
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
};
foreach (var plugin in PluginAreaBootstrapper.PluginNames())
{
masterLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
masterLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
masterLocationFormats.Add(
"~/Areas/" + plugin + "/Views/Shared/{1}/{0}.cshtml");
masterLocationFormats.Add(
"~/Areas/" + plugin + "/Views/Shared/{1}/{0}.vbhtml");
partialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
partialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
partialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/Shared/{0}.cshtml");
partialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/Shared/{0}.vbhtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.cshtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Views/{1}/{0}.vbhtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.cshtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Areas/{2}/Views/{1}/{0}.vbhtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.cshtml");
areaViewAndPartialViewLocationFormats.Add(
"~/Areas/" + plugin + "/Areas/{2}/Views/Shared/{0}.vbhtml");
}
ViewLocationFormats = partialViewLocationFormats.ToArray();
MasterLocationFormats = masterLocationFormats.ToArray();
PartialViewLocationFormats = partialViewLocationFormats.ToArray();
AreaPartialViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
AreaViewLocationFormats = areaViewAndPartialViewLocationFormats.ToArray();
}
}
5 - Register your Areas from your different Mvc (Area) Projects
namespace MvcApplication8.Web.MyPlugin1
{
public class MyPlugin1AreaRegistration : AreaRegistration
{
public override string AreaName
{
get { return "MyPlugin1"; }
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"MyPlugin1_default",
"MyPlugin1/{controller}/{action}/{id}",
new {action = "Index", id = UrlParameter.Optional}
);
}
}
}
Sourcecode and additional references can can be found here:http://blog.longle.io/2012/03/29/building-a-composite-mvc3-application-with-pluggable-areas
I have my target language in Session["lang"], which is either "en" or "it". I have added this to the Site.master:
<script runat="server">
void Page_Load(object sender, EventArgs e) {
string lang = Session["lang"].ToString();
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture(lang);
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture(lang);
}
</script>
Then I'd like to invoke a resource string like this:
<asp:Label ID="Label1" runat="server" Text="<%$ Resources:Global, test %>"></asp:Label>
I have two files in the App_GlobalResources, named Global.resx and Global.en.resx.
The problems is that no matter what is in the lang variable, I always get the results from the main Global.resx, and I never get the english version from Global.en.resx
I am doing this wrong entirely??
I tried putting the System.Threading... part in the Application_PreRequestHandlerExecute method in Global.asax.cs but the result was the same.
Thanks
PS: I am asking about a way to make this work in a simple way. If I was to use the complicate way, I'd go with this: http://helios.ca/2009/05/27/aspnet-mvc-and-localization/
i had the same dilema(how to implement localization) in my asp.net mvc app.
I followed the instructions posted here and it works like a charm.
So i created a folder named Localization under Content and then i create Resources resx files for each language i want to translate. Keep in mind that there is a convention for the resx file names. ie
Resources.resx is the default fall back for everything.
Resources.en-GB.resx is for english GB
Resources.en-US.resx is for english US
etc.
Just make sure you follow the instructions posted in the link to embed and make the Resources available in all places in your app (views, controllers etc)
Edit:
I want to add that i ommited this line from web.config since i wanted to manually set the local from my app.
<globalization uiCulture="auto" culture="auto"/>
Instead i have created the following class:
public class SmartController : Controller
{
public SmartController()
{
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
}
}
All controllers inherit from this class.
Since this is an administrative set of the locale i have to set it from my apps settings. You could read it from Cookies and set it, or otherwise. This is imo the simplest solution for localization that i have encountered so far.
Once implemented you can refer to any string you add by the following simple line of code, no extra code needed.
<%= Resources.Strings.TranslatedTerm %>
I bet this one is a duplicate.
Anyway - all you need is here (assuming that you are using webforms viewengine (might work with others too, haven't investigated)).
Oh well... here goes my 'summary':
Helpers are just a part. You need to do some modifications with your default view engine too . On createview/createpartialview it should return localizationwebformview which adds a path key to viewdata which is used by htmlhelper to find resourceexpressionsfields and pass them to localizationhelpers class which retrieves desired value.
Little bonus=>
This might be handy if you don't want to recreate resource folders for view subfolders
(in case you modify viewengine.view/partialviewlocationformats):
private static string ReformatVirtualPath(string virtualPath)
{
//This allows NOT to duplicate App_localResources directory
// ~/Views/Shared/Partial/Some/BulltihS/_View.ascx
// turns into =>
// ~/Views/Shared/_View.ascx
var start = #"(~(/?\w*/?){2})";
var end = #"(\w*.as(c|p)x)";
start = Regex.Match(virtualPath, start).Value;
end = Regex.Match(virtualPath, end).Value;
return start + end;
}
usage:
internal static ResourceExpressionFields GetResourceFields
(string expression, string virtualPath)
{
virtualPath = ReformatVirtualPath(virtualPath);
var context = new ExpressionBuilderContext(virtualPath);
var builder = new ResourceExpressionBuilder();
return (ResourceExpressionFields)
builder.ParseExpression(expression, typeof(string), context);
}
EDIT:
but it might be a good idea to avoid App_GlobalResources and App_LocalResources as K. Scott Allen suggests (check Konstantinos answer).