I am currently working on a Java application that uses a JavaFX webview to display its UI (with HTML/CSS).
Everything is working fine but I have trouble loading a new page in the system. When I do, the communication between the Java and the new page's JavaScript seems to be broken.
Here is my code :
** Broser **
public class Browser extends Region {
final WebView browser = new WebView();
final WebEngine webEngine = browser.getEngine();
public Browser() {
//apply the styles
getStyleClass().add("browser");
// load the web page
webEngine.load(some_url);
JSObject jsobj = (JSObject) webEngine.executeScript("window");
Bridge bridge = Bridge.getInstance();
bridge.init(webEngine);
jsobj.setMember("java", bridge);
//add the web view to the scene
getChildren().add(browser);
}
}
** Bridge **
public class Bridge {
private static Bridge instance = null;
private WebEngine webEngine;
public Bridge () {
}
public static Bridge getInstance() {
if(instance == null){
instance = new Bridge();
}
return instance;
}
public void init(WebEngine webEngine) {
if(this.webEngine == null) {
this.webEngine = webEngine;
}
}
public void btnStartSessionOnClick(String sessionName, String speakerNickname) {
// Load the new page
webEngine.load(some_other_url);
}
}
Whenever the web engine loads a new page, it replaces the DOM, so there is a different window object. The jsobj you define is only set once, so when a new page is loaded it will be pointing to the wrong object. You need to reset this object every time the page loads, which you can do by observing the engine's load state.
Your design doesn't make a whole lot of sense to me: it makes more sense to me to have the window (jsobj) object as part of the Bridge class, rather than the application class. And since the Browser is not a singleton, it doesn't make sense to make the Bridge a singleton (what if you had multiple web views in your application, for example?).
Here's an SSCCE:
package application;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
public class WebViewTest extends Application {
#Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
WebEngine engine = webView.getEngine();
Label output = new Label();
Bridge bridge = new Bridge(engine);
engine.load(getClass().getResource("/resources/First.html").toExternalForm());
Button first = new Button("Load First");
first.setOnAction(e -> engine.load(getClass().getResource("/resources/First.html").toExternalForm()));
Button second = new Button("Load Second");
second.setOnAction(e -> engine.load(getClass().getResource("/resources/Second.html").toExternalForm()));
TextField textField = new TextField();
Button button = new Button("Send");
EventHandler<ActionEvent> handler = e -> {
bridge.execute(result -> output.setText(result.toString()),
"showText", textField.getText());
textField.setText("");
};
button.setOnAction(handler);
textField.setOnAction(handler);
HBox controls = new HBox(5, first, second, textField, button, new Label("Web page says: "), output);
controls.setPadding(new Insets(10));
BorderPane root = new BorderPane(webView, null, null, controls, null);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The Bridge class:
package application;
import java.util.function.Consumer;
import javafx.concurrent.Worker.State;
import javafx.scene.web.WebEngine;
import netscape.javascript.JSObject;
public class Bridge {
private JSObject window ;
public Bridge(WebEngine engine) {
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
if (newState == State.SUCCEEDED) {
window = (JSObject) engine.executeScript("window");
window.setMember("application", this);
}
});
}
public void execute(Consumer<Object> callback, String function, Object... args) {
callback.accept(window.call(function, args));
}
}
And some simple test HTML files, which I have in a resources folder in the root of the classpath.
First.HTML:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>First</title>
<script>
function showText(text) {
document.getElementById("text").innerHTML = text;
return text;
}
</script>
</head>
<body>
<p>This is the first page</p>
Go to the second page
<div id="text"></div>
</body>
</html>
and Second.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Second</title>
<script>
function showText(text) {
document.getElementById("text").innerHTML = text;
return text;
}
</script>
</head>
<body>
<p>This is the second page</p>
Go back to the first page
<div id="text"></div>
</body>
</html>
This is the entire Java code I've used. I will explain in more detail below...
public class Test7 extends Activity {
//debug
private final static String TAG = "JSInterface";
private WebView wv;
private class JSInterface {
private WebView wv;
// Variables to manage interfacing with JS
private String returnValue;
private boolean canReadReturnValue;
private Lock lockOnJS;
private Condition condVarOnJS;
public JSInterface (WebView wv) {
this.wv = wv;
this.canReadReturnValue = false;
this.lockOnJS = new ReentrantLock();
this.condVarOnJS = lockOnJS.newCondition();
}
public void setReturnValue(String ret) {
lockOnJS.lock();
returnValue = ret;
canReadReturnValue = true;
condVarOnJS.signal();
lockOnJS.unlock();
Log.d(TAG, "returnValue = " + returnValue);
}
public String getReturnValue() {
Log.d(TAG, "enter in getReturnValue");
lockOnJS.lock();
while (!canReadReturnValue) {
try {
Log.d(TAG, "get wait...");
condVarOnJS.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lockOnJS.unlock();
Log.d(TAG, "returnValue: " + returnValue);
return returnValue;
}
public String getNewString() {
wv.loadUrl("javascript:JSInterface.setReturnValue(createNewString())");
return getReturnValue();
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
wv = (WebView) findViewById(R.id.webView1);
wv.getSettings().setJavaScriptEnabled(true);
wv.addJavascriptInterface(new JSInterface(wv), "JSInterface");
wv.loadUrl("file:///android_asset/prova7.html");
}
public void button1(View v) {
wv.loadUrl("javascript:func('1')");
}
}
And it seems work fine.
You can see that I've got a button (that we can call button1), and clicking on it, it tries to execute a JS method, called func().
public void button1(View v) {
wv.loadUrl("javascript:func('1')");
}
Inside this JS method, I have to call another Java method. This is the code:
function func(id) {
document.getElementById(id).innerHTML = JSInterface.getNewString();
}
I need to return the result of JSInterface.getNewString() to the innerHTML variable.
The code of JSInterface.getNewString() is this:
public String getNewString() {
wv.loadUrl("javascript:JSInterface.setReturnValue(createNewString())");
return getReturnValue();
}
You can see that I use the method setReturnValue and getReturnValue to return the value returned by another JS method. This is the code:
function createNewString() {
return "my New String";
}
The problem is that when I try to set the returnValue, the function createNewString is never executed! If I add a console.log() line, my logCat display nothing!
I cannot understand why this happens.
All the javascript and your JSInterface methods called from javascript are running on the single thread in Android WebView. So while you are waiting in condVarOnJS.await() no javascript can be executed, just because it is executed on the same thread.
Moreover, all the webview instances in your application share the same javascript thread.
In Internet Explorer I found the same problem. You can use setTimeout like this:
function func(id) {
setTimeout(
function(){
document.getElementById(id).innerHTML = JSInterface.getNewString();
},
500);
}
I did the functionality what you intend in that code,for me createNewString() is called,
I will show up the code i used,
In java
,
public String videoPlay(){
System.out.println("videoPlay");
mWebView.loadUrl("javascript:window.demo.setReturnValue(createNewString())");
return getReturnValue();}
public void setReturnValue(String test){
rotValue=test;
System.out.println(test);
}
public String getReturnValue(){
System.out.println("get"+rotValue);
return rotValue;
}
in HTML,
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script>
function inform(){
alert('et');
document.getElementById('myText').value=window.demo.videoPlay();
alert('et');
}
function createNewString() {
return "my New String";
}
</script>
</head>
<body onload="init()">
<form>
<input type='text' id='myText' />
<input type="button" name="test" value="Click me" onclick="inform()">
</form>
</body>
</html>
The function getter and setter called and values also set, but i have the output log as..
11-08 19:18:17.155: INFO/System.out(847): videoPlay
11-08 19:18:17.165: INFO/System.out(847): getnull
11-08 19:18:17.875: INFO/System.out(847): my New String
videoPlay called from JS and createnewString() also called from java to JS , but it returns the value before it set , because i don`t what is the purpose to use lock , even i tried using lock as you did for that it will print
11-08 19:18:17.155: INFO/System.out(847): videoPlay
11-08 19:18:17.165: INFO/System.out(847): getnull
using lock also the function callback works in wrong manner, you need work on locks.
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>