[Enhancement] Add EvaluateJavaScript method to WebView (#2140)

* Add EvaluateJavaScript method to return string result of js calls

* add docs

* account for some quirks between platforms

* cleanup

* edge cases, standardization, and error handling

* early escape

* add null check

* fix code styles

* clarify sort

* code styles adjustment and null check
This commit is contained in:
Alan Grgic 2018-03-22 12:20:47 -05:00 коммит произвёл Jason Smith
Родитель 2ca6fdaeea
Коммит 578c0eb23a
16 изменённых файлов: 289 добавлений и 1 удалений

Просмотреть файл

@ -108,10 +108,30 @@ namespace Xamarin.Forms.Controls
jsAlertWebView
);
var evaluateJsWebView = new WebView
{
Source = new UrlWebViewSource { Url = "https://www.google.com/" },
HeightRequest = 50
};
var evaluateJsWebViewSourceContainer = new ViewContainer<WebView>(Test.WebView.EvaluateJavaScript,
evaluateJsWebView
);
var resultsLabel = new Label();
var execButton = new Button();
execButton.Text = "Evaluate Javascript";
execButton.Command = new Command(async() => resultsLabel.Text = await evaluateJsWebView.EvaluateJavaScriptAsync(
"var test = function(){ return 'This string came from Javascript!'; }; test();"));
evaluateJsWebViewSourceContainer.ContainerLayout.Children.Add(resultsLabel);
evaluateJsWebViewSourceContainer.ContainerLayout.Children.Add(execButton);
Add (urlWebViewSourceContainer);
Add (htmlWebViewSourceContainer);
Add (htmlFileWebSourceContainer);
Add (javascriptAlertWebSourceContainer);
Add (evaluateJsWebViewSourceContainer);
}
}
}

Просмотреть файл

