I would like to override Url.Content to append a query string parameter to the resulting string on Url.Content.
The reason being, I have a web application that I develop, and with each release, users must clear their cache to get the new css and js. A solution for this is to append a version number to the querystring to force loading of the new version.
A working solution is as follows:
#{ var version = "?v=" + ViewBag.VersionNumber; }
<head>
<link href="#Url.Content("~/ux/css/base.css")#version" rel="stylesheet" type="text/css" />
</head>
Version is set in a config file so with each release, the version is updated. I would like this to be more automatic though, as currently any time a new css reference is added, we must remember to add #version to the string. An extension method the returns the path with the version number already appended would be perfect.
Also, if anyone knows who I could make changing the version number automatic with TFS check-ins or compiles that would be really useful too.
You could do something like this:
public static string VersionedContent(this UrlHelper urlHelper, string contentPath)
{
string result = urlHelper.Content(contentPath);
var versionService = Engine.IocService.GetInstance<IVersionService>();
string tag = versionService.GetVersionTag();
if (result.Contains('?'))
{
result += "&v="+tag;
}
else
{
result += "?v="+tag;
}
return result;
}
Version Service could look something like this:
public class VersionService : IVersionService
{
string _versionTag;
public VersionService()
{
_versionTag = Assembly.GetExecutingAssembly().GetName().Version.ToString();
_versionTag = _versionTag.Replace('.', '-');
}
#region IVersionedContentService Members
public string GetVersionTag()
{
return _versionTag;
}
#endregion
}
You might want to take a look at cassette
* EDIT *
For autom. build numbers with TFS, check out:
automatic-assembly-file-version-numbering-in-tfs-2010
Related
I am trying to implement the HTMLTagHelper asp-append-version="true" to my images.
The problem is as regards the DOM distribution, I am not assigning the attribute to an <img> tag but to to a <div> containing the image with the background-url property.
Moreover, the div is generated before all the DOM is loaded and I don't know if there would be a different approach of doing it.
One is obvious, change the div to an img tag, but I don't want it as my design has to remain the same.
My javascript has hitherto been like this:
cardHTML += '<div asp-append-version="true" class="card littleCard" style="background-image: url(/Content/Img/Especialistas/LittleCard/' + especialista.idEspecialista + '.jpg' + ')' + '" >';
cardHTML += '</div>';
The asp-append-version="true" won't work on the div tag.
Any ideas on how to find an approach of dealing with this ?
Thanks
You can create a custom TagHelper to target all elements having an inline style attribute. The following example I've tried looks working fine but if you want something more standard (similar to ImageTagHelper, ...), you can try looking into the base class UrlResolutionTagHelper. I'm not so sure why it need to be more complicated in there in which basically you need to resolve the URL before actually processing it more. I've tried with a simple IFileVersionProvider and it works for relative paths as well (of course the resolved path should be at the current server's web root).
The following simple example works fine for attribute values of HtmlString (which is almost the usual case, some custom rendering may inject IHtmlContent that is not of HtmlString, for such complicated cases, you can refer to the source code for UrlResolutionTagHelper, even copying almost the exact relevant code there is fine):
//target only elements having an inline style attribute
[HtmlTargetElement(Attributes = "style")]
public class InlineStyleBackgroundElementTagHelper : TagHelper
{
readonly IFileVersionProvider _fileVersionProvider;
const string BACKGROUND_URL_PATTERN = "(background(?:-image)?\\s*:[^;]*url)(\\([^)]+\\))";
public InlineStyleBackgroundElementTagHelper(IFileVersionProvider fileVersionProvider)
{
_fileVersionProvider = fileVersionProvider;
}
//bind the asp-append-version property
[HtmlAttributeName("asp-append-version")]
public bool AppendsVersion { get; set; }
//inject ViewContext from the current request
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (AppendsVersion)
{
if (output.Attributes.TryGetAttribute("style", out var styleAttr))
{
//the value here should be an HtmlString, so this basically
//gets the raw plain string of the style attribute's value
var inlineStyle = styleAttr.Value.ToString();
var basePath = ViewContext.HttpContext.Request.PathBase;
inlineStyle = Regex.Replace(inlineStyle, BACKGROUND_URL_PATTERN, m =>
{
//extract the background url contained in the inline style
var backgroundUrl = m.Groups[2].Value.Trim('(', ')', ' ');
//append the version
var versionedUrl = _fileVersionProvider.AddFileVersionToPath(basePath, backgroundUrl);
//format back the inline style with the versioned url
return $"{m.Groups[1]}({versionedUrl})";
}, RegexOptions.Compiled | RegexOptions.IgnoreCase);
output.Attributes.SetAttribute("style", inlineStyle);
}
}
}
}
Usage: just like how you use the asp-append-version on other built-in tag helps. (like in your example).
I have a series of template files (*.html) that are used by some JS components. Instead of having those JS components write the templates to the DOM when loaded, I wanted to bundle them together like scripts and have them downloaded separately by the client. This way should be faster (clientside), allow for caching (less round trips), and be more readable (template doesn't have to be stored in a JS string which breaks highlighting/intellisense).
How can this be accomplished?
I.
Use BundleTransformer [http://bundletransformer.codeplex.com/] and Mustache templates [https://mustache.github.io/] or Handlebars [http://handlebarsjs.com/]
II.
[Angular example but you can inspire a lot]
I'm not saying this is the best approach for your case, but i cannot left it like a comment.
Here is an example where the OP stores his bundle in $templateCache. Angular has a templateCache object, which stores all the templates it has loaded so far. It also lets you pre-load templates into the template cache.
Create a BundleTransform class, as he did:
public class PartialsTransform : IBundleTransform
{
private readonly string _moduleName;
public PartialsTransform(string moduleName)
{
_moduleName = moduleName;
}
public void Process(BundleContext context, BundleResponse response)
{
var strBundleResponse = new StringBuilder();
// Javascript module for Angular that uses templateCache
strBundleResponse.AppendFormat(
#"angular.module('{0}').run(['$templateCache',function(t){{",
_moduleName);
foreach (var file in response.Files)
{
// Get the partial page, remove line feeds and escape quotes
var content = File.ReadAllText(file.FullName)
.Replace("\r\n", "").Replace("'", "\\'");
// Create insert statement with template
strBundleResponse.AppendFormat(
#"t.put('partials/{0}','{1}');", file.Name, content);
}
strBundleResponse.Append(#"}]);");
response.Files = new FileInfo[] {};
response.Content = strBundleResponse.ToString();
response.ContentType = "text/javascript";
}
}
But you can store the templates where you want [i don't know where you want to store them].
Then create a Bundle.
public class PartialsBundle : Bundle
{
public PartialsBundle(string moduleName, string virtualPath)
: base(virtualPath, new[] { new PartialsTransform(moduleName) })
{
}
}
And you can use it like a ScriptBundle or StyleBundle.
bundles.Add(new PartialsBundle("testSPA", "~/bundles/partials").Include(
"~/Partials/nav-bar.html",
"~/Partials/home-page.html",
"~/Partials/investment-filter.html",
"~/Partials/investments-component.html",
"~/Partials/sector-component.html",
"~/Partials/transactions-component.html"));
And render like this: #Scripts.Render("~/bundles/partials")
In production transforming in this:
<script src="/bundles/partials?v=dq0i_tF8ogDVZ0X69xyBCdV2O2Qr3nCu0iVsatAzhq41"></script>
This guy is using the $templateCache object forcing Angular not to dynamically download template when are needed.
Further reading here: http://blog.scottlogic.com/2014/08/18/asp-angular-optimisation.html
I decided to help my friend with a project he's working on. I'm trying to write a test webpage for him to verify some new functionality, but in my auto-generated code I get
CS1106: Extension method must be defined in a non-generic static class
Implementing the code in index.cshtml isn't the best way to do this, but we are just trying to do a proof of concept and will do a proper implementation later.
In all the places I looked they pretty much said that all the functions I define must be in a static class (as the error states). That wouldn't be so bad except for the class that holds all my functions is auto-generated and not static. I'm not really sure what settings I can change to fix this.
Here is a copy of the relevant (I believe) parts of code. The implementation of some or all of the functions may be incorrect. I haven't tested them yet
#{
HttpRequest req = System.Web.HttpContext.Current.Request;
HttpResponse resp = System.Web.HttpContext.Current.Response;
var url = req.QueryString["url"];
//1 Download web data from URL
//2 Write the final edited version of the document to the response object using resp.write(String x);
//3 Add Script tag for dom-outline-1.0 to html agility pack document
//4 Search for relative URLs and correct them to become absolute URL's that point back to the hostname
}
#functions
{
public static void PrintNodes(this HtmlAgilityPack.HtmlNode tag)
{
HttpResponse resp = System.Web.HttpContext.Current.Response;
resp.Write(tag.Name + tag.InnerHtml);
if (!tag.HasChildNodes)
{
return;
}
PrintNodes(tag.FirstChild);
}
public static void AddScriptNode(this HtmlAgilityPack.HtmlNode headNode, HtmlAgilityPack.HtmlDocument htmlDoc, string filePath)
{
string content = "";
using (StreamReader rdr = File.OpenText(filePath))
{
content = rdr.ReadToEnd();
}
if (headNode != null)
{
HtmlAgilityPack.HtmlNode scripts = htmlDoc.CreateElement("script");
scripts.Attributes.Add("type", "text/javascript");
scripts.AppendChild(htmlDoc.CreateComment("\n" + content + "\n"));
headNode.AppendChild(scripts);
}
}
}
<HTML CODE HERE>
If you were really smart you would encapsulate the design to take Delegates, reason being if you use a delegate you don't have to worry about referencing something static.
public delegate void MyUrlThing(string url, object optional = null);
Possibly some state...
public enum UrlState
{
None,
Good,
Bad
}
Then void would become UrlState...
Also if you wanted you could also setup a text box and blindly give it CIL....
Then you would compile the delegates using something like this
http://www.codeproject.com/Articles/578116/Complete-Managed-Media-Aggregation-Part-III-Quantu
This way you can use also then optionally just use the IL to augment whatever you wanted.
You could also give it CSharp code I suppose...
If you want to keep you design you can also then optionally use interfaces... and then put the compiled dll in a directory and then load it etc... as traditionally
I'm trying to generate a HTML document with RazorEngine (http://razorengine.codeplex.com/). Everything is mostly working, but the problem I have now is some of the HTML is being rendered correctly, and HTML that I have nested in that is being rendered as literal HTML, so rather than the browser displaying the tables & divs as expected, it's displaying e.g.
"<table></table><div></div>"
I kick off this process by calling the following:
string completeHTML = RazorEngine.Razor.Parse("InstallationTemplate.cshtml", new { Data = viewModels });
The completeHTML is then written to a file.
"InstallationTemplate.cshtml" is defined as:
#{
var installationReport = new InstallationReport(Model.Data);
}
<!DOCTYPE html>
<html>
<head></head>
<body>
<div>
<!-- I would expect this to write the rendered HTML
in place of "#installationReport.DigiChannels()" -->
#installationReport.DigiChannels()
</div>
</body>
</html>
Where InstallationReport and DigiChannels are defined as follows:
public static class InstallationReportExtensions
{
public static string DigiChannels(this InstallationReport installationReport)
{
return installationReport.GetDigiChannelsHtml();
}
}
public class InstallationReport
{
public string GetDigiChannelsHtml()
{
// the following renders the template correctly
string renderedHtml = RazorReport.GetHtml("DigiChannels.cshtml", GetDigiChannelData());
return renderedHtml;
}
}
public static string GetHtml(string templateName, object data)
{
var templateString = GetTemplateString(templateName);
return RazorEngine.Razor.Parse(templateString, data);
}
After GetDigiChannelsHtml() runs and returns renderedHtml, the line of execution returns to TemplateBase.cs into the method ITemplate.Run(ExecuteContext context), which is defined as:
string ITemplate.Run(ExecuteContext context)
{
_context = context;
var builder = new StringBuilder();
using (var writer = new StringWriter(builder))
{
_context.CurrentWriter = writer;
Execute(); // this is where my stuff gets called
_context.CurrentWriter = null;
}
if (Layout != null)
{
// Get the layout template.
var layout = ResolveLayout(Layout);
// Push the current body instance onto the stack for later execution.
var body = new TemplateWriter(tw => tw.Write(builder.ToString()));
context.PushBody(body);
return layout.Run(context);
}
return builder.ToString();
}
When I inspect builder.ToString(), I can see it contains proper HTML for the InstallationTemplate.cshtml stuff, and escaped HTML for the DigiChannels.cshtml stuff. For example:
How can I get #installationReport.DigiChannels() to include the correct HTML instead of the escaped HTML that it's currently doing?
Have you tried:
#Raw(installationReport.DigiChannels())
Edit : I could use it in following way (MVC3)
#Html.Raw(installationReport.DigiChannels())
The alternative to #Raw is to change your API to return HtmlStrings in appropriate places
Represents an HTML-encoded string that should not be encoded again.
The default razor behaviour is to encode strings.
I can't figure out how to enable a custom route for static files.
I'd like to setup my asp.net mvc application to use synthetic URLs for versioned assets. This allows me to set expires to max for my static assets, and to have the version be incremented whenever I do a build.
I'd like to put that version into the path of my static assets when composing my cshtml templates. Lastly, I need to setup the routing to ignore the version component of the URL and just serve the static asset.
For example: /js/1.2.3.4/libs/require.js -> /js/libs/require.js
I've got the first two pieces working, but I can't figure out how to have part of a URL ignored for static files.
Layout.cshtml
...
<script data-main="/js/#(((myApplication)(HttpContext.Current.ApplicationInstance)).Version)/config"
src="/js/#(((myApplication)(HttpContext.Current.ApplicationInstance)).Version)/libs/require.js"></script>
...
myApplication.cs
public class myApplication : HttpApplication {
...
public string Version {
get {
var baseVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString();
#if DEBUG
var span = (DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0).ToLocalTime());
baseVersion += "_" + span.TotalMilliseconds;
#endif
return baseVersion;
}
}
}
Edit:
I managed to make a workable solution (courtesy of sheikhomar) using an OnBeginRequest handler at the application to rewrite URL when it matched a regex, but that seems excessive to do that on every request:
private readonly Regex jsPattern = new Regex("^/js/[0-9._]*/(.*)$", RegexOptions.IgnoreCase);
protected void OnBeginRequest(Object sender, EventArgs e) {
var match = jsPattern.Match(Request.Url.AbsolutePath);
if (match.Success) {
var fileName = match.Groups[1].Value;
Context.RewritePath(string.Format("/js/{0}", fileName));
}
}
I would suggest just letting MVC handle the route. Doing URL rewriting outside of the normal MVC route handling is something I would advise against unless you have a really good reason for it. The point of ASP.NET MVC is to eliminate the need for doing these kinds of low-level actions.
Anyway, in MVC, map your route similar to this:
routes.MapRoute("staticJs", "js/{version}/{*fileName}", new { controller = "Static", action = "Js" });
and then in StaticController
public ActionResult Js(string version, string fileName) {
var basePath = #"c:\path\to\js";
var realPath = Path.Combine(basePath, fileName);
return File(realPath, "application/javascript");
}
which will just stream the file back to the client (assuming that c:\path\to\js is the path to the javascript file on disk).
You can consult this answer for more info on FileResults.