Play Framework 2.1 websockets in Chrome - javascript

I can't seem to get websocket communication to work in the Play Framework version 2.1.
I created a simple test that does nothing but send messages back and forth with a push of a button. All the code for it is below. But nothing shows up except for the button.
Has anybody seen this problem or can someone tell me what I may be doing wrong in the code below?
I am using the latest version of Chrome.
Here is my simple setup.
In Application.java
public static Result index() {
return ok(index.render());
}
public static WebSocket<String> sockHandler() {
return new WebSocket<String>() {
// called when the websocket is established
public void onReady(WebSocket.In<String> in,
WebSocket.Out<String> out) {
// register a callback for processing instream events
in.onMessage(new Callback<String>() {
public void invoke(String event) {
System.out.println(event);
}
});
// write out a greeting
out.write("I'm contacting you regarding your recent websocket.");
}
};
}
In Routes File
GET / controllers.Application.index()
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file)
GET /greeter controllers.Application.sockHandler()
In Index.Scala.html
#main(null) {
<div class="greeting"></div>
<button class="send">Send</button>
<script type="text/javascript" charset="utf-8">
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var sock = new WS("#routes.Application.sockHandler()")
sock.onmessage = function(event) {
$('.greeting').append(event.data)
}
$('button.send').click(function() {
sock.send("I'm sending a message now.")
});
})
</script>
}
In Main.scala.html
#(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
<title>#title</title>
<link rel="stylesheet" media="screen" href="#routes.Assets.at("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="#routes.Assets.at("images/favicon.png")">
<script src="#routes.Assets.at("javascripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
</head>
<body>
#content
</body>

The problem is in
var sock = new WS("#routes.Application.sockHandler()")
you have to specify the protocol and the complete url in the format: ws://localhost:9000/greeter.
Check this question to do it in javascript: How to construct a WebSocket URI relative to the page URI?

you can use a Route's webSocketURL() method to retrieve a url that can be passed to a WebSocket's constructor. Here's an example from Play's websocket-chat sample code:
$(function() {
var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
var chatSocket = new WS("#routes.Application.chat(username).webSocketURL()")
var sendMessage = function() {
chatSocket.send(JSON.stringify(
{text: $("#talk").val()}
))
$("#talk").val('')
}
// ...
So in your code you can use something like
var sock = new WS("#routes.Application.sockHandler().webSocketURL()");
Personally I don't like intermingling interpolated code with JS, since I think that any code executing on the client should only be concerned with the state of the client, and not the server (not to mention it makes refactoring the script out into an external file impossible), so I tend to do something like this:
<div class="container app-container"
data-ws-uri="#routes.Application.WSUri.webSocketURL()">
.......
</div>
Then in my JS I can just do something like
var sock = new WS(document.querySelector(".app-container").dataset.wsUri);
// ....

Related

SignalR 2.3 cannot read property client

Good day,
Unfortunately, I can not get SignalR to work for a private 1 on 1 chat.
Here the following things that I have done:
References that are situated in the head tag of my layout page in the right order:
<script src="~/Scripts/jquery-1.6.4.min.js"></script>
<script src="~/Scripts/jquery.signalR-2.3.0.js"></script>
Even added both bellow to check if either worked but no luck.
<script src="/signalr/hubs"></script>
<script src="http://localhost:50581/signalr/hubs"></script>
Have an Owin startup class:
[assembly: OwinStartup(typeof(Startup))]
namespace WebApplication6
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = new HubConfiguration();
config.EnableDetailedErrors = true;
config.EnableJavaScriptProxies = true;
app.MapSignalR("/signalr", config);
}
}
}
Have my chatHub class with my methods inside:
[HubName("chatHub")]
public class ChatHub : Hub
{
}
I have a javascript file named chatboxManager with the following inside:
var connection = $.hubConnection();
var ChatHub = connection.createHubProxy('chatHub');
Using the above I get a connection to the chatHub
but it does not recognise the .client the bellow:
// On New User Connected
debugger;
ChatHub.client.onNewUserConnected = function (id, name, userid) {
AddUser(ChatHub, id, name, userid);
}
If I use this at the beginning of the js file nothing works:
var ChatHub = $.connection.chatHub;
Is there something else that I could be missing?
I have de-installed the packages and re-installed them.
Any advice would greatly be appreciated to get this running.
Kind regards and thanks
I dont know if this might be the solution but give it a try
var hubConnection = new HubConnection("http://yourIpHere:yourPortNr/signalr");
var chatHubProxy = hubConnection.CreateHubProxy("chatHub");