@ -1,5 +1,6 @@
using System;
using Xamarin.Forms.Internals;
using System.Threading.Tasks;
namespace Xamarin.Forms
{
@ -8,6 +9,7 @@ namespace Xamarin.Forms
bool CanGoBack { get; set; }
bool CanGoForward { get; set; }
event EventHandler<EvalRequested> EvalRequested;
event EvaluateJavaScriptDelegate EvaluateJavaScriptRequested;
event EventHandler GoBackRequested;
event EventHandler GoForwardRequested;
void SendNavigated(WebNavigatedEventArgs args);

Просмотреть файл

@ -1,8 +1,12 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
namespace Xamarin.Forms.Internals
{
[EditorBrowsable(EditorBrowsableState.Never)]
public delegate Task<string> EvaluateJavaScriptDelegate(string script);
[EditorBrowsable(EditorBrowsableState.Never)]
public class EvalRequested : EventArgs
{

Просмотреть файл

@ -1,7 +1,11 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Xamarin.Forms.Internals;
using Xamarin.Forms.Platform;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace Xamarin.Forms
{
@ -77,6 +81,34 @@ namespace Xamarin.Forms
handler?.Invoke(this, new EvalRequested(script));
}
public async Task<string> EvaluateJavaScriptAsync(string script)
{
EvaluateJavaScriptDelegate handler = EvaluateJavaScriptRequested;
if (script == null)
return null;
//make all the platforms mimic Android's implementation, which is by far the most complete.
if (Xamarin.Forms.Device.RuntimePlatform != "Android")
{
script = EscapeJsString(script);
script = "try{JSON.stringify(eval('" + script + "'))}catch(e){'null'};";
}
var result = await handler?.Invoke(script);
//if the js function errored or returned null/undefined treat it as null
if (result == "null")
result = null;
//JSON.stringify wraps the result in literal quotes, we just want the actual returned result
//note that if the js function returns the string "null" we will get here and not above
else if (result != null)
result = result.Trim('"');
return result;
}
public void GoBack()
=> GoBackRequested?.Invoke(this, EventArgs.Empty);
@ -123,6 +155,9 @@ namespace Xamarin.Forms
[EditorBrowsable(EditorBrowsableState.Never)]
public event EventHandler<EvalRequested> EvalRequested;
[EditorBrowsable(EditorBrowsableState.Never)]
public event EvaluateJavaScriptDelegate EvaluateJavaScriptRequested;
[EditorBrowsable(EditorBrowsableState.Never)]
public event EventHandler GoBackRequested;
@ -145,5 +180,42 @@ namespace Xamarin.Forms
{
return _platformConfigurationRegistry.Value.On<T>();
}
static string EscapeJsString(string js)
{
if (js == null)
return null;
if (!js.Contains("'"))
return js;
//get every quote in the string along with all the backslashes preceding it
var singleQuotes = Regex.Matches(js, @"(\\*?)'");
var uniqueMatches = new List<string>();
for (var i=0; i < singleQuotes.Count; i++)
{
var matchedString = singleQuotes[i].Value;
if (!uniqueMatches.Contains(matchedString))
{
uniqueMatches.Add(matchedString);
}
}
uniqueMatches.Sort((x, y) => y.Length.CompareTo(x.Length));
//escape all quotes from the script as well as add additional escaping to all quotes that were already escaped
for (var i=0; i < uniqueMatches.Count; i++)
{
var match = uniqueMatches[i];
var numberOfBackslashes = match.Length - 1;
var slashesToAdd = (numberOfBackslashes * 2) + 1;
var replacementStr = "'".PadLeft(slashesToAdd + 1, '\\');
js = Regex.Replace(js, @"(?<=[^\\])" + Regex.Escape(match), replacementStr);
}
return js;
}
}
}

Просмотреть файл

@ -689,7 +689,8 @@ namespace Xamarin.Forms.CustomAttributes
LoadHtml,
MixedContentDisallowed,
MixedContentAllowed,
JavaScriptAlert
JavaScriptAlert,
EvaluateJavaScript
}
public enum UrlWebViewSource {

Просмотреть файл

@ -9,6 +9,7 @@ using Xamarin.Forms.PlatformConfiguration.AndroidSpecific;
using Xamarin.Forms.Internals;
using MixedContentHandling = Android.Webkit.MixedContentHandling;
using AWebView = Android.Webkit.WebView;
using System.Threading.Tasks;
namespace Xamarin.Forms.Platform.Android
{
@ -99,6 +100,7 @@ namespace Xamarin.Forms.Platform.Android
{
var oldElementController = e.OldElement as IWebViewController;
oldElementController.EvalRequested -= OnEvalRequested;
oldElementController.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
oldElementController.GoBackRequested -= OnGoBackRequested;
oldElementController.GoForwardRequested -= OnGoForwardRequested;
}
@ -107,6 +109,7 @@ namespace Xamarin.Forms.Platform.Android
{
var newElementController = e.NewElement as IWebViewController;
newElementController.EvalRequested += OnEvalRequested;
newElementController.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
newElementController.GoBackRequested += OnGoBackRequested;
newElementController.GoForwardRequested += OnGoForwardRequested;
@ -147,6 +150,15 @@ namespace Xamarin.Forms.Platform.Android
LoadUrl("javascript:" + eventArg.Script);
}
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
var jsr = new JavascriptResult();
Control.EvaluateJavascript(script, jsr);
return await jsr.JsResult.ConfigureAwait(false);
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (Control.CanGoBack())
@ -251,5 +263,22 @@ namespace Xamarin.Forms.Platform.Android
_renderer = null;
}
}
class JavascriptResult : Java.Lang.Object, IValueCallback
{
TaskCompletionSource<string> source;
public Task<string> JsResult { get { return source.Task; } }
public JavascriptResult()
{
source = new TaskCompletionSource<string>();
}
public void OnReceiveValue(Java.Lang.Object result)
{
string json = ((Java.Lang.String)result).ToString();
source.SetResult(json);
}
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
using System;
using System.ComponentModel;
using Xamarin.Forms.Internals;
using System.Threading.Tasks;
namespace Xamarin.Forms.Platform.GTK.Renderers
{
@ -98,6 +99,7 @@ namespace Xamarin.Forms.Platform.GTK.Renderers
}
WebViewController.EvalRequested += OnEvalRequested;
WebViewController.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
WebViewController.GoBackRequested += OnGoBackRequested;
WebViewController.GoForwardRequested += OnGoForwardRequested;
}
@ -129,6 +131,7 @@ namespace Xamarin.Forms.Platform.GTK.Renderers
}
WebViewController.EvalRequested -= OnEvalRequested;
WebViewController.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
WebViewController.GoBackRequested -= OnGoBackRequested;
WebViewController.GoForwardRequested -= OnGoForwardRequested;
}
@ -203,6 +206,12 @@ namespace Xamarin.Forms.Platform.GTK.Renderers
}
}
Task<string> OnEvaluateJavaScriptRequested(string script)
{
Control?.ExecuteScript(script);
return null;
}
private void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (Control == null)

