I am looking for a way to trigger a custom event in javascript from C# code written in a Windows Runtime Component with the AllowForWeb Attribute.
I know that the other way around works (triggering an event from javascript to C#) like this
public delegate void NotifyAppHandler( string str );
[AllowForWeb]
public sealed class WebViewInjectionObject
{
public event NotifyAppHandler OnNotifyApp;
public void NotifyApp( string str )
{
OnNotifyApp?.Invoke( str );
}
}
You can then subscribe to the event in C# and trigger it from javascript like this
nativeObject.notifyApp(JSON.stringify(callInfo));
Where nativeObject has been injected into the WebView with AddWebAllowedObject
Now I am looking for a solution to do the opposite.
I want to create an event in javascript and trigger it from C# using again the injected object from the Windows Runtime Component.
You can assume that I have control over the javascript code that will run in the WebView and can make changes there if required.
Note: The need to do this quite unorthodox thing arises from the fact that I cannot use/don't want to use InvokeScriptAsync due to this issue
Related
I just started playing around with Blazor and I can already see the great potential of this new framework.
I'm wondering, though, how it will handle doing simple things like setting focus on an input control? For instance, after I handle a click event, I want to set the focus to a text input control. Do I have to use JQuery for something like that, or will Blazor have some built-in methods for that sort of thing?
Thanks
Update: I posted an answer below with an example of how to set the focus to a control by invoking a JavaScript function from the .Net code.
As of right now (Blazor 0.9.0) you create your JavaScript functions in the Index.html (or reference them from Index.html) and then in your Blazor page or component you call JsRuntime.InvokeAsync("functionName", parms);
https://learn.microsoft.com/en-us/aspnet/core/razor-components/javascript-interop
Blazor is just the replacement (to be more precise "value addition") to JavaScript. It is a client-side only solution (but it might add some easy binding to ASP.NET in the future).
Still, it's completely based on HTML and CSS. C# is replacing the JS part using web assembly. So nothing has changed on how you access / modify HTML controls.
As of now (version 0.1.0) you have to rely on HTML DOM focus() Method to do what you intend to do (yes you have to use JavaScript as of now :( ).
// Not tested code
// This is JavaScript.
// Put this inside the index.html. Just below <script type="blazor-boot"></script>
<script>
Blazor.registerFunction('Focus', (controlId) => {
return document.getElementById(controlId).focus();
});
</script>
//and then wrap it for calls from .NET:
// This is C#
public static object Focus(string controlId)
{
return RegisteredFunction.Invoke<object>("Focus", controlId);
//object type is used since Invoke does not have a overload for void methods. Don't know why.
//this will return undefined according to js specs
}
For more information, you can refer to below.
If you want to improve the packaging of JS neatly, you can do something like this:
https://stackoverflow.com/a/49521216/476609
public class BlazorExtensionScripts : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
builder.OpenElement(0, "script");
builder.AddContent(1, "Blazor.registerFunction('Focus', (controlId) => { document.getElementById(controlId).focus(); });");
builder.CloseElement();
}
public static void Focus(string controlId)
{
RegisteredFunction.Invoke<object>("Focus", controlId);
}
}
then add this component to the root: (App.cshtml):
<BlazorExtensionScripts></BlazorExtensionScripts>
<Router AppAssembly=typeof(Program).Assembly />
I want to add a more up-to-date (as of 0.9.0) example of calling a JavaScript function to set the focus to another control after some event, like clicking on a button. This might be helpful for someone just starting out with Blazor (like me).
This example builds on the example code in the Blazor documentation "Build Your First Blazor Components App" at https://learn.microsoft.com/en-us/aspnet/core/tutorials/build-your-first-razor-components-app?view=aspnetcore-3.0
First, follow all the instructions in the documentation. When you have a working To-Do List page, then add the following:
At the bottom of Index.html, under wwwroot, and below the script tag that loads the webassembly.js, add the following script:
<script>
window.MySetFocus = (ctrl) => {
document.getElementById(ctrl).focus();
return true;
}
</script>
At the top of your todo.cshtml page, add the following using statement:
#inject IJSRuntime JsRuntime;
In the #functions section of your todo.cshtml page, add the following function:
async void Focus(string controlId)
{
var obj = JsRuntime.InvokeAsync<string>(
"MySetFocus", controlId);
}
In the AddToDo() function, just below the line where you set the "newToDo" variable to an empty string, add a call to the Focus function, passing in the string id of the input control. (The example in the docs does not assign an ID to the input control, so just add one yourself. I named mine "todoItem").
void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
Focus("todoItem"); // this is the new code
}
}
Build and run your app. When you click the add new item button, the new item should be added to the list, the input control blanked out, and the focus should be back in the input control, ready for another item to be added.
From .NET 5 Preview 8
Set UI focus in Blazor apps
Blazor now has a FocusAsync convenience method on ElementReference for setting the UI focus on that element.
<button #onclick="() => textInput.FocusAsync()">Set focus</button>
<input #ref="textInput"/>
You can't directly call JavaScript function. You are required to first register your functions like,
<script>
Blazor.registerFunction('ShowControl', (item) => {
var txtInput = document.getElementById("txtValue");
txtInput.style.display = "";
txtInput.value = item;
txtInput.focus();
});
return true;
</script>
Then you need to declare a method in C# which calls this JavaScript function. Like,
private void CallJavaScript()
{
RegisteredFunction.Invoke<bool>("ShowControl", itemName);
}
You can call this C# method on click of button. Like,
<button id="btnShow" class="btn btn-primary" #onclick(CallJavaScript)>Show</button>
This post Create a CRUD App using Blazor and ASP.NET Core
shows a working demo of calling JavaScript from Blazor.
Since the way you call javascript on a WebView is through loadUrl("javascript: ... "); The keyboard cannot stay open.
The loadUrl() method calls loadUrlImpl() , which calls a method called clearHelpers() which then calls clearTextEntry(), which then calls hideSoftKeyboard() and then we become oh so lonely as the keyboard goes away.
As far as I can see all of those are private and cannot be overridden.
Has anyone found a workaround for this? Is there a way to force the keyboard to stay open or to call the javascript directly without going through loadUrl()?
Is there anyway to override the WebView in a way to prevent (the private method) clearTextEntry() from being called?
Update
KitKat added a public method for invoking javascript directly: evaluateJavascript()
For older apis, you could try a solution like below, but if I had to do this again I'd look at just building an compatibility method that on KitKat uses the above method and on older devices, uses reflection to drill down to a inner private method: BrowserFrame.stringByEvaluatingJavaScriptFromString()
Then you could call javascript directly without having to deal with loadUrl and adding "javascript: " to the script.
Old Answer
As requested by Alok Kulkarni, I'll give a rough overview of a possible workaround I thought of for this. I haven't actually tried it but in theory it should work. This code is going to be rough and is just to serve as an example.
Instead of sending the calls down through loadUrl(), you queue your javascript calls and then have javascript pull them down. Some thing like:
private final Object LOCK = new Object();
private StringBuilder mPendingJS;
public void execJS(String js) {
synchronized(LOCK) {
if (mPendingJS == null) {
mPendingJS = new StringBuilder();
mPendingJS.append("javascript: ");
}
mPendingJS
.append(js)
.append("; ");
}
}
Instead of calling loadUrl() call that method. (For making this simple I used a synchronized block, but this might be better suited to a different route. Since javascript runs on its own thread, this will need to be thread safe in some way or another).
Then your WebView would have an interface like this:
public class JSInterface {
public String getPendingJS() {
synchronized(LOCK) {
String pendingCommands = mPendingJS.toString();
mPendingJS.setLength(0);
mPendingJS.append("javascript: ");
return pendingCommands;
}
}
}
That returns a String with the pending commands and clears them so they don't get returned again.
You would add it to the WebView like this:
mWebView.addJavascriptInterface(new JSInterface(), "JSInterface");
Then in your javascript you would set some interval in which to flush the pending commands. On each interval it would call JSInterface.getPendingJS() which would return a String of all of the pending commands and then you could execute them.
You could further improve this by adding a check in the execJS method to see if a EditText field exists in the WebView and is in focus. If there is one, then you would use this queueing method, but if there wasn't one in focus then you could just call loadUrl() like normal. That way it only uses this workaround when it actually needs to.
Regarding older APIs (pre 19), I used a similar method to the excepted answer, but slightly different.
First, I keep track of if the keyboard is displayed by using javascript in the webview roughly like so:
document.addEventListener( "focus", function(e){
var el = e.target;
reportKeyboardDisplayedToJava( isInputElement( el ) );
}, true);
document.addEventListener( "blur", function(e){
reportKeyboardDisplayedToJava( false );
}, true);
If the keyboard is displayed, and a js injection is attempted by the Android Java layer – I “defer” that injection. I add it to a string list, allow the user to finish up their input, and then upon the keyboard disappearing, I detect that and execute the backlog of injections.
I could implement cottonBallPaws's idea to use the internals of WebView with reflection, and got it to work for my 4.2 device. There are gracious fallbacks for Android versions older than KitKat.
The code is written in Xamarin, but it should be easily adaptable to native Java code.
/// <summary>
/// Executes a JavaScript on an Android WebView. This method offers fallbacks for older
/// Android versions, to avoid closing of the soft keyboard when executing JavaScript.
/// </summary>
/// <param name="webView">The WebView to run the JavaScript.</param>
/// <param name="script">The JavaScript code.</param>
private static void ExecuteJavaScript(Android.Webkit.WebView webView, string script)
{
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat)
{
// Best way for Android level 19 and above
webView.EvaluateJavascript(script, null);
}
else
{
try
{
// Try to do with reflection
CompatExecuteJavaScript(webView, script);
}
catch (Exception)
{
// Fallback to old way, which closes any open soft keyboard
webView.LoadUrl("javascript:" + script);
}
}
}
private static void CompatExecuteJavaScript(Android.Webkit.WebView androidWebView, string script)
{
Java.Lang.Class webViewClass = Java.Lang.Class.FromType(typeof(Android.Webkit.WebView));
Java.Lang.Reflect.Field providerField = webViewClass.GetDeclaredField("mProvider");
providerField.Accessible = true;
Java.Lang.Object webViewProvider = providerField.Get(androidWebView);
Java.Lang.Reflect.Field webViewCoreField = webViewProvider.Class.GetDeclaredField("mWebViewCore");
webViewCoreField.Accessible = true;
Java.Lang.Object mWebViewCore = webViewCoreField.Get(webViewProvider);
Java.Lang.Reflect.Method sendMessageMethod = mWebViewCore.Class.GetDeclaredMethod(
"sendMessage", Java.Lang.Class.FromType(typeof(Message)));
sendMessageMethod.Accessible = true;
Java.Lang.String javaScript = new Java.Lang.String(script);
Message javaScriptCodeMsg = Message.Obtain(null, 194, javaScript);
sendMessageMethod.Invoke(mWebViewCore, javaScriptCodeMsg);
}
I want to create a plugin for phone which pass and returns the value between javascript and android.
Can anybody suggest any ideas on how to do this?
Actually, this is not very difficult. Here, I will show you how to call native code from javascript within the page and vice-versa:
Calling native code from within web view:
When creating the web view add javascript interface (basically java class whose methods will be exposed to be called via javascript in the web view.
JavaScriptInterface jsInterface = new JavaScriptInterface(this);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(jsInterface, "JSInterface");
The definition of the javascript interface class itself (this is exemplary class I took from another answer of mine and opens video in native intent)
public class JavaScriptInterface {
private Activity activity;
public JavaScriptInterface(Activity activiy) {
this.activity = activiy;
}
public void startVideo(String videoAddress){
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(videoAddress), "video/3gpp"); // The Mime type can actually be determined from the file
activity.startActivity(intent);
}
}
Now if you want to call this code form the HTML of the page you provide the following method:
<script>
function playVideo(video){
window.JSInterface.startVideo(video);
}
</script>
Easy isn't it?
Calling javascript code from native code:
This is also simple suppose in the code of the HTML loaded in WebView you have javascript function defined:
<script>
function function(){
//... do something
}
</script>
Then you call this function through the WebView in the native code like that:
webView.loadUrl("javascript:function()");
Here's a tutorial for creating a PhoneGap Plugin. Also the instructions for the ChildBrowser plugin are especially good.
I tried looking for a similar post but couldn't find any, hence posting this query for your help. Essentially, I have a custom UI created on the GWT side. Now, I want to send events occuring at the GWT side over to the javascript/jsp page. For this I was wondering if there's a way for the jsp/javascript to register a method in the GWT code, and whenever, any event happens at the GWT side, the GWT java code can simply invoke this javascript method (which is like a function pointer/object), and the information would be notified at the jsp page. Though I can directly call javascript methods from within the GWT code, however, that means that the GWT code also need to know the javascript method name, and this results in a tight coupling. Instead the javascript could simply pass in a handle to the function to the GWT code, which would simply invoke this handle to pass in necessary events on the jsp/javascript code. Any ideas would be very helpful.
You can use a Dictionary to pass the function name to your application.
In the JSP host page:
<script type="text/javascript">
function myFunction() {
// Do stuff...
}
var MyDictionary = {
myCallback = "myFunction"
};
</script>
And in your application:
public void onEvent(EventType event) {
Dictionary d = Dictionary.getDictionary("MyDictionary");
invokeNativeCallback(d.get("myCallback"));
}
private native void invokeNativeCallback(String callbackName) /*-{
if (typeof $wnd[callbackName] === "function") {
$wnd[callbackName]();
}
}-*/;
I am working on a project where I have a lot of interaction between JavaScript and managed code. In fact, I need the JS application to interface with the Silverlight application in the page right from the beginning. So I need the Silverlight application to load before the JS code gets executed.
But while doing so, most of the times I get the error that the object was not found as the Silverlight application has not yet been loaded. So I need the Silverlight to load before the JS executes.
Is there a way I can put a stop on the JS application until the Silverlight loads and then start executing?
First, you need to add an event handler to the Loaded event of your app, from that event handler you can call the javascript function like this:
using System.Windows.Browser;
namespace SilverlightApplication3
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
HtmlPage.Window.CreateInstance("SomeFunction", new string[] { "parameter1", "parameter2" });
}
}
}
Note that you need the System.Windows.Browser namespace to use HtmlPage