Silence net::ERR_CONNECTION_REFUSED

Connecting to a non-existent web socket server results in loud errors being logged to the console, usually to the tune of ... net::ERR_CONNECTION_REFUSED.
Anyone have an idea for a hackaround to silence this output? XMLHttpRequest won't work since it yields the same verbose error output if the server is not reachable.
The goal here is to test if the server is available, if it is then connect to it, otherwise use a fallback, and to do this without spamming the console with error output.
Chrome itself is emitting these messages, and there is no way to block them. This is a function of how chrome was built; whenever a ResourceFetcher object attempts to fetch a resource, its response is passed back to its context, and if there's an error, the browser prints it to the console - see here.
Similar question can be found here.
If you'd like, you can use a chrome console filter as this question discusses to block these errors in your console, but there is no way to programmatically block the messages.
I don't know why do you want to prevent this error output. I guess you just want to get rid of them when debugging. So I provide a work around here may be just useful for debugging.
Live demo: http://blackmiaool.com/soa/43012334/boot.html
How to use it?
Open the demo page, click the "boot" button, it will open a new tab. Click the "test" button in the new tab and check the result below. If you want to get a positive result, change the url to wss://echo.websocket.org.
Why?
By using post message, we can make browser tabs communicate with each other. So we can move those error output to a tab that we don't concern.
P.S. You can refresh the target page freely without loosing the connection between it and boot page.
P.P.S You can also use storage event to achieve this.
boot.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>boot page</title>
</head>
<body>
<button onclick="boot()">boot</button>
<p>BTW, you can boot the page without the button if you are willing to allow the "pop-up"</p>
<script>
var targetWindow;
function init() {
targetWindow
}
function boot() {
targetWindow = window.open("target.html");
}
boot();
window.addEventListener('message', function(e) {
var msg = e.data;
var {
action,
url,
origin,
} = msg;
if (action === "testUrl") {
let ws = new WebSocket(url);
ws.addEventListener("error", function() {
targetWindow.postMessage({
action: "urlResult",
url,
data: false,
}, origin);
ws.close();
});
ws.addEventListener("open", function() {
targetWindow.postMessage({
action: "urlResult",
url,
data: true,
}, origin);
ws.close();
});
}
});
</script>
</body>
</html>
target.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>target page</title>
</head>
<body>
<h4>input the url you want to test:</h4>
<textarea type="text" id="input" style="width:300px;height:100px;">
</textarea>
<br>
<div>try <span style="color:red">wss://echo.websocket.org</span> for success result(may be slow)</div>
<button onclick="test()">test</button>
<div id="output"></div>
<script>
var origin = location.origin;
var testUrl = origin.replace(/^https?/, "ws") + "/abcdef"; //not available of course
document.querySelector("#input").value = testUrl;
function output(val) {
document.querySelector("#output").textContent = val;
}
function test() {
if (window.opener) {
window.opener.postMessage({
action: "testUrl",
url: document.querySelector("#input").value,
origin,
}, origin);
} else {
alert("opener is not available");
}
}
window.addEventListener('message', function(e) {
var msg = e.data;
if (msg.action === "urlResult") {
output(`test ${msg.url} result: ${msg.data}`);
}
});
</script>
</body>
</html>

How do I create chat rooms in Firechat?