Просмотреть файл

@ -3,6 +3,7 @@ using System.ComponentModel;
using AppKit;
using Foundation;
using Xamarin.Forms.Internals;
using System.Threading.Tasks;
using WebKit;
@ -83,6 +84,7 @@ namespace Xamarin.Forms.Platform.MacOS
});
Element.EvalRequested += OnEvalRequested;
Element.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
Element.GoBackRequested += OnGoBackRequested;
Element.GoForwardRequested += OnGoForwardRequested;
@ -109,6 +111,7 @@ namespace Xamarin.Forms.Platform.MacOS
{
_disposed = true;
Element.EvalRequested -= OnEvalRequested;
Element.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
Element.GoBackRequested -= OnGoBackRequested;
Element.GoForwardRequested -= OnGoForwardRequested;
}
@ -138,6 +141,18 @@ namespace Xamarin.Forms.Platform.MacOS
Control?.StringByEvaluatingJavaScriptFromString(eventArg?.Script);
}
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
var tcr = new TaskCompletionSource<string>();
var task = tcr.Task;
Device.BeginInvokeOnMainThread(() => {
tcr.SetResult(Control?.StringByEvaluatingJavaScriptFromString(script));
});
return await task.ConfigureAwait(false);
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (Control.CanGoBack())

Просмотреть файл

@ -3,6 +3,7 @@ using System.ComponentModel;
using Xamarin.Forms.Internals;
using TChromium = Tizen.WebView.Chromium;
using TWebView = Tizen.WebView.WebView;
using System.Threading.Tasks;
namespace Xamarin.Forms.Platform.Tizen
{
@ -42,6 +43,7 @@ namespace Xamarin.Forms.Platform.Tizen
if (Element != null)
{
Element.EvalRequested -= OnEvalRequested;
Element.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
Element.GoBackRequested -= OnGoBackRequested;
Element.GoForwardRequested -= OnGoForwardRequested;
}
@ -72,6 +74,7 @@ namespace Xamarin.Forms.Platform.Tizen
if (e.NewElement != null)
{
e.NewElement.EvalRequested += OnEvalRequested;
e.NewElement.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
e.NewElement.GoForwardRequested += OnGoForwardRequested;
e.NewElement.GoBackRequested += OnGoBackRequested;
Load();
@ -137,6 +140,12 @@ namespace Xamarin.Forms.Platform.Tizen
_control.Eval(eventArg.Script);
}
Task<string> OnEvaluateJavaScriptRequested(string script)
{
_control.Eval(script);
return null;
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (_control.CanGoBack())

Просмотреть файл

@ -5,6 +5,7 @@ using Windows.UI.Xaml.Controls;
using Xamarin.Forms.Internals;
using static System.String;
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;
using System.Threading.Tasks;
namespace Xamarin.Forms.Platform.UWP
@ -91,6 +92,7 @@ if(bases.length == 0){
{
var oldElement = e.OldElement;
oldElement.EvalRequested -= OnEvalRequested;
oldElement.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
oldElement.GoBackRequested -= OnGoBackRequested;
oldElement.GoForwardRequested -= OnGoForwardRequested;
}
@ -109,6 +111,7 @@ if(bases.length == 0){
var newElement = e.NewElement;
newElement.EvalRequested += OnEvalRequested;
newElement.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
newElement.GoForwardRequested += OnGoForwardRequested;
newElement.GoBackRequested += OnGoBackRequested;
@ -140,6 +143,11 @@ if(bases.length == 0){
await Control.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => await Control.InvokeScriptAsync("eval", new[] { eventArg.Script }));
}
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
return await Control.InvokeScriptAsync("eval", new[] { script });
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (Control.CanGoBack)

Просмотреть файл

@ -22,6 +22,7 @@ namespace Xamarin.Forms.Platform.WPF
if (e.OldElement != null) // Clear old element event
{
e.OldElement.EvalRequested -= OnEvalRequested;
e.OldElement.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
e.OldElement.GoBackRequested -= OnGoBackRequested;
e.OldElement.GoForwardRequested -= OnGoForwardRequested;
}
@ -40,6 +41,7 @@ namespace Xamarin.Forms.Platform.WPF
// Suscribe element event
Element.EvalRequested += OnEvalRequested;
Element.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
Element.GoBackRequested += OnGoBackRequested;
Element.GoForwardRequested += OnGoForwardRequested;
}
@ -91,6 +93,18 @@ namespace Xamarin.Forms.Platform.WPF
Control.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => Control.InvokeScript("eval", eventArg.Script)));
}
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
var tcr = new TaskCompletionSource<string>();
var task = tcr.Task;
Device.BeginInvokeOnMainThread(() => {
tcr.SetResult((string)Control.InvokeScript("eval", new[] { script }));
});
return await task.ConfigureAwait(false);
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (Control.CanGoBack)

