Constraints: I'm not using MVC, just regular ol' .aspx files in my web app. Not using master pages either - each page is a different beast, so that solution isn't right for me.
Most examples I've read for bundling and minification require either some special MVC markup or require you to identify the bundled scripts / stylesheets up front and then refer to these bundles. I want to avoid recompiling DLLs every time I add or modify a .js reference in a .aspx page.
I'm a bit stumped from reading the Msft docs.. is there a way (like an ASP.NET control) that I can just wrap a series of script tags (or link tags for CSS) to create and use a bundle dynamically? I don't want to reinvent the wheel, but seriously considering creating my own user control / custom control that handles this. Are there other options?
For example, looking for something like this:
<asp:AdHocScriptBundle id="mypage_bundle" runat="server">
<script type="text/javascript" src="~/scripts/mypage1.js"></script>
<script type="text/javascript" src="~/scripts/mypage2.js"></script>
<script type="text/javascript" src="~/scripts/mypage3.js"></script>
</asp:AdHocScriptBundle>
that, when bundling is enabled, automatically replaces the contents of asp:AdHocScriptBundle with a single script tag that resembles this:
<script type="text/javascript" src="/webappname/bundles/mypage_bundle.js?v=dh120398dh1298dh192d8hd32d"></script>
And when Bundling is disabled, outputs the contents normally like this:
<script type="text/javascript" src="/webappname/scripts/mypage1.js"></script>
<script type="text/javascript" src="/webappname/scripts/mypage2.js"></script>
<script type="text/javascript" src="/webappname/scripts/mypage3.js"></script>
Any thoughts?
About to roll my own anyway, but if there is already a solution for this please share, thanks!
I rolled my own solution and it works great! I created 4 classes that I can use as custom server controls:
ScriptBundle
Script
StyleBundle
Link
These call functions around my custom bundling library which is itself a wrapper for the System.Web.Optimization API.
During Render of ScriptBundle and StyleBundle I then check an internal setting (the same one that I use to set EnableOptimizations in the System.Web.Optimization API) that tells the page to either use bundling, or simply write out the normal script / link tags. If Bundling is enabled it calls this function from my custom bundling library (for Scripts, similar code for Styles tho. Bundler in code below is the class for my custom bundling library - just in case Microsoft changes the System.Web.Optimization API I wanted a layer in-between so that I wouldn't have to change my code as much):
public static void AddScriptBundle(string virtualTargetPath, params string[] virtualSourcePaths)
{
var scriptBundle = new System.Web.Optimization.ScriptBundle(virtualTargetPath);
scriptBundle.Include(virtualSourcePaths);
System.Web.Optimization.BundleTable.Bundles.Add(scriptBundle);
}
To make sure that I only create the Bundle if it does not already exist, I first check for the Bundle using this method (before using the above method):
public static bool BundleExists(string virtualTargetPath)
{
return System.Web.Optimization.BundleTable.Bundles.GetBundleFor(virtualTargetPath) != null;
}
Then I use this function to spit out the URL to the bundle by using System.Web.Optimization:
public static System.Web.IHtmlString GetScriptBundleHTML(string virtualTargetPath)
{
return System.Web.Optimization.Scripts.Render(virtualTargetPath);
}
Within my .aspx files, I do this:
<%# Register TagPrefix="cc1" Namespace="AdHocBundler" Assembly="AdHocBundler" %>
...
<cc1:ScriptBundle name="MyBundle" runat="Server">
<cc1:script src='~/js/script1.js'/>
<cc1:script src='~/js/utils/script2.js'/>
</cc1:ScriptBundle>
The trick for me was figuring out that I had to convert script and link tags to be work as list items within the ScriptBundle and StyleBundle controls, but after that it works great AND it let me use the tilde operator for easy references relative to app root (using Page.ResolveClientUrl(), which is helpful for creating module content).
Thanks go to this SO answer for helping me figure out how to create a custom collection control: How do you build an ASP.NET custom control with a collection property?
UPDATE: In the interest of full disclosure, I got permission to share the code for ScriptBundle (StyleBundle is almost identical, so did not include it):
[DefaultProperty("Name")]
[ParseChildren(true, DefaultProperty = "Scripts")]
public class ScriptBundle : Control
{
public ScriptBundle()
{
this.Enabled = true;
this.Scripts = new List<Script>();
}
[PersistenceMode(PersistenceMode.Attribute)]
public String Name { get; set; }
[PersistenceMode(PersistenceMode.Attribute)]
[DefaultValue(true)]
public Boolean Enabled { get; set; }
[PersistenceMode(PersistenceMode.InnerDefaultProperty)]
public List<Script> Scripts { get; set; }
protected override void Render(HtmlTextWriter writer)
{
if (String.IsNullOrEmpty(this.Name))
{
// Name is used to generate the bundle; tell dev if he forgot it
throw new Exception("ScriptBundle Name is not defined.");
}
writer.BeginRender();
if (this.Enabled && Bundler.EnableOptimizations)
{
if (this.Scripts.Count > 0)
{
string bundleName = String.Format("~/bundles{0}/{1}.js",
HttpContext.Current.Request.FilePath,
this.Name).ToLower();
// create a bundle if not exists
if (!Bundler.BundleExists(bundleName))
{
string[] scriptPaths = new string[this.Scripts.Count];
int len = scriptPaths.Length;
for (int i = 0; i < len; i++)
{
if (!string.IsNullOrEmpty(this.Scripts[i].Src))
{
// no need for resolve client URL here - bundler already does it for us, so paths like "~/scripts" will already be expanded
scriptPaths[i] = this.Scripts[i].Src;
}
}
Bundler.AddScriptBundle(bundleName, scriptPaths);
}
// spit out a reference to bundle
writer.Write(Bundler.GetScriptBundleHTML(bundleName));
}
}
else
{
// do not use bundling. generate normal script tags for each Script
foreach (Script s in this.Scripts)
{
if (!string.IsNullOrEmpty(s.Src))
{
// render <script type='<type>' src='<src'>/> ... and resolve URL to expand tilde, which lets us use paths relative to app root
// calling writer.Write() directly since it has less overhead than using RenderBeginTag(), etc., assumption is no special/weird chars in the cc1:script attrs
writer.Write(String.Format(Script.TAG_FORMAT_DEFAULT,
s.Type,
Page.ResolveClientUrl(s.Src)));
}
}
}
writer.EndRender();
}
}
public class Script
{
public const String ATTR_TYPE_DEFAULT = "text/javascript";
public const String TAG_FORMAT_DEFAULT = "<script type=\"{0}\" src=\"{1}\"></script>";
public Script()
{
this.Type = ATTR_TYPE_DEFAULT;
this.Src = null;
}
public String Type { get; set; }
public String Src { get; set; }
public String Language { get; set; }
}
This isn't possible with the default Bundling/Minification in ASP.NET. The entire point of bundling is to create one single to file to reduce the number of browser requests to load static files such as .JS and .CSS files.
Rolling your own is your only option. However, please note that each <script> line will result in a browser request. Since most browsers can only handle 6 requests concurrently, you can have wait times just to load these static files.
And FYI, you don't have to recompile DLLs every time you update your .JS files with built-in bundling. You can simply reset the application pool the app is running on. If you're running with an external session persistence model, your users won't notice when this happens.
Your problem here is that you aren't really thinking this problem through. If you were, you would realize that what you are asking for can't work.
Why? Because the script tag ahs to generate an external link reference to a different url. So anything you place in the header of the current file will have no affect on your other URL that actually contains your bundles. As such, there is no way to dynamically change your bundles in the page itself because bundles have to, by definition, be defined in an external resource.
Now, there's nothing that says those bundles have to be compiled into DLL's in your own solution, but they cannot be embedded in the page that's currently being rendered.
You might want to investigate some of the javascript based minification tools out there, since they are typically not compiled.
Related
I have a WebView containing HTML data. The data is generated at runtime. A main feature of my app is highlighting certain parts of this HTML data. I tried this by using javascript.
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_reader_page, container, false);
webview = (WebView) rootView.findViewById(R.id.resultText);
webview.getSettings().setJavaScriptEnabled(true);
String page = makeHTML();
webview.loadUrl("file:///android_asset/jquery-1.8.2.js");
webview.loadUrl("file:///android_asset/src_javascript.js");
webview.loadData(page, "text/html", "UTF-8");
return rootView;
}
private String makeHTML() {
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html>\n");
sb.append("<head>\n");
sb.append("</head>\n");
sb.append("<body>\n");
sb.append(tokenizedText + "\n");
sb.append("</body>\n");
sb.append("</html>\n");
return sb.toString();
}
tokenizedText is my at runtime generated data with this format:
<YT_SEN id="_YT_SEN_0">This is my first sentence.</YT_SEN>
<YT_SEN id="_YT_SEN_1">This is my second sentence.</YT_SEN>
...
When my data is loaded in the WebView, the user can highlight a particular sentence by giving its number. This method then calls the corresponding javascript function:
public void highlightSentence(int sent_id) {
if (android.os.Build.VERSION.SDK_INT < 19) {
webview.loadUrl("javascript:highlightSentence('_YT_SEN_" +sent_id+ "', " +color+ ")");
} else {
webview.evaluateJavascript("javascript:highlightSentence('_YT_SEN_" +sent_id+ "', " +color+ ")", null);
}
}
The javascript function for highlighting (defined inside file:///android_asset/src_javascript.js):
function highlightSentence(object,color)
{
document.getElementById(object).style.backgroundColor = color;
}
The output of Logcat when I execute the highlightSentence method:
I/chromium﹕ [INFO:CONSOLE(1)] "Uncaught ReferenceError: highlightSentence is not defined", source: (1)
Somehow the WebView can't find the highlightSentence function. I think it's because of the way I load the Javascript and JQuery files. Yet I don't know (and can't find) the proper way to load external js-files within at runtime generated HTML data.
Note: I use the WebView solely for offline use, I have no need for any internet communication whatsoever. WebView seemed like the easiest way to enable dynamic highlighting.
It seems the Javascript same origin policy is the root of the problem. The WebView will only load javascript files which are from the same origin as the html. Since no origin for the html was given, the data scheme is used as default. If, however, the data is loaded with the same base url as the javascript files, no problem arises.
Load the html data (with file:///android_asset/javascript/ being the directory of the javascript files):
webview.loadDataWithBaseURL("file:///android_asset/javascript/", page, "text/html", "UTF-8", null);
Then reference the javascript files like this inside the html:
<script src='jquery-1.8.2.js' type='text/javascript'></script>
<script src='src_javascript.js' type='text/javascript'></script>
Assuming your javascript is in the assets directory point to it with a file url
file://android_asset/<some java script file in assets>
file://android_asset/ points to the assets directory in your apk.
So you can reference the script in your html when you build it for the webview.
<script charset='utf-8' type='text/javascript'
src='file://android_asset/myjavascript.js'></script>
In a project using JSF, we have JavaScript files located here:
MyProject/view/javascript/*.js
In the xhtml file, I include these resources as
<script type="text/javascript" src="javascript/#{myBean.jsFileName}.js" />
This works fine, but it's possible that #{myBean.jsFileName}.js doesn't exist, in which case I want to load "Default.js".
In myBean (or in the xhtml file itself), how can I first check for the existence of a js file before setting the #{myBean.jsFileName} value? I've tried variations of this:
File f = new File("javascript/myFile.js");
if (!f.exists){ jsFileName="Default"};
But I don't think that's right. Am I on the right track?
You can use ExternalContext#getResource() to obtain a web resource as URL. It will return null if resource doesn't exist. It takes a path relative to the web root and should preferably always start with /.
So. this should do in bean:
String fullPath = "/view/javascript/myFile.js";
URL resource = FacesContext.getCurrentInstance().getExternalContext().getResource(fullPath);
if (resource == null) {
fullPath = "/view/javascript/default.js";
}
Or, equivalently, this in the view, which is IMO somewhat hacky:
<c:set var="available" value="#{not empty facesContext.externalContext.getResource(bean.fullPath)}" />
<script src="#{available ? bean.fullPath : bean.fullDefaultPath}" />
See also:
getResourceAsStream() vs FileInputStream
Unrelated to the concrete problem, if you were using JSF 2.x (at least Facelets, as implied by being able to use unified EL in template text in your code snippet), you should be using resource libraries with <h:outputScript>. See also How to reference CSS / JS / image resource in Facelets template?
Is there any tool to make adding files to MVC3 razor pages faster?
I find myself having to drag script files onto the page to generate:
<script src="../../Scripts/rails.js" type="text/javascript"></script>
Which then i'll copy and paste
<script src="#Url.Content("~/Scripts/")" type="text/javascript"></script>
Then cut/drag the rails.js fragment into the new script statement. Then at some point after this hopefully I remember I need to clean up a whole bunch of duplicated and/or broken script links.
There has to be a better way than this that doesn't involve typing urls out manually.
Came across this blog post earlier today and ASP.NET MVC Best Practices (Part 1) and it's guidance shows
public static string Image(this UrlHelper helper, string fileName)
{
return helper.Content("~/assets/images/{0}".FormatWith(fileName));
}
public static string Stylesheet(this UrlHelper helper, string fileName)
{
return helper.Content("~/assets/stylesheets/{0}".FormatWith(fileName));
}
public static string NoIcon(this UrlHelper helper)
{
return Image(helper, "noIcon.png");
}
This seems like the optimal solution if you use a good layout scheme for your resources.
Edit: FWIW
public static string FormatWith(this string format, params object[] inputs)
{
return string.Format(format, inputs)
}
You can use T4MVC to get a compile-time validation your your links.
2.3. Strongly typed links to script files and static resources
T4MVC generates static helpers for
your content files and script files.
So instead of writing:
<img src="/Content/nerd.jpg" />
You
can write:
<img src="<%= Links.Content.nerd_jpg %>" />
Likewise, instead of
<script src="/Scripts/Map.js" type="text/javascript"></script>
You
can write
<script src="<%= Links.Scripts.Map_js %>" type="text/javascript"></script>
The obvious benefit is that you’ll get
a compile error if you ever move or
rename your static resource, so you’ll
catch it earlier.
Another benefit is that you get a more
versatile reference. When you write
src="/Content/nerd.jpg", your app will
only work when it’s deployed at the
root of the site. But when you use the
helper, it executes some server side
logic that makes sure your reference
is correct wherever your site is
rooted. It does this by calling
VirtualPathUtility.ToAbsolute("~/Content/nerd.jpg").
Looking for an elegant way of having scripts added once on a page and that's it.
I have a partial view that requires 2 CSS files and 2 JS files. In most places, there is only need for 1 of the partial views. On a single page though, I need 3 of these same partial views, and each partial view has the 4 files, so I have 6 JS links and 6 CSS links. Quite ugly.
I original idea was to use jQuery to see if the tags (by id) are existant on the page yet or not. If they aren't, then add them in. Otherwise, do nothing. This was going to be an inline script like....
<script type="text/javascript" language="javascript">
function(){
var jQueryUICSS = $("#jQueryUICSS");
if(!jQueryUICSS){
document.write('link id="jQueryUICSS" href="/Content/smoothness/jquery-ui-1.8.5.custom.css" rel="stylesheet" type="text/css" />')
}
...And so on for the other 3 tags.
};
But, I'm not sure that will work (or will the lead dev accept it):P
Any other ideas?
David,
I use a couple of static htmlhelpers in my code for exactly this scenario. it works on the principle that the context.items collection gets populated per request and therefore if an item exists in the context.items collection then it doesn't get added twice. anyway, enough of the scottish words of wisdOOOm, 'yill jist be waantin the coade'...
for our Scripts:
public static MvcHtmlString Script(this HtmlHelper html, string path)
{
var filePath = VirtualPathUtility.ToAbsolute(path);
HttpContextBase context = html.ViewContext.HttpContext;
// don't add the file if it's already there
if (context.Items.Contains(filePath))
return MvcHtmlString.Create("");
// add the beast...
context.Items.Add(filePath, filePath);
return MvcHtmlString.Create(
string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", filePath));
}
for our cuddly css:
// standard method - renders as defined in as(cp)x file
public static MvcHtmlString Css(this HtmlHelper html, string path)
{
return html.Css(path, false);
}
// override - to allow javascript to put css in head
public static MvcHtmlString Css(this HtmlHelper html,
string path,
bool renderAsAjax)
{
var filePath = VirtualPathUtility.ToAbsolute(path);
HttpContextBase context = html.ViewContext.HttpContext;
// don't add the file if it's already there
if (context.Items.Contains(filePath))
return null;
// otherwise, add it to the context and put on page
// this of course only works for items going in via the current
// request and by this method
context.Items.Add(filePath, filePath);
// js and css function strings
const string jsHead = "<script type='text/javascript'>";
const string jsFoot = "</script>";
const string jsFunctionStt = "$(function(){";
const string jsFunctionEnd = "});";
string linkText = string.Format("<link rel=\"stylesheet\" type=\"text/css\" href=\"{0}\"></link>", filePath);
string jsBody = string.Format("$('head').prepend('{0}');", linkText);
var sb = new StringBuilder();
if (renderAsAjax)
{
// join it all up now
sb.Append(jsHead);
sb.AppendFormat("\r\n\t");
sb.Append(jsFunctionStt);
sb.AppendFormat("\r\n\t\t");
sb.Append(jsBody);
sb.AppendFormat("\r\n\t");
sb.Append(jsFunctionEnd);
sb.AppendFormat("\r\n");
sb.Append(jsFoot);
}
else
{
sb.Append(linkText);
}
return MvcHtmlString.Create( sb.ToString());
}
usage in both cases:
<%=Html.Css("~/Content/Site.Css")%>
<%=Html.Script("~/Scripts/default.js")%>
have fun...
[edit] - pay particular attn to the comment line:
// this of course only works for items going in via the current
// request and by this method
Put them in your master. CSS and Javascript files are cached. Load once and don't worry about it.
I am currently in the process of converting old "Web 1.0" code to meet current standards.
Is there a better way to generate and append a client-side script other than appending a plethora of lines to a StringBuilder and then registering it to the page via ClientScript.RegisterStartupScript(Me.GetType(), "startUpScript", strScript)?
Is there any other way (besides putting all of this into a global .js file) that this example can be improved? If including this into the main .js file is the "best practice" alternative, then why?
Dim lsbScript As New Text.StringBuilder
lsbScript.Append(vbCrLf)
lsbScript.Append("<script language=""javascript>""" & vbCrLf)
lsbScript.Append("<!--" & vbCrLf)
...
lsbScript.Append("//-->" & vbCrLf)
lsbScript.Append("</SCRIPT>" & vbCrLf)
If Not ClientScript.IsStartupScriptRegistered("someScript") Then
ClientScript.RegisterStartupScript(Me.GetType(), "someScript", lsbScript.ToString)
End If
a good middle ground might be offloading the script contents to a new js file, and including a script node from codebehind.
protected void Page_Load(object sender, EventArgs e) {
bool someCondition = Whatever();
if (someCondition)
{
System.Web.UI.HtmlControls.HtmlGenericControl script;
script = new System.Web.UI.HtmlControls.HtmlGenericControl("script");
script.Attributes["src"] = "myscript.js";
this.Controls.Add(script);
}
}
this will just drop the node at the end of the page. alternatively, you could put a placeholder control whereever you like on the page, and add the new HtmlGenericControl to that.
(sorry, vb is not my native language).
Yes you can include a .js file in your project. Set it's Build Action (right click the file in your project explorer) to Embedded Resource
For example: common.js
Above the namespace of your server control (or whereever you need it):
[assembly: System.Web.UI.WebResource(
"MyFullNameSpace.Common.js",
"text/javascript", PerformSubstitution = true)]
OnPreRender event:
if (!Page.ClientScript.IsClientScriptIncludeRegistered("Common"))
{
string url = Page.ClientScript.GetWebResourceUrl(this.GetType(), "MyFullNameSpace.Common.js");
Page.ClientScript.RegisterClientScriptInclude("Common", url);
}
Beware to replace MyFullNameSpace with the complete and exact namespace of where .js is located. If it doesn't work probably .net can't find it and you can use reflector to open your dll and find the .js as embedded resource so you will know the exact namespace it's in.
I always do this from Server Controls but i image it could be done in a web app project as well.
You can get a little mileage out of using AppendLine() instead of concatenating vbCrLf.