I am using Firechat and I am able to successfully initiate the chat window. I am using Firebase custom authentication and I can login without any problem. However, I now try to create a new chat room and then enter it. Based on the Firechat documentation I did the following:
<!doctype html>
<html>
<head>
<title>Test</title>
<meta charset="UTF-8" />
<script src='https://cdn.firebase.com/js/client/2.0.2/firebase.js'></script>
<link rel='stylesheet' href='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.css' />
<script src='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.js'></script>
</head>
<body>
<script type='text/javascript'>
var fireBase = new Firebase("https://XXXXXXXXX.firebaseio.com/");
function initChat(authData) {
var Firechat = new FirechatUI(fireBase, document.getElementById('firechat'));
Firechat.setUser(authData.uid, "Username");
Firechat.createRoom("Test chat room", "public");
}
fireBase.authWithCustomToken("UNIQUE_TOKEN", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
console.log("Login successful", authData);
initChat(authData);
}
});
</script>
<div id='firechat'>
</div>
</body>
</html>
In the javascript console I can see that login is successful:
Login successful Object { auth: Object, expires: XXXXXXXXX, token: "XXXXXXXX…", uid: "XXXXXX", provider: "custom" }
But the createRoom function is not found:
TypeError: Firechat.createRoom is not a function
Any idea what is going wrong here?
From the docs:
Firechat.createRoom(roomName, roomType, callback(roomId))
Creates a new room with the given name (string) and type (string - public or private) and invokes the callback with the room ID on completion.
It would seem that you do not have a callback.
Firechat.prototype.createRoom = function(roomName, roomType, callback) {
var self = this,
newRoomRef = this._roomRef.push();
var newRoom = {
id: newRoomRef.name(),
name: roomName,
type: roomType || 'public',
createdByUserId: this._userId,
createdAt: Firebase.ServerValue.TIMESTAMP
};
if (roomType === 'private') {
newRoom.authorizedUsers = {};
newRoom.authorizedUsers[this._userId] = true;
}
newRoomRef.set(newRoom, function(error) {
if (!error) {
self.enterRoom(newRoomRef.name());
}
if (callback) {
callback(newRoomRef.name());
}
});
};
Source: https://firechat.firebaseapp.com/docs/firechat.html
So it turns out that there are two classes (is that the right word) used in the Firechat javascript plugin:
var chat = new FirechatUI
var chat = new Firechat
Because they seem so similar I did not notice the difference. Nowhere in the documentation have I been able to find details of the FirechatUI instance (even though this code is recommended on the github readme).
So anyway, the thing is that new FirechatUI loads the actual UI for the chat. new Firechat loads the API that allows you to talk to the chat plugin (but NOT to the UI). This is an important difference. The documentation found here only relates to the API so if you initiate a new Firechat instance. However, the trick is to get the UI up and running and then interact with it directly (doing things like creating new rooms or entering rooms). I have honestly not found out how to do this the official/recommended way. The only thing I've been able to come up with is a hack. It's ugly, but it works. The code below includes functionality to create a new chat room (using Firechat) and to open a particular chatroom in the UI (that bit is hacked as I couldn't find a way to interact with the UI directly).
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>Title</title>
<!-- jQuery -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js'></script>
<!-- Firebase -->
<script src='https://cdn.firebase.com/js/client/2.1.0/firebase.js'></script>
<!-- Firechat -->
<link rel='stylesheet' href='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.css' />
<script src='https://cdn.firebase.com/libs/firechat/2.0.1/firechat.min.js'></script>
<!-- This plugin here: https://gist.github.com/buu700/4200601 -->
<script type="text/javascript" src="js/arrive.js"></script>
<style type="text/css">
#firechat{width:400px}
</style>
</head>
<body>
<h1>Test</h1>
<script type='text/javascript'>
var fireBase = new Firebase("https://XXXXXX.firebaseio.com/");
function roomChatSetup(authData) {
var chat = new Firechat(fireBase);
chat.setUser(authData.uid, "My User Name", function(user) {
console.log("Creating chatroom...");
chat.createRoom("New Chatroom Name", "public", function(roomId) {
console.log("Created room "+roomId);
});
$("#firechat").html("<div class='alert alert-success'>Your chatroom has been set up. Refresh to view</div>");
});
}
function initChat(authData) {
var chatUI = new FirechatUI(fireBase, document.getElementById('firechat'));
chatUI.setUser(authData.uid, "My User Name");
console.log("Simulating clicks...");
$("#firechat-tab-content div.tab-pane").waitUntilExists(function(){
console.log("Close all other tabs by simulating clicking the X button");
$("#firechat-tab-content div.tab-pane:not(#XXXXXXXXX) a.close").click(); // XXXXX should have the chatroom name of the one you want to open
});
$("#firechat-btn-rooms").waitUntilExists(function(){
$("#firechat-btn-rooms").click();
console.log("Open submenu to load all possible rooms");
});
$("li[data-room-id='XXXXXXXXXXXXX']").waitUntilExists(function(){
$("li[data-room-id='XXXXXXXXXX'] a").click();
console.log("Simulating clicking on chatroom XXXXXXXXXXXXXX");
});
}
fireBase.authWithCustomToken("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
} else {
console.log("Login successful", authData);
// Here you can use a programming language to decide. If you already have a
// chatroom, run initChat. If you don't, run createRoom. I haven't been
// able to run them both at the same time.
initChat(authData);
// or:
roomChatSetup(authData);
}
});
</script>
<div id="firechat"></div>
</body>
</html>
The FirechatUI object is separate from the Firechat object. FirechatUI does not have the same methods that Firechat does.
In order to get the associated Firechat object from a FirechatUI object you can do the following:
let chatUI = new FirechatUI(chatRef, document.getElementById("firechat-wrapper"));
let chat = chatUI._chat;
You can then do any normal Firechat operations without any issues.
chat.setUser(user.uid, firstName, function(user) {
chat.resumeSession();
});
Please keep in mind that the _chat element is not really supposed to be used (as you can tell from the naming convention), but since FirechatUI does not properly expose enough functionality this is probably the cleanest way to do it.