Просмотреть файл

@ -4,6 +4,7 @@ using System.Drawing;
using Foundation;
using UIKit;
using Xamarin.Forms.Internals;
using System.Threading.Tasks;
namespace Xamarin.Forms.Platform.iOS
{
@ -37,6 +38,7 @@ namespace Xamarin.Forms.Platform.iOS
Element = element;
Element.PropertyChanged += HandlePropertyChanged;
WebView.EvalRequested += OnEvalRequested;
WebView.EvaluateJavaScriptRequested += OnEvaluateJavaScriptRequested;
WebView.GoBackRequested += OnGoBackRequested;
WebView.GoForwardRequested += OnGoForwardRequested;
Delegate = new CustomWebViewDelegate(this);
@ -100,6 +102,7 @@ namespace Xamarin.Forms.Platform.iOS
Element.PropertyChanged -= HandlePropertyChanged;
WebView.EvalRequested -= OnEvalRequested;
WebView.EvaluateJavaScriptRequested -= OnEvaluateJavaScriptRequested;
WebView.GoBackRequested -= OnGoBackRequested;
WebView.GoForwardRequested -= OnGoForwardRequested;
@ -139,6 +142,16 @@ namespace Xamarin.Forms.Platform.iOS
EvaluateJavascript(eventArg.Script);
}
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
var tcr = new TaskCompletionSource<string>();
var task = tcr.Task;
Device.BeginInvokeOnMainThread(() => { tcr.SetResult(EvaluateJavascript(script)); });
return await task.ConfigureAwait(false);
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
{
if (CanGoBack)

Просмотреть файл

@ -0,0 +1,28 @@
<Type Name="EvaluateJavaScriptDelegate" FullName="Xamarin.Forms.Internals.EvaluateJavaScriptDelegate">
<TypeSignature Language="C#" Value="public delegate System.Threading.Tasks.Task&lt;string&gt; EvaluateJavaScriptDelegate(string script);" />
<TypeSignature Language="ILAsm" Value=".class public auto ansi sealed EvaluateJavaScriptDelegate extends System.MulticastDelegate" />
<AssemblyInfo>
<AssemblyName>Xamarin.Forms.Core</AssemblyName>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Base>
<BaseTypeName>System.Delegate</BaseTypeName>
</Base>
<Attributes>
<Attribute>
<AttributeName>System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)</AttributeName>
</Attribute>
</Attributes>
<Parameters>
<Parameter Name="script" Type="System.String" />
</Parameters>
<ReturnValue>
<ReturnType>System.Threading.Tasks.Task&lt;System.String&gt;</ReturnType>
</ReturnValue>
<Docs>
<param name="script">To be added.</param>
<summary>To be added.</summary>
<returns>To be added.</returns>
<remarks>To be added.</remarks>
</Docs>
</Type>

