diff --git a/src/Microsoft.Blazor.Browser.JS/src/RegisteredFunction.ts b/src/Microsoft.Blazor.Browser.JS/src/RegisteredFunction.ts
index d6a2442665..755aacb518 100644
--- a/src/Microsoft.Blazor.Browser.JS/src/RegisteredFunction.ts
+++ b/src/Microsoft.Blazor.Browser.JS/src/RegisteredFunction.ts
@@ -1,4 +1,7 @@
-const registeredFunctions = {};
+import { System_String } from './Platform/Platform';
+import { platform } from './Environment';
+
+const registeredFunctions: { [identifier: string]: Function } = {};
// Code in Mono 'driver.c' looks for the registered functions here
window['__blazorRegisteredFunctions'] = registeredFunctions;
@@ -10,3 +13,16 @@ export function registerFunction(identifier: string, implementation: Function) {
registeredFunctions[identifier] = implementation;
}
+
+// Handle the JSON-marshalled RegisteredFunction.Invoke calls
+registerFunction('__blazor_InvokeJson', (identifier: System_String, ...argsJson: System_String[]) => {
+ const identifierJsString = platform.toJavaScriptString(identifier);
+ if (!(registeredFunctions && registeredFunctions.hasOwnProperty(identifierJsString))) {
+ throw new Error(`Could not find registered function with name "${identifier}".`);
+ }
+ const funcInstance = registeredFunctions[identifierJsString];
+ const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json)));
+ const result = funcInstance.apply(null, args);
+ const resultJson = JSON.stringify(result);
+ return platform.toDotNetString(resultJson);
+});
diff --git a/src/Microsoft.Blazor.Browser/Interop/Json.cs b/src/Microsoft.Blazor.Browser/Interop/Json.cs
new file mode 100644
index 0000000000..96e67eb373
--- /dev/null
+++ b/src/Microsoft.Blazor.Browser/Interop/Json.cs
@@ -0,0 +1,138 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Microsoft.Blazor.Browser.Interop
+{
+ ///
+ /// Temporary internal JSON serialization library.
+ /// This will be removed when public JSON methods are added to Microsoft.Blazor
+ /// (which will be needed for HTTP support at least).
+ ///
+ internal static class Json
+ {
+ public static string Serialize(object value)
+ {
+ return MiniJSON.Json.Serialize(value);
+ }
+
+ public static T Deserialize(string json)
+ {
+ var deserialized = MiniJSON.Json.Deserialize(json);
+ return (T)CoerceShallow(deserialized, typeof(T));
+ }
+
+ private static object CoerceShallow(object deserializedValue, Type typeOfT)
+ {
+ if (deserializedValue == null)
+ {
+ // Return default value for type
+ if (typeOfT.GetTypeInfo().IsValueType)
+ {
+ return Activator.CreateInstance(typeOfT);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else if (deserializedValue is int || deserializedValue is long)
+ {
+ var deserializedValueLong = (long)deserializedValue;
+ if (typeOfT == typeof(int)) { return (int)deserializedValueLong; }
+ if (typeOfT == typeof(int?)) { return new int?((int)deserializedValueLong); }
+ if (typeOfT == typeof(uint)) { return (uint)deserializedValueLong; }
+ if (typeOfT == typeof(long)) { return (long)deserializedValueLong; }
+ if (typeOfT == typeof(ulong)) { return (ulong)deserializedValueLong; }
+ if (typeOfT == typeof(short)) { return (short)deserializedValueLong; }
+ if (typeOfT == typeof(ushort)) { return (ushort)deserializedValueLong; }
+ if (typeOfT == typeof(float)) { return (float)deserializedValueLong; }
+ if (typeOfT == typeof(double)) { return (double)deserializedValueLong; }
+
+ throw new ArgumentException($"Can't convert JSON value parsed as type {deserializedValue.GetType().FullName} to a value of type {typeOfT.FullName}");
+ }
+ else if (deserializedValue is string s)
+ {
+ if (typeOfT == typeof(string))
+ {
+ return deserializedValue;
+ }
+
+ if (typeOfT == typeof(DateTime))
+ {
+ return DateTime.Parse(s);
+ }
+
+ if (typeOfT == typeof(DateTime?))
+ {
+ return new DateTime?(DateTime.Parse(s));
+ }
+
+ throw new ArgumentException($"Can't convert JSON value parsed as type {deserializedValue.GetType().FullName} to a value of type {typeOfT.FullName}");
+ }
+ else if (deserializedValue is bool)
+ {
+ if (typeOfT == typeof(bool))
+ {
+ return deserializedValue;
+ }
+
+ throw new ArgumentException($"Can't convert JSON value parsed as type {deserializedValue.GetType().FullName} to a value of type {typeOfT.FullName}");
+ }
+ else if (deserializedValue is double)
+ {
+ var deserializedValueDouble = (double)deserializedValue;
+ if (typeOfT == typeof(float)) { return (float)deserializedValueDouble; }
+ if (typeOfT == typeof(double)) { return deserializedValueDouble; }
+
+ throw new ArgumentException($"Can't convert JSON value parsed as type {deserializedValue.GetType().FullName} to a value of type {typeOfT.FullName}");
+ }
+ else if (deserializedValue is List)
+ {
+ if (!typeOfT.IsArray)
+ {
+ return null;
+ //throw new ArgumentException($"Can't convert JSON array to type {typeOfT.FullName}, because that's not an array type.");
+ }
+
+ var deserializedValueList = (List)deserializedValue;
+ var count = deserializedValueList.Count;
+ var elementType = typeOfT.GetElementType();
+ var result = Array.CreateInstance(elementType, count);
+ for (var index = 0; index < count; index++)
+ {
+ var deserializedPropertyValue = deserializedValueList[index];
+ var mappedPropertyValue = CoerceShallow(deserializedPropertyValue, elementType);
+ result.SetValue(mappedPropertyValue, index);
+ }
+ return result;
+ }
+ else if (deserializedValue is Dictionary)
+ {
+ var result = Activator.CreateInstance(typeOfT);
+ var deserializedPropertyDict = (Dictionary)deserializedValue;
+ foreach (var propInfo in typeOfT.GetRuntimeProperties())
+ {
+ if (deserializedPropertyDict.TryGetValue(propInfo.Name, out var deserializedPropertyValue))
+ {
+ var setMethod = propInfo.SetMethod;
+ if (!object.Equals(setMethod, null))
+ {
+ var mappedPropertyValue = CoerceShallow(deserializedPropertyValue, propInfo.PropertyType);
+ setMethod.Invoke(result, new[] { mappedPropertyValue });
+ }
+ }
+ }
+
+ return result;
+ }
+ else
+ {
+ throw new ArgumentException($"Unexpected type received by CoerceShallow. Type was: { deserializedValue.GetType().FullName }");
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor.Browser/Interop/MiniJSON/MiniJSON.cs b/src/Microsoft.Blazor.Browser/Interop/MiniJSON/MiniJSON.cs
new file mode 100644
index 0000000000..644f9c7d16
--- /dev/null
+++ b/src/Microsoft.Blazor.Browser/Interop/MiniJSON/MiniJSON.cs
@@ -0,0 +1,587 @@
+// From https://github.com/Jackyjjc/MiniJSON.cs @ 5665930fd7d10e41ea45152f5e1843c3fe8c2568
+// Changes:
+// - Flag top-level type as 'internal', not 'public'
+// - Add 'GetPropsAsDict' so that anonymously-typed objects are serialized as
+// JSON objects recursively, instead of as theObject.ToString()
+
+/*
+ * Copyright (c) 2013 Calvin Rien
+ *
+ * Based on the JSON parser by Patrick van Bergen
+ * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html
+ *
+ * Simplified it so that it doesn't throw exceptions
+ * and can be used in Unity iPhone with maximum code stripping.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace MiniJSON {
+ // Example usage:
+ //
+ // using UnityEngine;
+ // using System.Collections;
+ // using System.Collections.Generic;
+ // using MiniJSON;
+ //
+ // public class MiniJSONTest : MonoBehaviour {
+ // void Start () {
+ // var jsonString = "{ \"array\": [1.44,2,3], " +
+ // "\"object\": {\"key1\":\"value1\", \"key2\":256}, " +
+ // "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " +
+ // "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " +
+ // "\"int\": 65536, " +
+ // "\"float\": 3.1415926, " +
+ // "\"bool\": true, " +
+ // "\"null\": null }";
+ //
+ // var dict = Json.Deserialize(jsonString) as Dictionary;
+ //
+ // Debug.Log("deserialized: " + dict.GetType());
+ // Debug.Log("dict['array'][0]: " + ((List) dict["array"])[0]);
+ // Debug.Log("dict['string']: " + (string) dict["string"]);
+ // Debug.Log("dict['float']: " + (double) dict["float"]); // floats come out as doubles
+ // Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs
+ // Debug.Log("dict['unicode']: " + (string) dict["unicode"]);
+ //
+ // var str = Json.Serialize(dict);
+ //
+ // Debug.Log("serialized: " + str);
+ // }
+ // }
+
+ ///
+ /// This class encodes and decodes JSON strings.
+ /// Spec. details, see http://www.json.org/
+ ///
+ /// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary.
+ /// All numbers are parsed to doubles.
+ ///
+ internal static class Json {
+ ///
+ /// Parses the string json into a value
+ ///
+ /// A JSON string.
+ /// An List<object>, a Dictionary<string, object>, a double, an integer,a string, null, true, or false
+ public static object Deserialize(string json) {
+ // save the string for debug information
+ if (json == null) {
+ return null;
+ }
+
+ return Parser.Parse(json);
+ }
+
+ sealed class Parser : IDisposable {
+ const string WORD_BREAK = "{}[],:\"";
+
+ public static bool IsWordBreak(char c) {
+ return Char.IsWhiteSpace(c) || WORD_BREAK.IndexOf(c) != -1;
+ }
+
+ const string HEX_DIGIT = "0123456789ABCDEFabcdef";
+
+ public static bool IsHexDigit(char c) {
+ return HEX_DIGIT.IndexOf(c) != -1;
+ }
+
+ enum TOKEN {
+ NONE,
+ CURLY_OPEN,
+ CURLY_CLOSE,
+ SQUARED_OPEN,
+ SQUARED_CLOSE,
+ COLON,
+ COMMA,
+ STRING,
+ NUMBER,
+ TRUE,
+ FALSE,
+ NULL
+ };
+
+ StringReader json;
+
+ Parser(string jsonString) {
+ json = new StringReader(jsonString);
+ }
+
+ public static object Parse(string jsonString) {
+ using (var instance = new Parser(jsonString)) {
+ return instance.ParseValue();
+ }
+ }
+
+ public void Dispose() {
+ json.Dispose();
+ json = null;
+ }
+
+ Dictionary ParseObject() {
+ Dictionary table = new Dictionary();
+
+ // ditch opening brace
+ json.Read();
+
+ // {
+ while (true) {
+ switch (NextToken) {
+ case TOKEN.NONE:
+ return null;
+ case TOKEN.COMMA:
+ continue;
+ case TOKEN.CURLY_CLOSE:
+ return table;
+ case TOKEN.STRING:
+ // name
+ string name = ParseString();
+ if (name == null) {
+ return null;
+ }
+
+ // :
+ if (NextToken != TOKEN.COLON) {
+ return null;
+ }
+ // ditch the colon
+ json.Read();
+
+ // value
+ TOKEN valueToken = NextToken;
+ object value = ParseByToken(valueToken);
+ if(value==null && valueToken!=TOKEN.NULL)
+ return null;
+ table[name] = value;
+ break;
+ default:
+ return null;
+ }
+ }
+ }
+
+ List ParseArray() {
+ List array = new List();
+
+ // ditch opening bracket
+ json.Read();
+
+ // [
+ var parsing = true;
+ while (parsing) {
+ TOKEN nextToken = NextToken;
+
+ switch (nextToken) {
+ case TOKEN.NONE:
+ return null;
+ case TOKEN.COMMA:
+ continue;
+ case TOKEN.SQUARED_CLOSE:
+ parsing = false;
+ break;
+ default:
+ object value = ParseByToken(nextToken);
+ if(value==null && nextToken!=TOKEN.NULL)
+ return null;
+ array.Add(value);
+ break;
+ }
+ }
+
+ return array;
+ }
+
+ object ParseValue() {
+ TOKEN nextToken = NextToken;
+ return ParseByToken(nextToken);
+ }
+
+ object ParseByToken(TOKEN token) {
+ switch (token) {
+ case TOKEN.STRING:
+ return ParseString();
+ case TOKEN.NUMBER:
+ return ParseNumber();
+ case TOKEN.CURLY_OPEN:
+ return ParseObject();
+ case TOKEN.SQUARED_OPEN:
+ return ParseArray();
+ case TOKEN.TRUE:
+ return true;
+ case TOKEN.FALSE:
+ return false;
+ case TOKEN.NULL:
+ return null;
+ default:
+ return null;
+ }
+ }
+
+ string ParseString() {
+ StringBuilder s = new StringBuilder();
+ char c;
+
+ // ditch opening quote
+ json.Read();
+
+ bool parsing = true;
+ while (parsing) {
+
+ if (json.Peek() == -1) {
+ parsing = false;
+ break;
+ }
+
+ c = NextChar;
+ switch (c) {
+ case '"':
+ parsing = false;
+ break;
+ case '\\':
+ if (json.Peek() == -1) {
+ parsing = false;
+ break;
+ }
+
+ c = NextChar;
+ switch (c) {
+ case '"':
+ case '\\':
+ case '/':
+ s.Append(c);
+ break;
+ case 'b':
+ s.Append('\b');
+ break;
+ case 'f':
+ s.Append('\f');
+ break;
+ case 'n':
+ s.Append('\n');
+ break;
+ case 'r':
+ s.Append('\r');
+ break;
+ case 't':
+ s.Append('\t');
+ break;
+ case 'u':
+ var hex = new char[4];
+
+ for (int i=0; i< 4; i++) {
+ hex[i] = NextChar;
+ if (!IsHexDigit(hex[i]))
+ return null;
+ }
+
+ s.Append((char) Convert.ToInt32(new string(hex), 16));
+ break;
+ }
+ break;
+ default:
+ s.Append(c);
+ break;
+ }
+ }
+
+ return s.ToString();
+ }
+
+ object ParseNumber() {
+ string number = NextWord;
+
+ if (number.IndexOf('.') == -1 && number.IndexOf('E') == -1 && number.IndexOf('e') == -1) {
+ long parsedInt;
+ Int64.TryParse(number, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out parsedInt);
+ return parsedInt;
+ }
+
+ double parsedDouble;
+ Double.TryParse(number, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out parsedDouble);
+ return parsedDouble;
+ }
+
+ void EatWhitespace() {
+ while (Char.IsWhiteSpace(PeekChar)) {
+ json.Read();
+
+ if (json.Peek() == -1) {
+ break;
+ }
+ }
+ }
+
+ char PeekChar {
+ get {
+ return Convert.ToChar(json.Peek());
+ }
+ }
+
+ char NextChar {
+ get {
+ return Convert.ToChar(json.Read());
+ }
+ }
+
+ string NextWord {
+ get {
+ StringBuilder word = new StringBuilder();
+
+ while (!IsWordBreak(PeekChar)) {
+ word.Append(NextChar);
+
+ if (json.Peek() == -1) {
+ break;
+ }
+ }
+
+ return word.ToString();
+ }
+ }
+
+ TOKEN NextToken {
+ get {
+ EatWhitespace();
+
+ if (json.Peek() == -1) {
+ return TOKEN.NONE;
+ }
+
+ switch (PeekChar) {
+ case '{':
+ return TOKEN.CURLY_OPEN;
+ case '}':
+ json.Read();
+ return TOKEN.CURLY_CLOSE;
+ case '[':
+ return TOKEN.SQUARED_OPEN;
+ case ']':
+ json.Read();
+ return TOKEN.SQUARED_CLOSE;
+ case ',':
+ json.Read();
+ return TOKEN.COMMA;
+ case '"':
+ return TOKEN.STRING;
+ case ':':
+ return TOKEN.COLON;
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '-':
+ return TOKEN.NUMBER;
+ }
+
+ switch (NextWord) {
+ case "false":
+ return TOKEN.FALSE;
+ case "true":
+ return TOKEN.TRUE;
+ case "null":
+ return TOKEN.NULL;
+ }
+
+ return TOKEN.NONE;
+ }
+ }
+ }
+
+ ///
+ /// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string
+ ///
+ /// A Dictionary<string, object> / List<object>
+ /// A JSON encoded string, or null if object 'json' is not serializable
+ public static string Serialize(object obj) {
+ return Serializer.Serialize(obj);
+ }
+
+ sealed class Serializer {
+ StringBuilder builder;
+
+ Serializer() {
+ builder = new StringBuilder();
+ }
+
+ public static string Serialize(object obj) {
+ var instance = new Serializer();
+
+ instance.SerializeValue(obj);
+
+ return instance.builder.ToString();
+ }
+
+ void SerializeValue(object value) {
+ IList asList;
+ IDictionary asDict;
+ string asStr;
+
+ if (value == null) {
+ builder.Append("null");
+ } else if ((asStr = value as string) != null) {
+ SerializeString(asStr);
+ } else if (value is bool) {
+ builder.Append((bool) value ? "true" : "false");
+ } else if ((asList = value as IList) != null) {
+ SerializeArray(asList);
+ } else if ((asDict = value as IDictionary) != null) {
+ SerializeObject(asDict);
+ } else if (value is char) {
+ SerializeString(new string((char) value, 1));
+ } else {
+ SerializeOther(value);
+ }
+ }
+
+ void SerializeObject(IDictionary obj) {
+ bool first = true;
+
+ builder.Append('{');
+
+ foreach (object e in obj.Keys) {
+ if (!first) {
+ builder.Append(',');
+ }
+
+ SerializeString(e.ToString());
+ builder.Append(':');
+
+ SerializeValue(obj[e]);
+
+ first = false;
+ }
+
+ builder.Append('}');
+ }
+
+ void SerializeArray(IList anArray) {
+ builder.Append('[');
+
+ bool first = true;
+
+ for (int i=0; i= 32) && (codepoint <= 126)) {
+ builder.Append(c);
+ } else {
+ builder.Append("\\u");
+ builder.Append(codepoint.ToString("x4"));
+ }
+ break;
+ }
+ }
+
+ builder.Append('\"');
+ }
+
+ void SerializeOther(object value) {
+ // NOTE: decimals lose precision during serialization.
+ // They always have, I'm just letting you know.
+ // Previously floats and doubles lost precision too.
+ if (value is float) {
+ builder.Append(((float) value).ToString("R", System.Globalization.CultureInfo.InvariantCulture));
+ } else if (value is int
+ || value is uint
+ || value is long
+ || value is sbyte
+ || value is byte
+ || value is short
+ || value is ushort
+ || value is ulong) {
+ builder.Append(value);
+ } else if (value is double
+ || value is decimal) {
+ builder.Append(Convert.ToDouble(value).ToString("R", System.Globalization.CultureInfo.InvariantCulture));
+ } else {
+ SerializeObject(GetPropsAsDict(value));
+ }
+ }
+
+ private Dictionary GetPropsAsDict(object value)
+ {
+ var propsDict = new Dictionary();
+ var propInfos = value.GetType().GetRuntimeProperties();
+ foreach (var pi in propInfos)
+ {
+ var getMethod = pi.GetMethod;
+ if (getMethod != null)
+ {
+ propsDict[pi.Name] = getMethod.Invoke(value, null);
+ }
+ }
+
+ return propsDict;
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Blazor.Browser/Interop/RegisteredFunction.cs b/src/Microsoft.Blazor.Browser/Interop/RegisteredFunction.cs
index e9c624f7b2..7d4ac60b23 100644
--- a/src/Microsoft.Blazor.Browser/Interop/RegisteredFunction.cs
+++ b/src/Microsoft.Blazor.Browser/Interop/RegisteredFunction.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System.Linq;
using WebAssembly;
namespace Microsoft.Blazor.Browser.Interop
@@ -10,6 +11,24 @@ namespace Microsoft.Blazor.Browser.Interop
///
public static class RegisteredFunction
{
+ ///
+ /// Invokes the JavaScript function registered with the specified identifier.
+ /// Arguments and return values are marshalled via JSON serialization.
+ ///
+ /// The .NET type corresponding to the function's return value type. This type must be JSON deserializable.
+ /// The identifier used when registering the target function.
+ /// The arguments to pass, each of which must be JSON serializable.
+ /// The result of the function invocation.
+ public static TRes Invoke(string identifier, params object[] args)
+ {
+ // This is a low-perf convenience method that bypasses the need to deal with
+ // .NET memory and data structures on the JS side
+ var argsJson = args.Select(Json.Serialize);
+ var resultJson = InvokeUnmarshalled("__blazor_InvokeJson",
+ argsJson.Prepend(identifier).ToArray());
+ return Json.Deserialize(resultJson);
+ }
+
///
/// Invokes the JavaScript function registered with the specified identifier.
///
diff --git a/src/Microsoft.Blazor.Browser/Microsoft.Blazor.Browser.csproj b/src/Microsoft.Blazor.Browser/Microsoft.Blazor.Browser.csproj
index 5766db614c..9f5c4f4abb 100644
--- a/src/Microsoft.Blazor.Browser/Microsoft.Blazor.Browser.csproj
+++ b/src/Microsoft.Blazor.Browser/Microsoft.Blazor.Browser.csproj
@@ -1,7 +1,7 @@
- netcoreapp2.0
+ netstandard2.0