Load a javascript method from Android Webview

I am trying to render a chart in an Android WebView using HighCharts.
What my app does should be simple :
- Load the HTML page and external (but local) css and js files
- Load data from the Java part of the app
- Call a javascript function with the previously-loaded data as a parameter
I setup my webview by activating Javascript :
mWebView.getSettings().setJavaScriptEnabled(true);
And also by setting a WebClient to catch javascript's console messages :
mWebView.setWebChromeClient(new WebChromeClient() {
public boolean onConsoleMessage(ConsoleMessage cm) {
Log.d(TAG, cm.message() + " -- From line "
+ cm.lineNumber() + " of "
+ cm.sourceId() );
return true;
}
});
This is my HTML page :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="style.css" />
<title>Chart</title>
<script src="jquery.js"></script>
</head>
<body>
<script src="highstock.js"></script>
<script src="exporting.js"></script>
<script language="javascript">
function plot() {
console.log("Hello !");
}
</script>
<div id="container" style="height: 100%; width: 100%"></div>
</body>
</html>
I load this page from the app's assets by calling
mWebView.loadUrl("file:///android_asset/page.html");
My external CSS file seems to be read and I suppose the external JS files are also correctly loaded
I read everywhere that I can call my javascript method anytime by calling
mWebView.loadUrl("javascript:plot()");
However, I always get the error
Uncaught ReferenceError: plot is not defined -- From line 1 of null
Is there anything I might have forgotten ?
Note that I load my page and call the javascript method right after the loadUrl call.
Thanks !
Ok, I'm sorry for your time loss. I was looking for an answer for hours, and then within 15 minutes of posting the question I found the solution.
The problem seemed to be the two consecutive calls to loadUrl. Maybe the page wasn't properly loaded yet while I was already calling the javascript function.
I added a load listener on the WebView like this
mWebView.setWebViewClient(new WebViewClient() {
#Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
if (url.equals("file:///android_asset/page.html")) {
mWebView.loadUrl("javascript:plot();");
}
}
});
so that I call the javascript method only when the page is completely loaded.
Now it works properly on every call.
Thank you again for your attention !
Try this:
<body>
<script src="highstock.js"></script>
<script src="exporting.js"></script>
<script language="javascript">
function plot() {
console.log("Hello !");
}
plot(); //Note the calling of the function within the HTML file once loaded into webView
</script>
<div id="container" style="height: 100%; width: 100%"></div>
</body>
And remove the line:
mWebView.loadUrl("javascript:plot()");
If you want to pass data to your javascript function, hen send your data through the URL like this:
mWebView.loadUrl("file:///android_asset/page.html?data=something");
and use:
var param1var = getQueryVariable("data");
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
alert('Query Variable ' + variable + ' not found');
}
in your JavaScript like what is done here. You can also check out this example.

force browsers to get latest js and css files in asp.net application