Просмотреть файл

@ -62,6 +62,21 @@
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="EvaluateJavaScriptRequested">
<MemberSignature Language="C#" Value="public event Xamarin.Forms.Internals.EvaluateJavaScriptDelegate EvaluateJavaScriptRequested;" />
<MemberSignature Language="ILAsm" Value=".event class Xamarin.Forms.Internals.EvaluateJavaScriptDelegate EvaluateJavaScriptRequested" />
<MemberType>Event</MemberType>
<AssemblyInfo>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<ReturnValue>
<ReturnType>Xamarin.Forms.Internals.EvaluateJavaScriptDelegate</ReturnType>
</ReturnValue>
<Docs>
<summary>To be added.</summary>
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="GoBackRequested">
<MemberSignature Language="C#" Value="public event EventHandler GoBackRequested;" />
<MemberSignature Language="ILAsm" Value=".event class System.EventHandler GoBackRequested" />

Просмотреть файл

@ -214,6 +214,54 @@ namespace FormsGallery
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="EvaluateJavaScriptAsync">
<MemberSignature Language="C#" Value="public System.Threading.Tasks.Task&lt;string&gt; EvaluateJavaScriptAsync (string script);" />
<MemberSignature Language="ILAsm" Value=".method public hidebysig instance class System.Threading.Tasks.Task`1&lt;string&gt; EvaluateJavaScriptAsync(string script) cil managed" />
<MemberType>Method</MemberType>
<AssemblyInfo>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Attributes>
<Attribute>
<AttributeName>System.Diagnostics.DebuggerStepThrough</AttributeName>
</Attribute>
<Attribute>
<AttributeName>System.Runtime.CompilerServices.AsyncStateMachine(typeof(Xamarin.Forms.WebView/&lt;EvaluateJavaScriptAsync&gt;d__21))</AttributeName>
</Attribute>
</Attributes>
<ReturnValue>
<ReturnType>System.Threading.Tasks.Task&lt;System.String&gt;</ReturnType>
</ReturnValue>
<Parameters>
<Parameter Name="script" Type="System.String" />
</Parameters>
<Docs>
<param name="script">To be added.</param>
<summary>To be added.</summary>
<returns>To be added.</returns>
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="EvaluateJavaScriptRequested">
<MemberSignature Language="C#" Value="public event Xamarin.Forms.Internals.EvaluateJavaScriptDelegate EvaluateJavaScriptRequested;" />
<MemberSignature Language="ILAsm" Value=".event class Xamarin.Forms.Internals.EvaluateJavaScriptDelegate EvaluateJavaScriptRequested" />
<MemberType>Event</MemberType>
<AssemblyInfo>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
<Attributes>
<Attribute>
<AttributeName>System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)</AttributeName>
</Attribute>
</Attributes>
<ReturnValue>
<ReturnType>Xamarin.Forms.Internals.EvaluateJavaScriptDelegate</ReturnType>
</ReturnValue>
<Docs>
<summary>To be added.</summary>
<remarks>To be added.</remarks>
</Docs>
</Member>
<Member MemberName="GoBack">
<MemberSignature Language="C#" Value="public void GoBack ();" />
<MemberSignature Language="ILAsm" Value=".method public hidebysig instance void GoBack() cil managed" />

Просмотреть файл

@ -471,6 +471,7 @@
<Type Name="EffectUtilities" Kind="Class" />
<Type Name="EnumerableExtensions" Kind="Class" />
<Type Name="EvalRequested" Kind="Class" />
<Type Name="EvaluateJavaScriptDelegate" Kind="Delegate" />
<Type Name="EventArg`1" DisplayName="EventArg&lt;T&gt;" Kind="Class" />
<Type Name="ExpressionSearch" Kind="Class" />
<Type Name="IDataTemplate" Kind="Interface" />