Some browsers cache js and css files, failing to refresh them unless you force them to. What's the easiest way.
I just implemented this solution that seems to work.
Declare a version variable on your page
public string version { get; set; }
Get the version number from web.config key
version = ConfigurationManager.AppSettings["versionNumber"];
In your aspx page make the calls to javascript and stylesheets like so
<script src="scripts/myjavascript.js?v=<%=version %>" type="text/javascript"></script>
<link href="styles/mystyle.css?v=<%=version %>" rel="stylesheet" type="text/css" />
So if you set the version = 1.1 from 1.0 in your web.config your browser will download the latest files which will hopefully save you and your users some frustration.
Is there another solution that works better, or will this cause any unforeseen issues for a website?
I solved this by tacking a last modified timestamp as a query parameter to the scripts.
I did this with an extension method, and using it in my CSHTML files. Note: this implementation caches the timestamp for 1 minute so we don't thrash the disk quite so much.
Here is the extension method:
public static class JavascriptExtension {
public static MvcHtmlString IncludeVersionedJs(this HtmlHelper helper, string filename) {
string version = GetVersion(helper, filename);
return MvcHtmlString.Create("<script type='text/javascript' src='" + filename + version + "'></script>");
}
private static string GetVersion(this HtmlHelper helper, string filename)
{
var context = helper.ViewContext.RequestContext.HttpContext;
if (context.Cache[filename] == null)
{
var physicalPath = context.Server.MapPath(filename);
var version = $"?v={new System.IO.FileInfo(physicalPath).LastWriteTime.ToString("MMddHHmmss")}";
context.Cache.Add(filename, version, null,
DateTime.Now.AddMinutes(5), TimeSpan.Zero,
CacheItemPriority.Normal, null);
return version;
}
else
{
return context.Cache[filename] as string;
}
}
}
And then in the CSHTML page:
#Html.IncludeVersionedJs("/MyJavascriptFile.js")
In the rendered HTML, this appears as:
<script type='text/javascript' src='/MyJavascriptFile.js?20111129120000'></script>
In ASP.NET Core (MVC 6) this works out of the box via the asp-append-version tag helper:
<script src="scripts/myjavascript.js" asp-append-version="true"></script>
<link href="styles/mystyle.css rel="stylesheet" asp-append-version="true" />
Your solution works. It is quite popular in fact.
Even Stack Overflow uses a similar method:
<link rel="stylesheet" href="http://sstatic.net/so/all.css?v=6184">
Where v=6184 is probably the SVN revision number.
ASP.NET MVC will handle this for you if you use bundles for your JS/CSS. It will automatically append a version number in the form of a GUID to your bundles and only update this GUID when the bundle is updated (aka any of the source files have changes).
This also helps if you have a ton of JS/CSS files as it can greatly improve content load times!
See Here
There are a built-in way in asp.net for this: bundling. Just use it. Each new version will have unique suffix "?v=XXXXXXX". In debug mode bundling is off, for switching on make setting in web.config:
<system.web>
<compilation debug="false" />
</system.web>
Or add to the method RegisterBundles(BundleCollection bundles) :
BundleTable.EnableOptimizations = true;
For example:
BundleConfig.cs :
bundles.Add(new ScriptBundle("~/Scripts/myjavascript.js")
.Include("~/Scripts/myjavascript.js"));
bundles.Add(new StyleBundle("~/Content/mystyle.css")
.Include("~/Content/mystyle.css"));
_Layout.cshtml :
#Scripts.Render("~/Scripts/myjavascript.js")
#Styles.Render("~/Content/mystyle.css")
I wanted a simple one liner to make the path unique to bust the cache. This worked for me:
<script src="scripts/main.js?bust_js_cache=<%=System.IO.File.GetLastWriteTime(Server.MapPath("scripts/main.js")).ToString("HH:mm:ss")%>" type="text/javascript"></script>
If the file has been modified since the last time it was loaded on the page the browser will pull the updated file.
It generates the last modified stamp from the .js file and chucks it in there instead of the version which may not be easy to gain access to.
<script src="scripts/main.js?bust_js_cache=10:18:38" type="text/javascript"></script>
Another option could be to get the checksum of the file.
There is a simpler answer to this than the answer given by the op in the question (the approach is the same):
Define the key in the web.config:
<add key="VersionNumber" value="06032014"/>
Make the call to appsettings directly from the aspx page:
<link href="styles/navigation.css?v=<%=ConfigurationManager.AppSettings["VersionNumber"]%>" rel="stylesheet" type="text/css" />
Based on Adam Tegan's answer, modified for use in a web forms application.
In the .cs class code:
public static class FileUtility
{
public static string SetJsVersion(HttpContext context, string filename) {
string version = GetJsFileVersion(context, filename);
return filename + version;
}
private static string GetJsFileVersion(HttpContext context, string filename)
{
if (context.Cache[filename] == null)
{
string filePhysicalPath = context.Server.MapPath(filename);
string version = "?v=" + GetFileLastModifiedDateTime(context, filePhysicalPath, "yyyyMMddhhmmss");
return version;
}
else
{
return string.Empty;
}
}
public static string GetFileLastModifiedDateTime(HttpContext context, string filePath, string dateFormat)
{
return new System.IO.FileInfo(filePath).LastWriteTime.ToString(dateFormat);
}
}
In the aspx markup:
<script type="text/javascript" src='<%= FileUtility.SetJsVersion(Context,"/js/exampleJavaScriptFile.js") %>'></script>
And in the rendered HTML, it appears as
<script type="text/javascript" src='/js/exampleJavaScriptFile.js?v=20150402021544'></script>
Here's an approach that works with ASP.NET 5 / MVC 6 / vNext.
Step 1: Create a class to return the last write time of the file, similar to other answers in this thread. Note, this requires ASP.NET 5 (or other) dependency injection.
public class FileVersionService
{
private IHostingEnvironment _hostingEnvironment;
public FileVersionService(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
public string GetFileVersion(string filename)
{
var path = string.Format("{0}{1}", _hostingEnvironment.WebRootPath, filename);
var fileInfo = new FileInfo(path);
var version = fileInfo.LastWriteTimeUtc.ToString("yyyyMMddhhmmssfff");
return version;
}
}
Step 2: Register the service to be injected inside startup.cs:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<FileVersionService>();
...
}
Step 3: Then, in ASP.NET 5, it is possible to inject the service directly into a layout view such as _Layout.cshtml like this:
#inject Namespace.Here.FileVersionService fileVersionService
<!DOCTYPE html>
<html lang="en" class="#ViewBag.HtmlClass">
<head>
...
<link href="/css/styles.css?v=#fileVersionService.GetFileVersion("\\css\\styles.css")" rel="stylesheet" />
...
</head>
<body>
...
</body>
There are some finishing touches that could be done to combine physical paths better and handle the file name in a style more consistent with the syntax, but this is a starting point. Hope it helps people moving to ASP.NET 5.
Interestingly, this very site has issues with the approach you describe in connection with some proxy setups, even though it should be fail-safe.
Check this Meta Stack Overflow discussion.
So in light of that, it might make sense not to use a GET parameter to update, but the actual file name:
href="/css/scriptname/versionNumber.css"
even though this is more work to do, as you'll have to actually create the file, or build a URL rewrite for it.
I have employed a slightly different technique in my aspnet MVC 4 site:
_ViewStart.cshtml:
#using System.Web.Caching
#using System.Web.Hosting
#{
Layout = "~/Views/Shared/_Layout.cshtml";
PageData.Add("scriptFormat", string.Format("<script src=\"{{0}}?_={0}\"></script>", GetDeployTicks()));
}
#functions
{
private static string GetDeployTicks()
{
const string cacheKey = "DeployTicks";
var returnValue = HttpRuntime.Cache[cacheKey] as string;
if (null == returnValue)
{
var absolute = HostingEnvironment.MapPath("~/Web.config");
returnValue = File.GetLastWriteTime(absolute).Ticks.ToString();
HttpRuntime.Cache.Insert(cacheKey, returnValue, new CacheDependency(absolute));
}
return returnValue;
}
}
Then in the actual views:
#Scripts.RenderFormat(PageData["scriptFormat"], "~/Scripts/Search/javascriptFile.min.js")
Starting from the above answer I modified a little bit the code to make the helper work with CSS files too and add a version every time when you do some change in the files and not only when you do the build
public static class HtmlHelperExtensions
{
public static MvcHtmlString IncludeVersionedJs(this HtmlHelper helper, string filename)
{
string version = GetVersion(helper, filename);
return MvcHtmlString.Create("<script type='text/javascript' src='" + filename + version + "'></script>");
}
public static MvcHtmlString IncludeVersionedCss(this HtmlHelper helper, string filename)
{
string version = GetVersion(helper, filename);
return MvcHtmlString.Create("<link href='" + filename + version + "' type ='text/css' rel='stylesheet'/>");
}
private static string GetVersion(this HtmlHelper helper, string filename)
{
var context = helper.ViewContext.RequestContext.HttpContext;
var physicalPath = context.Server.MapPath(filename);
var version = "?v=" +
new System.IO.FileInfo(physicalPath).LastWriteTime
.ToString("yyyyMMddHHmmss");
context.Cache.Add(physicalPath, version, null,
DateTime.Now.AddMinutes(1), TimeSpan.Zero,
CacheItemPriority.Normal, null);
if (context.Cache[filename] == null)
{
context.Cache[filename] = version;
return version;
}
else
{
if (version != context.Cache[filename].ToString())
{
context.Cache[filename] = version;
return version;
}
return context.Cache[filename] as string;
}
}
}
<?php $rand_no = rand(10000000, 99999999)?>
<script src="scripts/myjavascript.js?v=<?=$rand_no"></script>
This works for me in all browsers. Here I have used PHP to generate random no. You can use your own server side language.`
Get file modified time, as shown below
private static string GetLastWriteTimeForFile(string pathVal)
{
return System.IO.File.GetLastWriteTime(HostingEnvironment.MapPath(pathVal)).ToFileTime().ToString();
}
Append this with input as querystring
public static string AppendDateInFile(string pathVal)
{
var patheWithDate = new StringBuilder(pathVal);
patheWithDate.AppendFormat("{0}x={1}",
pathVal.IndexOf('?') >= 0 ? '&' : '?',
GetLastWriteTimeForFile(pathVal));
return patheWithDate.ToString();
}
Call this from markup.
MVC Extension Helper Approach
Add an extension method
namespace TNS.Portal.Helpers
{
public static class ScriptExtensions
{
public static HtmlString QueryStringScript<T>(this HtmlHelper<T> html, string path)
{
var file = html.ViewContext.HttpContext.Server.MapPath(path);
DateTime lastModified = File.GetLastWriteTime(file);
TagBuilder builder = new TagBuilder("script");
builder.Attributes["src"] = path + "?modified=" + lastModified.ToString("yyyyMMddhhmmss");
return new HtmlString(builder.ToString());
}
public static HtmlString QueryStringStylesheet<T>(this HtmlHelper<T> html, string path)
{
var file = html.ViewContext.HttpContext.Server.MapPath(path);
DateTime lastModified = File.GetLastWriteTime(file);
TagBuilder builder = new TagBuilder("link");
builder.Attributes["href"] = path + "?modified=" + lastModified.ToString("yyyyMMddhhmmss");
builder.Attributes["rel"] = "stylesheet";
return new HtmlString(builder.ToString());
}
}
}
Add this namespace in web.config
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="TNS.Portal" />
<add namespace="TNS.Portal.Helpers" />
</namespaces>
</pages>
</system.web.webPages.razor>
Use it in view as
#Html.QueryStringScript("/Scripts/NPIAjaxCalls.js")
#Html.QueryStringStylesheet("/Content/StyledRadio.css")
Simplified prior suggestions and providing code for .NET Web Forms developers.
This will accept both relative ("~/") and absolute urls in the file path to the resource.
Put in a static extensions class file, the following:
public static string VersionedContent(this HttpContext httpContext, string virtualFilePath)
{
var physicalFilePath = httpContext.Server.MapPath(virtualFilePath);
if (httpContext.Cache[physicalFilePath] == null)
{
httpContext.Cache[physicalFilePath] = ((Page)httpContext.CurrentHandler).ResolveUrl(virtualFilePath) + (virtualFilePath.Contains("?") ? "&" : "?") + "v=" + File.GetLastWriteTime(physicalFilePath).ToString("yyyyMMddHHmmss");
}
return (string)httpContext.Cache[physicalFilePath];
}
And then call it in your Master Page as such:
<link type="text/css" rel="stylesheet" href="<%= Context.VersionedContent("~/styles/mystyle.css") %>" />
<script type="text/javascript" src="<%= Context.VersionedContent("~/scripts/myjavascript.js") %>"></script>
The main problem with doing it this way is mainly that you will need to remember to update your version number in your code every time you make any change to your css or js files.
A possibly better way to do it is to set a guaranteed unique parameter with each of your css or js files, like so:
<script src="scripts/myjavascript.js?_=<%=DateTime.Now.Ticks%>" type="text/javascript"></script>
<link href="styles/mystyle.css?_=<%=DateTime.Now.Ticks%>" rel="stylesheet" type="text/css" />
This forces the files to be requested from the server every single time, which also means that your site will not be as performant upon page load, since those files will never be cached, and will use unneeded bandwidth each time.
Essentially, if you can remember to update the version number every time a change is made, you can get away with how you're doing it.
Based on the above answer I've written a small extension class to work with CSS and JS files:
public static class TimestampedContentExtensions
{
public static string VersionedContent(this UrlHelper helper, string contentPath)
{
var context = helper.RequestContext.HttpContext;
if (context.Cache[contentPath] == null)
{
var physicalPath = context.Server.MapPath(contentPath);
var version = #"v=" + new FileInfo(physicalPath).LastWriteTime.ToString(#"yyyyMMddHHmmss");
var translatedContentPath = helper.Content(contentPath);
var versionedContentPath =
contentPath.Contains(#"?")
? translatedContentPath + #"&" + version
: translatedContentPath + #"?" + version;
context.Cache.Add(physicalPath, version, null, DateTime.Now.AddMinutes(1), TimeSpan.Zero,
CacheItemPriority.Normal, null);
context.Cache[contentPath] = versionedContentPath;
return versionedContentPath;
}
else
{
return context.Cache[contentPath] as string;
}
}
}
Instead of writing something like:
<link href="#Url.Content(#"~/Content/bootstrap.min.css")" rel="stylesheet" type="text/css" />
<script src="#Url.Content(#"~/Scripts/bootstrap.min.js")"></script>
You can now write:
<link href="#Url.VersionedContent(#"~/Content/bootstrap.min.css")" rel="stylesheet" type="text/css" />
<script src="#Url.VersionedContent(#"~/Scripts/bootstrap.min.js")"></script>
I.e. simply replace Url.Content with Url.VersionedContent.
Generated URLs look something like:
<link href="/Content/bootstrap.min.css?v=20151104105858" rel="stylesheet" type="text/css" />
<script src="/Scripts/bootstrap.min.js?v=20151029213517"></script>
If you use the extension class you might want to add error handling in case the MapPath call doesn't work, since contentPath isn't a physical file.
I use a similar way to do the same you are doing without modifying each page. Added a PreRender event is master file. It keeps my logic at one place and applicable to both js and css files.
protected void Page_PreRender(object sender, EventArgs e)
{
HtmlLink link = null;
LiteralControl script = null;
foreach (Control c in Header.Controls)
{
//StyleSheet add version
if (c is HtmlLink)
{
link = c as HtmlLink;
if (link.Href.EndsWith(".css", StringComparison.InvariantCultureIgnoreCase))
{
link.Href += string.Format("?v={0}", ConfigurationManager.AppSettings["agVersion"]);
}
}
//Js add version
if (c is LiteralControl)
{
script = c as LiteralControl;
if (script.Text.Contains(".js"))
{
var foundIndexes = new List<int>();
for (int i = script.Text.IndexOf(".js\""); i > -1; i = script.Text.IndexOf(".js\"", i + 1))
{
foundIndexes.Add(i);
}
for (int i = foundIndexes.Count - 1; i >= 0; i--)
{
script.Text = script.Text.Insert(foundIndexes[i] + 3, string.Format("?v={0}", ConfigurationManager.AppSettings["agVersion"]));
}
}
}
}
}
You can override the DefaultTagFormat property of Scripts or Styles.
Scripts.DefaultTagFormat = #"<script src=""{0}?v=" + ConfigurationManager.AppSettings["pubversion"] + #"""></script>";
Styles.DefaultTagFormat = #"<link href=""{0}?v=" + ConfigurationManager.AppSettings["pubversion"] + #""" rel=""stylesheet""/>";
For resolving this issue in my ASP.Net Ajax application, I created an extension and then called in the master page.
For more details, you can go through the link.
Easy and smart way to implement css versioning in .net application by below concept.. no need to write back-end code.
<link href="<%="../../App_Themes/Base/css/main.css?v="+ DateTime.Now.ToString("yyyyMMddhhmmss") +""%>" rel="stylesheet" />
For ASP.NET pages I am using the following
BEFORE
<script src="/Scripts/pages/common.js" type="text/javascript"></script>
AFTER (force reload)
<script src="/Scripts/pages/common.js?ver<%=DateTime.Now.Ticks.ToString()%>" type="text/javascript"></script>
Adding the DateTime.Now.Ticks works very well.
just put this inside system.webserver in web.config
<caching enabled="true" enableKernelCache="true">
<profiles>
<add extension=".html" policy="CacheUntilChange" kernelCachePolicy="CacheUntilChange"/>
<add extension=".css" policy="CacheUntilChange" kernelCachePolicy="CacheUntilChange"/>
<add extension=".js" policy="CacheUntilChange" kernelCachePolicy="CacheUntilChange"/>
</profiles>
</caching>

Categories