From c22e3f84d8893716593742b6d14bfed1a89b18f7 Mon Sep 17 00:00:00 2001
From: Romans Pokrovskis
Date: Tue, 13 Feb 2018 17:34:07 +0000
Subject: [PATCH 01/52] Update README.md
Changed the link in the comment
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 1c1f36b..77b0dcd 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ class Program
};
// Publishing makes an object available at a given URL
- // The user should be directed to http://localhost:8080/button
+ // The user should be directed to http://localhost:8080/shared-button
UI.Publish ("/shared-button", button);
// Don't exit the app until someone hits return
From 7267bb2687255488318ac0f3dd00cb5e92b92185 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Wed, 21 Feb 2018 15:40:12 -0800
Subject: [PATCH 02/52] Don't keep ASP.NET sessions around
---
Ooui.AspNetCore/WebSocketHandler.cs | 23 +++++++++++------------
1 file changed, 11 insertions(+), 12 deletions(-)
diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs
index c93d3c4..a699176 100644
--- a/Ooui.AspNetCore/WebSocketHandler.cs
+++ b/Ooui.AspNetCore/WebSocketHandler.cs
@@ -11,21 +11,21 @@ namespace Ooui.AspNetCore
{
public static string WebSocketPath { get; set; } = "/ooui.ws";
- public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (5);
+ public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (1);
- static readonly ConcurrentDictionary activeSessions =
- new ConcurrentDictionary ();
+ static readonly ConcurrentDictionary pendingSessions =
+ new ConcurrentDictionary ();
public static string BeginSession (HttpContext context, Element element)
{
var id = Guid.NewGuid ().ToString ("N");
- var s = new ActiveSession {
+ var s = new PendingSession {
Element = element,
- LastConnectTimeUtc = DateTime.UtcNow,
+ CreateTimeUtc = DateTime.UtcNow,
};
- if (!activeSessions.TryAdd (id, s)) {
+ if (!pendingSessions.TryAdd (id, s)) {
throw new Exception ("Failed to schedule pending session");
}
@@ -62,19 +62,18 @@ namespace Ooui.AspNetCore
//
// Clear old sessions
//
- var toClear = activeSessions.Where (x => (DateTime.UtcNow - x.Value.LastConnectTimeUtc) > SessionTimeout).ToList ();
+ var toClear = pendingSessions.Where (x => (DateTime.UtcNow - x.Value.CreateTimeUtc) > SessionTimeout).ToList ();
foreach (var c in toClear) {
- activeSessions.TryRemove (c.Key, out var _);
+ pendingSessions.TryRemove (c.Key, out var _);
}
//
// Find the pending session
//
- if (!activeSessions.TryGetValue (id, out var activeSession)) {
+ if (!pendingSessions.TryRemove (id, out var activeSession)) {
BadRequest ("Unknown `id`");
return;
}
- activeSession.LastConnectTimeUtc = DateTime.UtcNow;
//
// Set the element's dimensions
@@ -102,10 +101,10 @@ namespace Ooui.AspNetCore
await session.RunAsync ().ConfigureAwait (false);
}
- class ActiveSession
+ class PendingSession
{
public Element Element;
- public DateTime LastConnectTimeUtc;
+ public DateTime CreateTimeUtc;
}
}
}
From 7f1a4363ef75c857c0b1cea3fc40b898668329a9 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Wed, 21 Feb 2018 15:47:46 -0800
Subject: [PATCH 03/52] Don't allow browsers to cache the ASP.NET page
This allows the WS URL to be re-generated for each session.
Fixes #83
---
Ooui.AspNetCore/ElementResult.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs
index e918d73..03f9975 100644
--- a/Ooui.AspNetCore/ElementResult.cs
+++ b/Ooui.AspNetCore/ElementResult.cs
@@ -22,6 +22,7 @@ namespace Ooui.AspNetCore
var response = context.HttpContext.Response;
response.StatusCode = 200;
response.ContentType = "text/html; charset=utf-8";
+ response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
if (element.WantsFullScreen) {
element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000);
From 00d14adcf1b5e01bdb5e98fabcededdb62845178 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Wed, 21 Feb 2018 16:32:12 -0800
Subject: [PATCH 04/52] Add HeadHtml, BodyHeaderHtml, and BodyFooterHtml to UI
Fixes #78
---
Ooui/UI.cs | 56 ++++++++++++++++++++++++++++++++++++------------------
1 file changed, 37 insertions(+), 19 deletions(-)
diff --git a/Ooui/UI.cs b/Ooui/UI.cs
index 540f74c..5445e51 100644
--- a/Ooui/UI.cs
+++ b/Ooui/UI.cs
@@ -34,24 +34,9 @@ namespace Ooui
public static byte[] ClientJsBytes => clientJsBytes;
public static string ClientJsEtag => clientJsEtag;
- public static string Template { get; set; } = $@"
-
-
- @Title
-
-
-
-
-
-
-
-@InitialHtml
-
-
-
-
-
-";
+ public static string HeadHtml { get; set; } = @"";
+ public static string BodyHeaderHtml { get; set; } = @"";
+ public static string BodyFooterHtml { get; set; } = @"";
static string host = "*";
public static string Host {
@@ -392,7 +377,40 @@ namespace Ooui
public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "")
{
- return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml);
+ using (var w = new System.IO.StringWriter ()) {
+ RenderTemplate (w, webSocketPath, title, initialHtml);
+ return w.ToString ();
+ }
+ }
+
+ public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml)
+ {
+ writer.Write (@"
+
+
+ ");
+ writer.Write (title.Replace ("&", "&").Replace ("<", "<"));
+ writer.Write (@"
+
+ ");
+ writer.WriteLine (HeadHtml);
+ writer.WriteLine (@"
+
+");
+ writer.WriteLine (BodyHeaderHtml);
+ writer.WriteLine (@"");
+ writer.WriteLine (initialHtml);
+ writer.Write (@"
+
+
+");
+ writer.WriteLine (BodyFooterHtml);
+ writer.WriteLine (@"
+");
}
class DataHandler : RequestHandler
From a0e45ab61ad00d009b2dc43b6caa8acad0f90712 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 12:56:32 -0800
Subject: [PATCH 05/52] Add netstandard1.0 (PCL) support
---
Ooui/EventTarget.cs | 5 +-
Ooui/Node.cs | 35 ++++++-
Ooui/Ooui.csproj | 8 +-
Ooui/Platform.cs | 231 +++++++++++++++++++++++---------------------
Ooui/Style.cs | 20 ++++
Ooui/UI.cs | 19 ++--
6 files changed, 195 insertions(+), 123 deletions(-)
diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs
index a37b69a..f13a255 100644
--- a/Ooui/EventTarget.cs
+++ b/Ooui/EventTarget.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
namespace Ooui
{
@@ -21,7 +22,7 @@ namespace Ooui
public IReadOnlyList StateMessages {
get {
lock (stateMessages) {
- return new List (stateMessages).AsReadOnly ();
+ return new ReadOnlyList (stateMessages);
}
}
}
@@ -242,7 +243,7 @@ namespace Ooui
public override bool CanConvert (Type objectType)
{
- return typeof (EventTarget).IsAssignableFrom (objectType);
+ return typeof (EventTarget).GetTypeInfo ().IsAssignableFrom (objectType.GetTypeInfo ());
}
}
diff --git a/Ooui/Node.cs b/Ooui/Node.cs
index 374c016..efea672 100644
--- a/Ooui/Node.cs
+++ b/Ooui/Node.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Linq;
@@ -11,7 +12,7 @@ namespace Ooui
public IReadOnlyList Children {
get {
lock (children) {
- return new List (children).AsReadOnly ();
+ return new ReadOnlyList (children);
}
}
}
@@ -35,7 +36,7 @@ namespace Ooui
protected Node (string tagName)
: base (tagName)
- {
+ {
}
public override EventTarget GetElementById (string id)
@@ -84,7 +85,7 @@ namespace Ooui
return null;
lock (children) {
if (!children.Remove (child)) {
- throw new ArgumentException ("Child not contained in this element", nameof(child));
+ throw new ArgumentException ("Child not contained in this element", nameof (child));
}
}
child.MessageSent -= HandleChildMessageSent;
@@ -130,8 +131,8 @@ namespace Ooui
case MessageType.Call when message.Key == "removeChild" && message.Value is Array ma && ma.Length == 1:
UpdateStateMessages (state => {
var mchild = ma.GetValue (0);
- Node nextChild = null;
- for (var i = 0; i < state.Count; ) {
+ Node nextChild = null;
+ for (var i = 0; i < state.Count;) {
var x = state[i];
if (x.Key == "insertBefore" && x.Value is Array xa && xa.Length == 2 && ReferenceEquals (xa.GetValue (0), mchild)) {
// Remove any inserts for this node
@@ -200,4 +201,28 @@ namespace Ooui
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
}
+
+ class ReadOnlyList : IReadOnlyList
+ {
+ readonly List list;
+
+ public ReadOnlyList (List items)
+ {
+ list = new List (items);
+ }
+
+ T IReadOnlyList.this[int index] => list[index];
+
+ int IReadOnlyCollection.Count => list.Count;
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return ((IEnumerable)list).GetEnumerator ();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator ()
+ {
+ return ((IEnumerable)list).GetEnumerator ();
+ }
+ }
}
diff --git a/Ooui/Ooui.csproj b/Ooui/Ooui.csproj
index 98357fe..5dcf5cd 100644
--- a/Ooui/Ooui.csproj
+++ b/Ooui/Ooui.csproj
@@ -8,12 +8,18 @@
https://github.com/praeclarum/Ooui
https://github.com/praeclarum/Ooui/blob/master/LICENSE
https://github.com/praeclarum/Ooui.git
- netstandard2.0
+ netstandard2.0;netstandard1.0
+
+
+ PCL
+
+
+
diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs
index 287bfa4..cb6c85e 100644
--- a/Ooui/Platform.cs
+++ b/Ooui/Platform.cs
@@ -7,131 +7,139 @@ namespace Ooui
{
static class Platform
{
- static readonly Assembly iosAssembly;
- static readonly Type iosUIViewControllerType;
- static readonly Type iosUIApplicationType;
- static readonly Type iosUIWebViewType;
- static readonly Type iosNSUrl;
- static readonly Type iosNSUrlRequest;
+#if PCL
- static readonly Assembly androidAssembly;
- static readonly Type androidActivityType;
- static readonly Type androidWebViewType;
+ public static void OpenBrowser (string url, object presenter)
+ {
+ }
- static Platform ()
- {
- var asms = AppDomain.CurrentDomain.GetAssemblies ().ToDictionary (
- x => x.GetName ().Name);
+#else
- asms.TryGetValue ("Xamarin.iOS", out iosAssembly);
- if (iosAssembly != null) {
- iosUIViewControllerType = iosAssembly.GetType ("UIKit.UIViewController");
- iosUIApplicationType = iosAssembly.GetType ("UIKit.UIApplication");
- iosUIWebViewType = iosAssembly.GetType ("UIKit.UIWebView");
- iosNSUrl = iosAssembly.GetType ("Foundation.NSUrl");
- iosNSUrlRequest = iosAssembly.GetType ("Foundation.NSUrlRequest");
- }
+ static readonly Assembly iosAssembly;
+ static readonly Type iosUIViewControllerType;
+ static readonly Type iosUIApplicationType;
+ static readonly Type iosUIWebViewType;
+ static readonly Type iosNSUrl;
+ static readonly Type iosNSUrlRequest;
- asms.TryGetValue ("Mono.Android", out androidAssembly);
- if (androidAssembly != null) {
- androidActivityType = androidAssembly.GetType ("Android.App.Activity");
- androidWebViewType = androidAssembly.GetType ("Android.Webkit.WebView");
- }
- }
+ static readonly Assembly androidAssembly;
+ static readonly Type androidActivityType;
+ static readonly Type androidWebViewType;
- public static void OpenBrowser (string url, object presenter)
- {
- if (iosAssembly != null) {
- OpenBrowserOniOS (url, presenter);
- }
- else if (androidAssembly != null) {
- OpenBrowserOnAndroid (url, presenter);
- }
- else {
- StartBrowserProcess (url);
- }
- }
+ static Platform ()
+ {
+ var asms = AppDomain.CurrentDomain.GetAssemblies ().ToDictionary (
+ x => x.GetName ().Name);
- static void OpenBrowserOnAndroid (string url, object presenter)
- {
- var presenterType = GetObjectType (presenter);
+ asms.TryGetValue ("Xamarin.iOS", out iosAssembly);
+ if (iosAssembly != null) {
+ iosUIViewControllerType = iosAssembly.GetType ("UIKit.UIViewController");
+ iosUIApplicationType = iosAssembly.GetType ("UIKit.UIApplication");
+ iosUIWebViewType = iosAssembly.GetType ("UIKit.UIWebView");
+ iosNSUrl = iosAssembly.GetType ("Foundation.NSUrl");
+ iosNSUrlRequest = iosAssembly.GetType ("Foundation.NSUrlRequest");
+ }
- object presenterWebView = null;
- if (presenter != null && androidWebViewType.IsAssignableFrom (presenterType)) {
- presenterWebView = presenter;
- }
+ asms.TryGetValue ("Mono.Android", out androidAssembly);
+ if (androidAssembly != null) {
+ androidActivityType = androidAssembly.GetType ("Android.App.Activity");
+ androidWebViewType = androidAssembly.GetType ("Android.Webkit.WebView");
+ }
+ }
- if (presenterWebView == null) {
- throw new ArgumentException ("Presenter must be a WebView", nameof(presenter));
- }
+ public static void OpenBrowser (string url, object presenter)
+ {
+ if (iosAssembly != null) {
+ OpenBrowserOniOS (url, presenter);
+ }
+ else if (androidAssembly != null) {
+ OpenBrowserOnAndroid (url, presenter);
+ }
+ else {
+ StartBrowserProcess (url);
+ }
+ }
- var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public|BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof(string) }, null);
- m.Invoke (presenterWebView, new object[] { url });
- }
+ static void OpenBrowserOnAndroid (string url, object presenter)
+ {
+ var presenterType = GetObjectType (presenter);
- static void OpenBrowserOniOS (string url, object presenter)
- {
- var presenterType = GetObjectType (presenter);
+ object presenterWebView = null;
+ if (presenter != null && androidWebViewType.IsAssignableFrom (presenterType)) {
+ presenterWebView = presenter;
+ }
- //
- // Find a presenter view controller
- // 1. Try the given presenter
- // 2. Find the key window vc
- // 3. Create a window?
- //
- object presenterViewController = null;
- if (presenter != null && iosUIViewControllerType.IsAssignableFrom (presenterType)) {
- presenterViewController = presenter;
- }
+ if (presenterWebView == null) {
+ throw new ArgumentException ("Presenter must be a WebView", nameof (presenter));
+ }
- if (presenterViewController == null) {
- var app = iosUIApplicationType.GetProperty ("SharedApplication").GetValue (null, null);
- var window = iosUIApplicationType.GetProperty ("KeyWindow").GetValue (app, null);
- if (window != null) {
- var rvc = window.GetType ().GetProperty ("RootViewController").GetValue (window, null);
- if (rvc != null) {
- var pvc = rvc.GetType ().GetProperty ("PresentedViewController").GetValue (rvc, null);
- presenterViewController = pvc ?? rvc;
- }
- }
- }
+ var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof (string) }, null);
+ m.Invoke (presenterWebView, new object[] { url });
+ }
- if (presenterViewController == null) {
- throw new InvalidOperationException ("Cannot find a view controller from which to present");
- }
+ static void OpenBrowserOniOS (string url, object presenter)
+ {
+ var presenterType = GetObjectType (presenter);
- //
- // Create the browser
- //
- var browserVC = Activator.CreateInstance (iosUIViewControllerType);
- var browserV = Activator.CreateInstance (iosUIWebViewType);
+ //
+ // Find a presenter view controller
+ // 1. Try the given presenter
+ // 2. Find the key window vc
+ // 3. Create a window?
+ //
+ object presenterViewController = null;
+ if (presenter != null && iosUIViewControllerType.IsAssignableFrom (presenterType)) {
+ presenterViewController = presenter;
+ }
- var nsUrl = iosNSUrl.GetMethod ("FromString").Invoke (null, new object[] { url });
- var nsUrlRequest = iosNSUrlRequest.GetMethod ("FromUrl").Invoke (null, new object[] { nsUrl });
- iosUIWebViewType.GetMethod ("LoadRequest").Invoke (browserV, new object[] { nsUrlRequest });
- iosUIViewControllerType.GetProperty ("View").SetValue (browserVC, browserV, null);
+ if (presenterViewController == null) {
+ var app = iosUIApplicationType.GetProperty ("SharedApplication").GetValue (null, null);
+ var window = iosUIApplicationType.GetProperty ("KeyWindow").GetValue (app, null);
+ if (window != null) {
+ var rvc = window.GetType ().GetProperty ("RootViewController").GetValue (window, null);
+ if (rvc != null) {
+ var pvc = rvc.GetType ().GetProperty ("PresentedViewController").GetValue (rvc, null);
+ presenterViewController = pvc ?? rvc;
+ }
+ }
+ }
- var m = iosUIViewControllerType.GetMethod ("PresentViewController");
+ if (presenterViewController == null) {
+ throw new InvalidOperationException ("Cannot find a view controller from which to present");
+ }
- // Console.WriteLine (presenterViewController);
- // Console.WriteLine (browserVC);
- m.Invoke (presenterViewController, new object[] { browserVC, false, null });
- }
+ //
+ // Create the browser
+ //
+ var browserVC = Activator.CreateInstance (iosUIViewControllerType);
+ var browserV = Activator.CreateInstance (iosUIWebViewType);
- static Type GetObjectType (object o)
- {
- var t = typeof (object);
- if (o is IReflectableType rt) {
- t = rt.GetTypeInfo ().AsType ();
- }
- else if (o != null) {
- t = o.GetType ();
- }
- return t;
- }
+ var nsUrl = iosNSUrl.GetMethod ("FromString").Invoke (null, new object[] { url });
+ var nsUrlRequest = iosNSUrlRequest.GetMethod ("FromUrl").Invoke (null, new object[] { nsUrl });
+ iosUIWebViewType.GetMethod ("LoadRequest").Invoke (browserV, new object[] { nsUrlRequest });
+ iosUIViewControllerType.GetProperty ("View").SetValue (browserVC, browserV, null);
- static Process StartBrowserProcess (string url)
- {
+ var m = iosUIViewControllerType.GetMethod ("PresentViewController");
+
+ // Console.WriteLine (presenterViewController);
+ // Console.WriteLine (browserVC);
+ m.Invoke (presenterViewController, new object[] { browserVC, false, null });
+ }
+
+ static Type GetObjectType (object o)
+ {
+ var t = typeof (object);
+ if (o is IReflectableType rt) {
+ t = rt.GetTypeInfo ().AsType ();
+ }
+ else if (o != null) {
+ t = o.GetType ();
+ }
+ return t;
+ }
+
+ static void StartBrowserProcess (string url)
+ {
// var vs = Environment.GetEnvironmentVariables ();
// foreach (System.Collections.DictionaryEntry kv in vs) {
// System.Console.WriteLine($"K={kv.Key}, V={kv.Value}");
@@ -139,9 +147,14 @@ namespace Ooui
// Console.WriteLine ($"Process.Start {cmd} {args}");
- return Environment.OSVersion.Platform == PlatformID.Unix
- ? Process.Start ("open", url)
- : Process.Start (new ProcessStartInfo (url) { UseShellExecute = true });
+ if (Environment.OSVersion.Platform == PlatformID.Unix) {
+ Process.Start ("open", url);
+ }
+ else {
+ Process.Start (new ProcessStartInfo (url) { UseShellExecute = true });
+ }
}
+
+#endif
}
}
diff --git a/Ooui/Style.cs b/Ooui/Style.cs
index 71f3177..c35f499 100644
--- a/Ooui/Style.cs
+++ b/Ooui/Style.cs
@@ -412,8 +412,19 @@ namespace Ooui
return null;
if (val is string s)
return s;
+
+ if (val is int i)
+ return i + units;
+ if (val is double d)
+ return d.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
+ if (val is float f)
+ return f.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
+
+#if !PCL
if (val is IConvertible c)
return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
+#endif
+
return val.ToString ();
}
@@ -431,8 +442,17 @@ namespace Ooui
return num;
}
+ if (v is int i)
+ return i;
+ if (v is double d)
+ return d;
+ if (v is float f)
+ return f;
+
+#if !PCL
if (v is IConvertible c)
return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture);
+#endif
return 0;
}
diff --git a/Ooui/UI.cs b/Ooui/UI.cs
index 5445e51..5d55082 100644
--- a/Ooui/UI.cs
+++ b/Ooui/UI.cs
@@ -6,12 +6,17 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
+
+#if !PCL
using System.Net.WebSockets;
+#endif
namespace Ooui
{
public static class UI
{
+#if !PCL
+
static readonly ManualResetEvent started = new ManualResetEvent (false);
[ThreadStatic]
@@ -22,12 +27,6 @@ namespace Ooui
static readonly Dictionary publishedPaths =
new Dictionary ();
- static readonly Dictionary styles =
- new Dictionary ();
- static readonly StyleSelectors rules = new StyleSelectors ();
-
- public static StyleSelectors Styles => rules;
-
static readonly byte[] clientJsBytes;
static readonly string clientJsEtag;
@@ -809,6 +808,14 @@ namespace Ooui
}
}
+#endif
+
+ static readonly Dictionary styles =
+ new Dictionary ();
+ static readonly StyleSelectors rules = new StyleSelectors ();
+
+ public static StyleSelectors Styles => rules;
+
public class StyleSelectors
{
public Style this[string selector] {
From 91627643e276cbcb789fded4284f5b196940a0e2 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 15:14:51 -0800
Subject: [PATCH 06/52] Add WebAssembly session
---
Ooui.AspNetCore/WebSocketHandler.cs | 2 +-
Ooui.Forms/Forms.cs | 2 +-
Ooui/Session.cs | 103 +++++++++++
Ooui/UI.cs | 255 +++++-----------------------
Ooui/WebAssemblySession.cs | 84 +++++++++
Ooui/WebSocketSession.cs | 205 ++++++++++++++++++++++
6 files changed, 433 insertions(+), 218 deletions(-)
create mode 100644 Ooui/Session.cs
create mode 100644 Ooui/WebAssemblySession.cs
create mode 100644 Ooui/WebSocketSession.cs
diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs
index a699176..03a7794 100644
--- a/Ooui.AspNetCore/WebSocketHandler.cs
+++ b/Ooui.AspNetCore/WebSocketHandler.cs
@@ -97,7 +97,7 @@ namespace Ooui.AspNetCore
//
var token = CancellationToken.None;
var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui");
- var session = new Ooui.UI.Session (webSocket, activeSession.Element, w, h, token);
+ var session = new Ooui.WebSocketSession (webSocket, activeSession.Element, w, h, token);
await session.RunAsync ().ConfigureAwait (false);
}
diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs
index a2b1f37..c648cfa 100644
--- a/Ooui.Forms/Forms.cs
+++ b/Ooui.Forms/Forms.cs
@@ -133,7 +133,7 @@ namespace Xamarin.Forms
{
if (timer != null)
return;
- var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.Session.MaxFps);
+ var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.MaxFps);
timer = new Timer ((_ => {
this.SendSignals ();
}), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds);
diff --git a/Ooui/Session.cs b/Ooui/Session.cs
new file mode 100644
index 0000000..029c530
--- /dev/null
+++ b/Ooui/Session.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ooui
+{
+ public abstract class Session
+ {
+ protected readonly Element element;
+ protected readonly double initialWidth;
+ protected readonly double initialHeight;
+
+ protected readonly HashSet createdIds;
+
+ protected readonly List queuedMessages = new List ();
+
+ public Session (Element element, double initialWidth, double initialHeight)
+ {
+ this.element = element;
+ this.initialWidth = initialWidth;
+ this.initialHeight = initialHeight;
+
+ //
+ // Keep a list of all the elements for which we've transmitted the initial state
+ //
+ createdIds = new HashSet {
+ "window",
+ "document",
+ "document.body",
+ };
+ }
+
+ void QueueStateMessagesLocked (EventTarget target)
+ {
+ if (target == null) return;
+ var created = false;
+ foreach (var m in target.StateMessages) {
+ if (m.MessageType == MessageType.Create) {
+ createdIds.Add (m.TargetId);
+ created = true;
+ }
+ if (created) {
+ QueueMessageLocked (m);
+ }
+ }
+ }
+
+ void QueueMessageLocked (Message message)
+ {
+ //
+ // Make sure all the referenced objects have been created
+ //
+ if (!createdIds.Contains (message.TargetId)) {
+ QueueStateMessagesLocked (element.GetElementById (message.TargetId));
+ }
+ if (message.Value is EventTarget ve) {
+ if (!createdIds.Contains (ve.Id)) {
+ QueueStateMessagesLocked (ve);
+ }
+ }
+ else if (message.Value is Array a) {
+ for (var i = 0; i < a.Length; i++) {
+ // Console.WriteLine ($"A{i} = {a.GetValue(i)}");
+ if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) {
+ QueueStateMessagesLocked (e);
+ }
+ }
+ }
+
+ //
+ // Add it to the queue
+ //
+ //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
+ queuedMessages.Add (message);
+ }
+
+ protected virtual void QueueMessage (Message message)
+ {
+ lock (queuedMessages) {
+ QueueMessageLocked (message);
+ }
+ }
+
+ protected void Error (string message, Exception ex)
+ {
+#if PCL
+ System.Diagnostics.Debug.WriteLine (string.Format ("{0}: {1}", message, ex));
+#else
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine ("{0}: {1}", message, ex);
+ Console.ResetColor ();
+#endif
+ }
+
+ protected void Info (string message)
+ {
+#if PCL
+ System.Diagnostics.Debug.WriteLine (message);
+#else
+ Console.WriteLine (message);
+#endif
+ }
+ }
+}
diff --git a/Ooui/UI.cs b/Ooui/UI.cs
index 5d55082..aff0c38 100644
--- a/Ooui/UI.cs
+++ b/Ooui/UI.cs
@@ -7,16 +7,13 @@ using System.Threading;
using System.Threading.Tasks;
using System.Net;
-#if !PCL
-using System.Net.WebSockets;
-#endif
-
namespace Ooui
{
public static class UI
{
-#if !PCL
+ public const int MaxFps = 30;
+#if !PCL
static readonly ManualResetEvent started = new ManualResetEvent (false);
[ThreadStatic]
@@ -538,8 +535,8 @@ namespace Ooui
//
// Connect the web socket
//
- WebSocketContext webSocketContext = null;
- WebSocket webSocket = null;
+ System.Net.WebSockets.WebSocketContext webSocketContext = null;
+ System.Net.WebSockets.WebSocket webSocket = null;
try {
webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false);
webSocket = webSocketContext.WebSocket;
@@ -577,10 +574,10 @@ namespace Ooui
// Create a new session and let it handle everything from here
//
try {
- var session = new Session (webSocket, element, w, h, serverToken);
+ var session = new WebSocketSession (webSocket, element, w, h, serverToken);
await session.RunAsync ().ConfigureAwait (false);
}
- catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) {
+ catch (System.Net.WebSockets.WebSocketException ex) when (ex.WebSocketErrorCode == System.Net.WebSockets.WebSocketError.ConnectionClosedPrematurely) {
// The remote party closed the WebSocket connection without completing the close handshake.
}
catch (Exception ex) {
@@ -598,217 +595,43 @@ namespace Ooui
Console.ResetColor ();
}
- public class Session
+#endif
+
+ static readonly Dictionary globalElements = new Dictionary ();
+ static readonly Dictionary globalElementSessions = new Dictionary ();
+
+ public static void SetGlobalElement (string globalElementId, Element element)
{
- readonly WebSocket webSocket;
- readonly Element element;
- readonly Action handleElementMessageSent;
-
- readonly CancellationTokenSource sessionCts = new CancellationTokenSource ();
- readonly CancellationTokenSource linkedCts;
- readonly CancellationToken token;
-
- readonly HashSet createdIds;
- readonly List queuedMessages = new List ();
-
- public const int MaxFps = 30;
-
- readonly System.Timers.Timer sendThrottle;
- DateTime lastTransmitTime = DateTime.MinValue;
- readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / MaxFps);
- readonly double initialWidth;
- readonly double initialHeight;
-
- public Session (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken)
- {
- this.webSocket = webSocket;
- this.element = element;
- this.initialWidth = initialWidth;
- this.initialHeight = initialHeight;
-
- //
- // Create a new session cancellation token that will trigger
- // automatically if the server shutsdown or the session shutsdown.
- //
- linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token);
- token = linkedCts.Token;
-
- //
- // Keep a list of all the elements for which we've transmitted the initial state
- //
- createdIds = new HashSet {
- "window",
- "document",
- "document.body",
- };
-
- //
- // Preparse handlers for the element
- //
- handleElementMessageSent = QueueMessage;
-
- //
- // Create a timer to use as a throttle when sending messages
- //
- sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds);
- sendThrottle.Elapsed += (s, e) => {
- // System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element);
- if ((e.SignalTime - lastTransmitTime) >= throttleInterval) {
- sendThrottle.Enabled = false;
- lastTransmitTime = e.SignalTime;
- TransmitQueuedMessages ();
- }
- };
- }
-
- public async Task RunAsync ()
- {
- //
- // Start watching for changes in the element
- //
- element.MessageSent += handleElementMessageSent;
-
- try {
- //
- // Add it to the document body
- //
- if (element.WantsFullScreen) {
- element.Style.Width = initialWidth;
- element.Style.Height = initialHeight;
- }
- QueueMessage (Message.Call ("document.body", "appendChild", element));
-
- //
- // Start the Read Loop
- //
- var receiveBuffer = new byte[64*1024];
-
- while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) {
- var receiveResult = await webSocket.ReceiveAsync(new ArraySegment(receiveBuffer), token).ConfigureAwait (false);
-
- if (receiveResult.MessageType == WebSocketMessageType.Close) {
- await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false);
- sessionCts.Cancel ();
- }
- else if (receiveResult.MessageType == WebSocketMessageType.Binary) {
- await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false);
- sessionCts.Cancel ();
- }
- else {
- var size = receiveResult.Count;
- while (!receiveResult.EndOfMessage) {
- if (size >= receiveBuffer.Length) {
- await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false);
- return;
- }
- receiveResult = await webSocket.ReceiveAsync (new ArraySegment(receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false);
- size += receiveResult.Count;
- }
- var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size);
-
- try {
- // Console.WriteLine ("RECEIVED: {0}", receivedString);
- var message = Newtonsoft.Json.JsonConvert.DeserializeObject (receivedString);
- element.Receive (message);
- }
- catch (Exception ex) {
- Error ("Failed to process received message", ex);
- }
- }
- }
- }
- finally {
- element.MessageSent -= handleElementMessageSent;
- }
- }
-
- void QueueStateMessagesLocked (EventTarget target)
- {
- if (target == null) return;
- var created = false;
- foreach (var m in target.StateMessages) {
- if (m.MessageType == MessageType.Create) {
- createdIds.Add (m.TargetId);
- created = true;
- }
- if (created) {
- QueueMessageLocked (m);
- }
- }
- }
-
- void QueueMessageLocked (Message message)
- {
- //
- // Make sure all the referenced objects have been created
- //
- if (!createdIds.Contains (message.TargetId)) {
- QueueStateMessagesLocked (element.GetElementById (message.TargetId));
- }
- if (message.Value is EventTarget ve) {
- if (!createdIds.Contains (ve.Id)) {
- QueueStateMessagesLocked (ve);
- }
- }
- else if (message.Value is Array a) {
- for (var i = 0; i < a.Length; i++) {
- // Console.WriteLine ($"A{i} = {a.GetValue(i)}");
- if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) {
- QueueStateMessagesLocked (e);
- }
- }
- }
-
- //
- // Add it to the queue
- //
- //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
- queuedMessages.Add (message);
- }
-
- void QueueMessage (Message message)
- {
- lock (queuedMessages) {
- QueueMessageLocked (message);
- }
- sendThrottle.Enabled = true;
- }
-
- async void TransmitQueuedMessages ()
- {
- try {
- //
- // Dequeue as many messages as we can
- //
- var messagesToSend = new List ();
- System.Runtime.CompilerServices.ConfiguredTaskAwaitable task;
- lock (queuedMessages) {
- messagesToSend.AddRange (queuedMessages);
- queuedMessages.Clear ();
-
- if (messagesToSend.Count == 0)
- return;
-
- //
- // Now actually send this message
- // Do this while locked to make sure SendAsync is called in the right order
- //
- var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
- var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json));
- //Console.WriteLine ("TRANSMIT " + json);
- task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false);
- }
- await task;
- }
- catch (Exception ex) {
- Error ("Failed to send queued messages, aborting session", ex);
- element.MessageSent -= handleElementMessageSent;
- sessionCts.Cancel ();
- }
+ lock (globalElements) {
+ globalElements[globalElementId] = element;
}
}
-#endif
+ public static void StartWebAssemblySession (string sessionId, string globalElementId)
+ {
+ Element element;
+ lock (globalElements) {
+ if (!globalElements.TryGetValue (globalElementId, out element))
+ return;
+ }
+
+ var g = new WebAssemblySession (sessionId, element, 640, 480);
+ lock (globalElementSessions) {
+ globalElementSessions[sessionId] = g;
+ }
+ g.StartSession ();
+ }
+
+ public static void ReceiveWebAssemblySessionMessageJson (string sessionId, string json)
+ {
+ WebAssemblySession g;
+ lock (globalElementSessions) {
+ if (!globalElementSessions.TryGetValue (sessionId, out g))
+ return;
+ }
+ g.ReceiveMessageJson (json);
+ }
+
static readonly Dictionary styles =
new Dictionary ();
diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs
new file mode 100644
index 0000000..41e8aba
--- /dev/null
+++ b/Ooui/WebAssemblySession.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+
+namespace Ooui
+{
+ public class WebAssemblySession : Session
+ {
+ readonly string id;
+ readonly Action handleElementMessageSent;
+
+ public WebAssemblySession (string id, Element element, double initialWidth, double initialHeight)
+ : base (element, initialWidth, initialHeight)
+ {
+ this.id = id;
+ handleElementMessageSent = QueueMessage;
+ }
+
+ void TransmitQueuedMessages ()
+ {
+ //
+ // Dequeue as many messages as we can
+ //
+ var messagesToSend = new List ();
+ lock (queuedMessages) {
+ messagesToSend.AddRange (queuedMessages);
+ queuedMessages.Clear ();
+ }
+
+ if (messagesToSend.Count == 0)
+ return;
+
+ //
+ // Now actually send the messages
+ //
+ var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
+ SendMessagesJson (json);
+ }
+
+ protected override void QueueMessage (Message message)
+ {
+ base.QueueMessage (message);
+ TransmitQueuedMessages ();
+ }
+
+ void SendMessagesJson (string json)
+ {
+ Info ("SEND: " + json);
+ }
+
+ public void ReceiveMessageJson (string json)
+ {
+ try {
+ Info ("RECEIVED: " + json);
+ var message = Newtonsoft.Json.JsonConvert.DeserializeObject (json);
+ element.Receive (message);
+ }
+ catch (Exception ex) {
+ Error ("Failed to process received message", ex);
+ }
+ }
+
+ public void StartSession ()
+ {
+ //
+ // Start watching for changes in the element
+ //
+ element.MessageSent += handleElementMessageSent;
+
+ //
+ // Add it to the document body
+ //
+ if (element.WantsFullScreen) {
+ element.Style.Width = initialWidth;
+ element.Style.Height = initialHeight;
+ }
+ QueueMessage (Message.Call ("document.body", "appendChild", element));
+ }
+
+ public void StopSession ()
+ {
+ element.MessageSent -= handleElementMessageSent;
+ }
+ }
+}
diff --git a/Ooui/WebSocketSession.cs b/Ooui/WebSocketSession.cs
new file mode 100644
index 0000000..3f8e3be
--- /dev/null
+++ b/Ooui/WebSocketSession.cs
@@ -0,0 +1,205 @@
+#if !PCL
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Net;
+using System.Net.WebSockets;
+
+namespace Ooui
+{
+ public class WebSocketSession : Session
+ {
+ readonly WebSocket webSocket;
+ readonly Action handleElementMessageSent;
+
+ readonly CancellationTokenSource sessionCts = new CancellationTokenSource ();
+ readonly CancellationTokenSource linkedCts;
+ readonly CancellationToken token;
+
+ readonly System.Timers.Timer sendThrottle;
+ DateTime lastTransmitTime = DateTime.MinValue;
+ readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / UI.MaxFps);
+
+ public WebSocketSession (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken)
+ : base (element, initialWidth, initialHeight)
+ {
+ this.webSocket = webSocket;
+
+ //
+ // Create a new session cancellation token that will trigger
+ // automatically if the server shutsdown or the session shutsdown.
+ //
+ linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token);
+ token = linkedCts.Token;
+
+ //
+ // Preparse handlers for the element
+ //
+ handleElementMessageSent = QueueMessage;
+
+ //
+ // Create a timer to use as a throttle when sending messages
+ //
+ sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds);
+ sendThrottle.Elapsed += (s, e) => {
+ // System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element);
+ if ((e.SignalTime - lastTransmitTime) >= throttleInterval) {
+ sendThrottle.Enabled = false;
+ lastTransmitTime = e.SignalTime;
+ TransmitQueuedMessages ();
+ }
+ };
+ }
+
+ public async Task RunAsync ()
+ {
+ //
+ // Start watching for changes in the element
+ //
+ element.MessageSent += handleElementMessageSent;
+
+ try {
+ //
+ // Add it to the document body
+ //
+ if (element.WantsFullScreen) {
+ element.Style.Width = initialWidth;
+ element.Style.Height = initialHeight;
+ }
+ QueueMessage (Message.Call ("document.body", "appendChild", element));
+
+ //
+ // Start the Read Loop
+ //
+ var receiveBuffer = new byte[64 * 1024];
+
+ while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) {
+ var receiveResult = await webSocket.ReceiveAsync (new ArraySegment (receiveBuffer), token).ConfigureAwait (false);
+
+ if (receiveResult.MessageType == WebSocketMessageType.Close) {
+ await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false);
+ sessionCts.Cancel ();
+ }
+ else if (receiveResult.MessageType == WebSocketMessageType.Binary) {
+ await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false);
+ sessionCts.Cancel ();
+ }
+ else {
+ var size = receiveResult.Count;
+ while (!receiveResult.EndOfMessage) {
+ if (size >= receiveBuffer.Length) {
+ await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false);
+ return;
+ }
+ receiveResult = await webSocket.ReceiveAsync (new ArraySegment (receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false);
+ size += receiveResult.Count;
+ }
+ var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size);
+
+ try {
+ // Console.WriteLine ("RECEIVED: {0}", receivedString);
+ var message = Newtonsoft.Json.JsonConvert.DeserializeObject (receivedString);
+ element.Receive (message);
+ }
+ catch (Exception ex) {
+ Error ("Failed to process received message", ex);
+ }
+ }
+ }
+ }
+ finally {
+ element.MessageSent -= handleElementMessageSent;
+ }
+ }
+
+ void QueueStateMessagesLocked (EventTarget target)
+ {
+ if (target == null) return;
+ var created = false;
+ foreach (var m in target.StateMessages) {
+ if (m.MessageType == MessageType.Create) {
+ createdIds.Add (m.TargetId);
+ created = true;
+ }
+ if (created) {
+ QueueMessageLocked (m);
+ }
+ }
+ }
+
+ void QueueMessageLocked (Message message)
+ {
+ //
+ // Make sure all the referenced objects have been created
+ //
+ if (!createdIds.Contains (message.TargetId)) {
+ QueueStateMessagesLocked (element.GetElementById (message.TargetId));
+ }
+ if (message.Value is EventTarget ve) {
+ if (!createdIds.Contains (ve.Id)) {
+ QueueStateMessagesLocked (ve);
+ }
+ }
+ else if (message.Value is Array a) {
+ for (var i = 0; i < a.Length; i++) {
+ // Console.WriteLine ($"A{i} = {a.GetValue(i)}");
+ if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) {
+ QueueStateMessagesLocked (e);
+ }
+ }
+ }
+
+ //
+ // Add it to the queue
+ //
+ //Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
+ queuedMessages.Add (message);
+ }
+
+ protected override void QueueMessage (Message message)
+ {
+ base.QueueMessage (message);
+ sendThrottle.Enabled = true;
+ }
+
+ async void TransmitQueuedMessages ()
+ {
+ try {
+ //
+ // Dequeue as many messages as we can
+ //
+ var messagesToSend = new List ();
+ System.Runtime.CompilerServices.ConfiguredTaskAwaitable task;
+ lock (queuedMessages) {
+ messagesToSend.AddRange (queuedMessages);
+ queuedMessages.Clear ();
+
+ if (messagesToSend.Count == 0)
+ return;
+
+ //
+ // Now actually send this message
+ // Do this while locked to make sure SendAsync is called in the right order
+ //
+ var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
+ var outputBuffer = new ArraySegment (Encoding.UTF8.GetBytes (json));
+ //Console.WriteLine ("TRANSMIT " + json);
+ task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false);
+ }
+ await task;
+ }
+ catch (Exception ex) {
+ Error ("Failed to send queued messages, aborting session", ex);
+ element.MessageSent -= handleElementMessageSent;
+ sessionCts.Cancel ();
+ }
+ }
+ }
+}
+
+#endif
From 3de84bb20ff2677ce9c1ac57d067d798f00f19d6 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 15:58:17 -0800
Subject: [PATCH 07/52] Add Ooui.Wasm sample
---
.gitignore | 13 +++
Ooui.Wasm/README.md | 26 ++++++
Ooui.Wasm/ooui-sample.cs | 16 ++++
Ooui.Wasm/ooui-sample.html | 176 +++++++++++++++++++++++++++++++++++++
4 files changed, 231 insertions(+)
create mode 100644 Ooui.Wasm/README.md
create mode 100644 Ooui.Wasm/ooui-sample.cs
create mode 100644 Ooui.Wasm/ooui-sample.html
diff --git a/.gitignore b/.gitignore
index fd8fca6..7da3b43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,16 @@
+
+# Wasm SDK
+/Ooui.Wasm/bcl
+/Ooui.Wasm/debug
+/Ooui.Wasm/release
+/Ooui.Wasm/managed
+/Ooui.Wasm/driver.c
+/Ooui.Wasm/mono.js
+/Ooui.Wasm/server.py
+/Ooui.Wasm/sample*
+/Ooui.Wasm/lib*
+/Ooui.Wasm/*.w*
+
# Social media files
/Media
diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md
new file mode 100644
index 0000000..f2518cf
--- /dev/null
+++ b/Ooui.Wasm/README.md
@@ -0,0 +1,26 @@
+
+## Install
+
+Download [Mono WebAssembly support on Jenkins](https://jenkins.mono-project.com//job/test-mono-mainline-webassembly/) by
+grabbing the latest build's Azure Artifact.
+
+[mono-wasm-03914603a3b.zip](https://jenkins.mono-project.com/job/test-mono-mainline-webassembly/71/label=highsierra/Azure/processDownloadRequest/71/highsierra/sdks/wasm/mono-wasm-03914603a3b.zip)
+
+Expand that into this directory.
+
+
+
+## Build
+
+```bash
+csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs
+```
+
+
+## Run
+
+```bash
+python server.py
+```
+
+Go to `locahost:8000/ooui.html`
diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs
new file mode 100644
index 0000000..a080e86
--- /dev/null
+++ b/Ooui.Wasm/ooui-sample.cs
@@ -0,0 +1,16 @@
+using System;
+using Ooui;
+
+public class Math {
+ public static string Add (string a, string b) {
+ try {
+ Console.WriteLine ("ENTER");
+ var e = new Div ();
+ return e.Id;
+ }
+ catch (Exception e) {
+ Console.WriteLine (e);
+ return e.ToString ();
+ }
+ }
+}
diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html
new file mode 100644
index 0000000..9062d34
--- /dev/null
+++ b/Ooui.Wasm/ooui-sample.html
@@ -0,0 +1,176 @@
+
+
+
+
+
+ C# output:
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 7bab361a63a3b45f3a52cd9045da3deb83aae3f5 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 16:43:50 -0800
Subject: [PATCH 08/52] Add convenience ctor
---
Ooui/Label.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/Ooui/Label.cs b/Ooui/Label.cs
index ca27a15..8bb063b 100644
--- a/Ooui/Label.cs
+++ b/Ooui/Label.cs
@@ -13,5 +13,12 @@ namespace Ooui
: base ("label")
{
}
+
+ public Label (string text)
+ : this ()
+ {
+ Text = text;
+ }
+
}
}
From 406a08c9ccd8b9085194dea65bed056131d94741 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 16:44:10 -0800
Subject: [PATCH 09/52] Refactor the JS wasm loader
---
Ooui.Wasm/README.md | 2 +-
Ooui.Wasm/ooui-sample.cs | 15 +++--
Ooui.Wasm/ooui-sample.html | 135 +++++++++++++++++++++----------------
3 files changed, 87 insertions(+), 65 deletions(-)
diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md
index f2518cf..d9af42a 100644
--- a/Ooui.Wasm/README.md
+++ b/Ooui.Wasm/README.md
@@ -13,7 +13,7 @@ Expand that into this directory.
## Build
```bash
-csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs
+csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime.dll /r:managed/Ooui.dll /out:managed/Ooui.Sample.dll ooui-sample.cs
```
diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs
index a080e86..d53d6ad 100644
--- a/Ooui.Wasm/ooui-sample.cs
+++ b/Ooui.Wasm/ooui-sample.cs
@@ -1,12 +1,17 @@
using System;
using Ooui;
-public class Math {
- public static string Add (string a, string b) {
+public class Program
+{
+ public static string Main (string a0, string a1)
+ {
try {
- Console.WriteLine ("ENTER");
- var e = new Div ();
- return e.Id;
+ var l = new Label { Text = "Hello" };
+ var b = new Button ("Click Me");
+ var e = new Div (new Div (l), b);
+
+ UI.SetGlobalElement ("main", e);
+ return e.ToString ();
}
catch (Exception e) {
Console.WriteLine (e);
diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html
index 9062d34..3588989 100644
--- a/Ooui.Wasm/ooui-sample.html
+++ b/Ooui.Wasm/ooui-sample.html
@@ -1,14 +1,16 @@
-
-
-
-
-
- C# output:
-
-
-
-
-
+
+
+
+ Ooui Wasm
+
+
+
+
+
+
-
-
-
\ No newline at end of file
+
+
+
+
From 2d15d2977477b28f05ea32045cd46323d855eec3 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 17:13:27 -0800
Subject: [PATCH 10/52] Add wasm support to client library
---
.gitignore | 1 +
Ooui.Wasm/ooui-sample.html | 208 ++++++-------------------------------
Ooui/Client.js | 161 +++++++++++++++++++++++++++-
3 files changed, 195 insertions(+), 175 deletions(-)
diff --git a/.gitignore b/.gitignore
index 7da3b43..eaa0c2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
/Ooui.Wasm/managed
/Ooui.Wasm/driver.c
/Ooui.Wasm/mono.js
+/Ooui.Wasm/ooui.js
/Ooui.Wasm/server.py
/Ooui.Wasm/sample*
/Ooui.Wasm/lib*
diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html
index 3588989..2c9dc7d 100644
--- a/Ooui.Wasm/ooui-sample.html
+++ b/Ooui.Wasm/ooui-sample.html
@@ -8,185 +8,45 @@
-
Loading...
+
Loading...
+
diff --git a/Ooui/Client.js b/Ooui/Client.js
index e50d972..e89fbe2 100644
--- a/Ooui/Client.js
+++ b/Ooui/Client.js
@@ -120,10 +120,35 @@ function ooui (rootElementPath) {
if (socket != null)
socket.send (ems);
if (debug) console.log ("Event", em);
- }
+ }
}());
}
+function oouiWasm (mainAsmName, mainNamspace, mainClassName, mainMethodNmae, assemblies)
+{
+ Module.assemblies = assemblies;
+
+ var initialSize = getSize ();
+
+ function send (json) {
+ if (debug) console.log ("Send", json);
+ }
+
+ function resizeHandler() {
+ const em = {
+ m: "event",
+ id: "window",
+ k: "resize",
+ v: getSize (),
+ };
+ const ems = JSON.stringify (em);
+ send (ems);
+ if (debug) console.log ("Event", em);
+ }
+
+ window.addEventListener ("resize", resizeHandler, false);
+}
+
function getNode (id) {
switch (id) {
case "window": return window;
@@ -294,3 +319,137 @@ function fixupValue (v) {
}
return v;
}
+
+// == WASM Support ==
+
+var Module = {
+ onRuntimeInitialized: function () {
+ console.log ("Done with WASM module instantiation.");
+
+ Module.FS_createPath ("/", "managed", true, true);
+
+ var pending = 0;
+ this.assemblies.forEach (function(asm_name) {
+ console.log ("Loading", asm_name);
+ ++pending;
+ fetch ("managed/" + asm_name, { credentials: 'same-origin' }).then (function (response) {
+ if (!response.ok)
+ throw "failed to load Assembly '" + asm_name + "'";
+ return response['arrayBuffer']();
+ }).then (function (blob) {
+ var asm = new Uint8Array (blob);
+ Module.FS_createDataFile ("managed/" + asm_name, null, asm, true, true, true);
+ --pending;
+ if (pending == 0)
+ Module.bclLoadingDone ();
+ });
+ });
+ },
+
+ bclLoadingDone: function () {
+ console.log ("Done loading the BCL.");
+ MonoRuntime.init ();
+ }
+};
+
+var MonoRuntime = {
+ init: function () {
+ this.load_runtime = Module.cwrap ('mono_wasm_load_runtime', null, ['string', 'number']);
+ this.assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']);
+ this.find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']);
+ this.find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']);
+ this.invoke_method = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']);
+ this.mono_string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'number', ['number']);
+ this.mono_string = Module.cwrap ('mono_wasm_string_from_js', 'number', ['string']);
+
+ this.load_runtime ("managed", 1);
+
+ console.log ("Done initializing the runtime.");
+
+ WebAssemblyApp.init ();
+ },
+
+ conv_string: function (mono_obj) {
+ if (mono_obj == 0)
+ return null;
+ var raw = this.mono_string_get_utf8 (mono_obj);
+ var res = Module.UTF8ToString (raw);
+ Module._free (raw);
+
+ return res;
+ },
+
+ call_method: function (method, this_arg, args) {
+ var args_mem = Module._malloc (args.length * 4);
+ var eh_throw = Module._malloc (4);
+ for (var i = 0; i < args.length; ++i)
+ Module.setValue (args_mem + i * 4, args [i], "i32");
+ Module.setValue (eh_throw, 0, "i32");
+
+ var res = this.invoke_method (method, this_arg, args_mem, eh_throw);
+
+ var eh_res = Module.getValue (eh_throw, "i32");
+
+ Module._free (args_mem);
+ Module._free (eh_throw);
+
+ if (eh_res != 0) {
+ var msg = this.conv_string (res);
+ throw new Error (msg);
+ }
+
+ return res;
+ },
+};
+
+var WebAssemblyApp = {
+ init: function () {
+ this.loading = document.getElementById ("loading");
+ this.output = document.getElementById ("output");
+
+ this.findMethods ();
+
+ var res = this.runApp ("1", "2");
+
+ this.output.value = res;
+ this.output.hidden = false;
+ this.loading.hidden = true;
+ },
+
+ runApp: function (a, b) {
+ try {
+ MonoRuntime.call_method (this.ooui_method, null, [MonoRuntime.mono_string ("main"), MonoRuntime.mono_string ("main")]);
+ var res = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]);
+ return MonoRuntime.conv_string (res);
+ } catch (e) {
+ return e.msg;
+ }
+ },
+
+ findMethods: function () {
+ this.ooui_module = MonoRuntime.assembly_load ("Ooui")
+ if (!this.ooui_module)
+ throw "Could not find Ooui.dll";
+
+ this.ooui_class = MonoRuntime.find_class (this.ooui_module, "Ooui", "UI")
+ if (!this.ooui_class)
+ throw "Could not find UI class in Ooui module";
+
+ this.ooui_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1)
+ if (!this.ooui_method)
+ throw "Could not find StartWebAssemblySession method";
+
+ this.main_module = MonoRuntime.assembly_load (mainAsmName)
+ if (!this.main_module)
+ throw "Could not find Main Module " + mainAsmName + ".dll";
+
+ this.math_class = MonoRuntime.find_class (this.main_module, "", "Program")
+ if (!this.math_class)
+ throw "Could not find Program class in main module";
+
+ this.add_method = MonoRuntime.find_method (this.math_class, "Main", -1)
+ if (!this.add_method)
+ throw "Could not find Main method";
+ },
+};
+
From 99b229ebedc7dd9b4b02938a5d340de6bfeaf9de Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 9 Mar 2018 18:43:02 -0800
Subject: [PATCH 11/52] Call JS methods for wasm
---
Ooui/WebAssemblySession.cs | 47 +++++++++++++++++++++++++++-----------
1 file changed, 34 insertions(+), 13 deletions(-)
diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs
index 41e8aba..47199ac 100644
--- a/Ooui/WebAssemblySession.cs
+++ b/Ooui/WebAssemblySession.cs
@@ -15,8 +15,18 @@ namespace Ooui
handleElementMessageSent = QueueMessage;
}
+ protected override void QueueMessage (Message message)
+ {
+ WebAssembly.Runtime.InvokeJS ("console.log('q 0')");
+ base.QueueMessage (message);
+ WebAssembly.Runtime.InvokeJS ("console.log('q 1')");
+ TransmitQueuedMessages ();
+ WebAssembly.Runtime.InvokeJS ("console.log('q end')");
+ }
+
void TransmitQueuedMessages ()
{
+ WebAssembly.Runtime.InvokeJS ("console.log('t 0')");
//
// Dequeue as many messages as we can
//
@@ -26,25 +36,19 @@ namespace Ooui
queuedMessages.Clear ();
}
+ WebAssembly.Runtime.InvokeJS ("console.log('t 1')");
+
if (messagesToSend.Count == 0)
return;
+ WebAssembly.Runtime.InvokeJS ("console.log('t 2')");
+
//
// Now actually send the messages
//
- var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
- SendMessagesJson (json);
- }
-
- protected override void QueueMessage (Message message)
- {
- base.QueueMessage (message);
- TransmitQueuedMessages ();
- }
-
- void SendMessagesJson (string json)
- {
- Info ("SEND: " + json);
+ //var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
+ WebAssembly.Runtime.InvokeJS ("alert(" + messagesToSend.Count + ")");
+ WebAssembly.Runtime.InvokeJS ("console.log('t end')");
}
public void ReceiveMessageJson (string json)
@@ -61,6 +65,7 @@ namespace Ooui
public void StartSession ()
{
+ WebAssembly.Runtime.InvokeJS ("console.log('was start session 0')");
//
// Start watching for changes in the element
//
@@ -73,7 +78,9 @@ namespace Ooui
element.Style.Width = initialWidth;
element.Style.Height = initialHeight;
}
+ WebAssembly.Runtime.InvokeJS ("console.log('was start session 1')");
QueueMessage (Message.Call ("document.body", "appendChild", element));
+ WebAssembly.Runtime.InvokeJS ("console.log('was start session end')");
}
public void StopSession ()
@@ -82,3 +89,17 @@ namespace Ooui
}
}
}
+
+namespace WebAssembly
+{
+ public sealed class Runtime
+ {
+ [System.Runtime.CompilerServices.MethodImplAttribute ((System.Runtime.CompilerServices.MethodImplOptions)4096)]
+ static extern string InvokeJS (string str, out int exceptional_result);
+
+ public static string InvokeJS (string str)
+ {
+ return InvokeJS (str, out var _);
+ }
+ }
+}
From 730b254f6e38dafb2e738d5e199be8dbd8af43b1 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 2 Mar 2018 22:06:46 -0800
Subject: [PATCH 12/52] Remove C# 7 pattern match
---
Ooui/Canvas.cs | 6 ++++--
Ooui/Node.cs | 7 ++++---
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs
index 66ff2cb..edc3029 100644
--- a/Ooui/Canvas.cs
+++ b/Ooui/Canvas.cs
@@ -36,8 +36,10 @@ namespace Ooui
{
if (message.TargetId == Id) {
switch (message.MessageType) {
- case MessageType.Call when message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0)):
- AddStateMessage (message);
+ case MessageType.Call:
+ if (message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0))) {
+ AddStateMessage (message);
+ }
break;
}
}
diff --git a/Ooui/Node.cs b/Ooui/Node.cs
index efea672..8de9494 100644
--- a/Ooui/Node.cs
+++ b/Ooui/Node.cs
@@ -124,6 +124,7 @@ namespace Ooui
protected override bool SaveStateMessageIfNeeded (Message message)
{
if (message.TargetId == Id) {
+ var handled = false;
switch (message.MessageType) {
case MessageType.Call when message.Key == "insertBefore":
AddStateMessage (message);
@@ -150,9 +151,9 @@ namespace Ooui
}
});
break;
- default:
- base.SaveStateMessageIfNeeded (message);
- break;
+ }
+ if (!handled) {
+ base.SaveStateMessageIfNeeded (message);
}
return true;
}
From 6fe8b41ca41d8a7bc96e681831778260a13bcf39 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 2 Mar 2018 22:14:22 -0800
Subject: [PATCH 13/52] Remove use of Linq
---
Ooui/Node.cs | 9 +++++++--
Ooui/Platform.cs | 1 -
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/Ooui/Node.cs b/Ooui/Node.cs
index 8de9494..c7456f6 100644
--- a/Ooui/Node.cs
+++ b/Ooui/Node.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Linq;
namespace Ooui
{
@@ -28,7 +27,13 @@ namespace Ooui
}
public virtual string Text {
- get { return String.Join ("", from c in Children select c.Text); }
+ get {
+ var sb = new System.Text.StringBuilder ();
+ foreach (var c in Children) {
+ sb.Append (c.Text);
+ }
+ return sb.ToString ();
+ }
set {
ReplaceAll (new TextNode (value ?? ""));
}
diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs
index cb6c85e..d0f3cc9 100644
--- a/Ooui/Platform.cs
+++ b/Ooui/Platform.cs
@@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
-using System.Linq;
using System.Reflection;
namespace Ooui
From 961fc34b4daafe70b3eb23a2ad2935fe256b95c5 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 2 Mar 2018 22:29:46 -0800
Subject: [PATCH 14/52] Add compile options to turn off features
---
Ooui/Element.cs | 4 ++++
Ooui/Node.cs | 4 ++++
Ooui/TextArea.cs | 4 ++++
Ooui/TextNode.cs | 4 ++++
4 files changed, 16 insertions(+)
diff --git a/Ooui/Element.cs b/Ooui/Element.cs
index ea203b1..cbd1430 100644
--- a/Ooui/Element.cs
+++ b/Ooui/Element.cs
@@ -232,6 +232,8 @@ namespace Ooui
protected virtual bool HtmlNeedsFullEndElement => false;
+#if !NO_XML
+
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteStartElement (TagName);
@@ -262,5 +264,7 @@ namespace Ooui
c.WriteOuterHtml (w);
}
}
+
+#endif
}
}
diff --git a/Ooui/Node.cs b/Ooui/Node.cs
index c7456f6..4d9ad8c 100644
--- a/Ooui/Node.cs
+++ b/Ooui/Node.cs
@@ -188,6 +188,8 @@ namespace Ooui
return false;
}
+#if !NO_XML
+
public virtual string OuterHtml {
get {
using (var stream = new System.IO.MemoryStream ()) {
@@ -206,6 +208,8 @@ namespace Ooui
}
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
+
+#endif
}
class ReadOnlyList : IReadOnlyList
diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs
index f3f2d5f..e6864b2 100644
--- a/Ooui/TextArea.cs
+++ b/Ooui/TextArea.cs
@@ -55,9 +55,13 @@ namespace Ooui
return base.TriggerEventFromMessage (message);
}
+#if !NO_XML
+
public override void WriteInnerHtml (System.Xml.XmlWriter w)
{
w.WriteString (val ?? "");
}
+
+#endif
}
}
diff --git a/Ooui/TextNode.cs b/Ooui/TextNode.cs
index 8fccf8c..b94625c 100644
--- a/Ooui/TextNode.cs
+++ b/Ooui/TextNode.cs
@@ -21,9 +21,13 @@ namespace Ooui
Text = text;
}
+#if !NO_XML
+
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteString (text);
}
+
+#endif
}
}
From 0ada0c749779c4b9dbeb18638749a551589cdeeb Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Fri, 2 Mar 2018 23:44:28 -0800
Subject: [PATCH 15/52] Use custom json serialization
---
Ooui/Message.cs | 116 +++++++++++++++++++++++++++++++++++++++++++++
Tests/JsonTests.cs | 52 ++++++++++++++++++++
2 files changed, 168 insertions(+)
create mode 100644 Tests/JsonTests.cs
diff --git a/Ooui/Message.cs b/Ooui/Message.cs
index 1e78fc9..485db30 100644
--- a/Ooui/Message.cs
+++ b/Ooui/Message.cs
@@ -35,6 +35,122 @@ namespace Ooui
Key = eventType,
Value = value,
};
+
+ static void WriteJsonString (System.IO.TextWriter w, string s)
+ {
+ w.Write ('\"');
+ for (var i = 0; i < s.Length; i++) {
+ var c = s[0];
+ if (c == '\r') {
+ w.Write ("\\r");
+ }
+ else if (c == '\n') {
+ w.Write ("\\n");
+ }
+ else if (c == '\t') {
+ w.Write ("\\t");
+ }
+ else if (c == '\b') {
+ w.Write ("\\b");
+ }
+ else if (c == '\\') {
+ w.Write ("\\");
+ }
+ else {
+ w.Write (c);
+ }
+ }
+ w.Write ('\"');
+ }
+
+ static void WriteJsonValue (System.IO.TextWriter w, object value)
+ {
+ if (value == null) {
+ w.Write ("null");
+ return;
+ }
+ var s = value as string;
+ if (s != null) {
+ WriteJsonString (w, s);
+ return;
+ }
+
+ var a = value as Array;
+ if (a != null) {
+ w.Write ('[');
+ var head = "";
+ foreach (var o in a) {
+ w.Write (head);
+ WriteJsonValue (w, o);
+ head = ",";
+ }
+ w.Write (']');
+ return;
+ }
+
+ var e = value as EventTarget;
+ if (e != null) {
+ w.Write ('\"');
+ w.Write (e.Id);
+ w.Write ('\"');
+ return;
+ }
+
+ if (value is Color) {
+ WriteJsonString (w, ((Color)value).ToString ());
+ return;
+ }
+
+ var icult = System.Globalization.CultureInfo.InvariantCulture;
+
+ if (value is double) {
+ w.Write (((double)value).ToString (icult));
+ }
+
+ if (value is int) {
+ w.Write (((int)value).ToString (icult));
+ }
+
+ if (value is float) {
+ w.Write (((float)value).ToString (icult));
+ }
+
+ WriteJsonString (w, Convert.ToString (value, icult));
+ }
+
+ public void WriteJson (System.IO.TextWriter w)
+ {
+ w.Write ('{');
+ switch (MessageType) {
+ case MessageType.Call: w.Write ("\"m\":\"call\",\"id\":\""); break;
+ case MessageType.Create: w.Write ("\"m\":\"create\",\"id\":\""); break;
+ case MessageType.Event: w.Write ("\"m\":\"event\",\"id\":\""); break;
+ case MessageType.Listen: w.Write ("\"m\":\"listen\",\"id\":\""); break;
+ case MessageType.Nop: w.Write ("\"m\":\"nop\",\"id\":\""); break;
+ case MessageType.RemoveAttribute: w.Write ("\"m\":\"remAttr\",\"id\":\""); break;
+ case MessageType.Set: w.Write ("\"m\":\"set\",\"id\":\""); break;
+ case MessageType.SetAttribute: w.Write ("\"m\":\"setAttr\",\"id\":\""); break;
+ }
+ w.Write (TargetId);
+ w.Write ("\",\"k\":\"");
+ w.Write (Key);
+ if (Value != null) {
+ w.Write ("\",\"v\":");
+ WriteJsonValue (w, Value);
+ w.Write ('}');
+ }
+ else {
+ w.Write ("\"}");
+ }
+ }
+
+ public string ToJson ()
+ {
+ using (var sw = new System.IO.StringWriter ()) {
+ WriteJson (sw);
+ return sw.ToString ();
+ }
+ }
}
[JsonConverter (typeof (StringEnumConverter))]
diff --git a/Tests/JsonTests.cs b/Tests/JsonTests.cs
new file mode 100644
index 0000000..f27fa35
--- /dev/null
+++ b/Tests/JsonTests.cs
@@ -0,0 +1,52 @@
+using System;
+
+#if NUNIT
+using NUnit.Framework;
+using TestClassAttribute = NUnit.Framework.TestFixtureAttribute;
+using TestMethodAttribute = NUnit.Framework.TestCaseAttribute;
+#else
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+#endif
+
+using Ooui;
+using System.IO;
+using System.Text.RegularExpressions;
+
+namespace Tests
+{
+ [TestClass]
+ public class JsonTests
+ {
+ static readonly Regex noid = new Regex ("⦙\\d+");
+ static string NoId (string s)
+ {
+ return noid.Replace (s, "⦙");
+ }
+
+ [TestMethod]
+ public void ButtonIndividualMessages ()
+ {
+ var b = new Button ();
+ b.Text = "Hello";
+ b.Click += (sender, e) => { };
+ Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}", NoId (b.StateMessages[0].ToJson ()));
+ Assert.AreEqual ("{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}", NoId (b.StateMessages[1].ToJson ()));
+ Assert.AreEqual ("{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (b.StateMessages[2].ToJson ()));
+ }
+
+ [TestMethod]
+ public void ButtonWriteMessages ()
+ {
+ var b = new Button ();
+ b.Text = "Hello";
+ b.Click += (sender, e) => { };
+ var sw = new StringWriter ();
+ foreach (var m in b.StateMessages) {
+ m.WriteJson (sw);
+ }
+ Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}" +
+ "{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}" +
+ "{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (sw.ToString ()));
+ }
+ }
+}
From b5dadbceb9b40c4a7242f169e2839134677fc556 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Sat, 3 Mar 2018 00:21:32 -0800
Subject: [PATCH 16/52] Use new WriteJson method in Sessions
---
Ooui/Message.cs | 7 +++++--
Tests/JsonTests.cs | 2 ++
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/Ooui/Message.cs b/Ooui/Message.cs
index 485db30..7254abd 100644
--- a/Ooui/Message.cs
+++ b/Ooui/Message.cs
@@ -40,8 +40,11 @@ namespace Ooui
{
w.Write ('\"');
for (var i = 0; i < s.Length; i++) {
- var c = s[0];
- if (c == '\r') {
+ var c = s[i];
+ if (c == '\"') {
+ w.Write ("\\\"");
+ }
+ else if (c == '\r') {
w.Write ("\\r");
}
else if (c == '\n') {
diff --git a/Tests/JsonTests.cs b/Tests/JsonTests.cs
index f27fa35..37c637a 100644
--- a/Tests/JsonTests.cs
+++ b/Tests/JsonTests.cs
@@ -29,9 +29,11 @@ namespace Tests
var b = new Button ();
b.Text = "Hello";
b.Click += (sender, e) => { };
+ b.Title = "\"Quote\"";
Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}", NoId (b.StateMessages[0].ToJson ()));
Assert.AreEqual ("{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}", NoId (b.StateMessages[1].ToJson ()));
Assert.AreEqual ("{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (b.StateMessages[2].ToJson ()));
+ Assert.AreEqual ("{\"m\":\"setAttr\",\"id\":\"⦙\",\"k\":\"title\",\"v\":\"\\\"Quote\\\"\"}", NoId (b.StateMessages[3].ToJson ()));
}
[TestMethod]
From e78bde556bdd03df20fd02b1140f4092116b1651 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Thu, 8 Mar 2018 13:28:28 -0800
Subject: [PATCH 17/52] Implement own Json
---
Ooui/JsonConvert.cs | 161 ++++++++++++++++++++++++++++++++++++++++++++
Ooui/Message.cs | 152 ++++++++++++++++++-----------------------
Ooui/UI.cs | 5 +-
3 files changed, 230 insertions(+), 88 deletions(-)
create mode 100644 Ooui/JsonConvert.cs
diff --git a/Ooui/JsonConvert.cs b/Ooui/JsonConvert.cs
new file mode 100644
index 0000000..fdeeed2
--- /dev/null
+++ b/Ooui/JsonConvert.cs
@@ -0,0 +1,161 @@
+using System;
+
+namespace Ooui
+{
+ class JsonConvert
+ {
+ static void WriteJsonString (System.IO.TextWriter w, string s)
+ {
+ w.Write ('\"');
+ for (var i = 0; i < s.Length; i++) {
+ var c = s[i];
+ if (c == '\"') {
+ w.Write ("\\\"");
+ }
+ else if (c == '\r') {
+ w.Write ("\\r");
+ }
+ else if (c == '\n') {
+ w.Write ("\\n");
+ }
+ else if (c == '\t') {
+ w.Write ("\\t");
+ }
+ else if (c == '\b') {
+ w.Write ("\\b");
+ }
+ else if (c == '\\') {
+ w.Write ("\\");
+ }
+ else {
+ w.Write (c);
+ }
+ }
+ w.Write ('\"');
+ }
+
+ public static void WriteJsonValue (System.IO.TextWriter w, object value)
+ {
+ if (value == null) {
+ w.Write ("null");
+ return;
+ }
+ var s = value as string;
+ if (s != null) {
+ WriteJsonString (w, s);
+ return;
+ }
+
+ var a = value as Array;
+ if (a != null) {
+ w.Write ('[');
+ var head = "";
+ foreach (var o in a) {
+ w.Write (head);
+ WriteJsonValue (w, o);
+ head = ",";
+ }
+ w.Write (']');
+ return;
+ }
+
+ var e = value as EventTarget;
+ if (e != null) {
+ w.Write ('\"');
+ w.Write (e.Id);
+ w.Write ('\"');
+ return;
+ }
+
+ if (value is Color) {
+ WriteJsonString (w, ((Color)value).ToString ());
+ return;
+ }
+
+ var icult = System.Globalization.CultureInfo.InvariantCulture;
+
+ if (value is double) {
+ w.Write (((double)value).ToString (icult));
+ }
+
+ if (value is int) {
+ w.Write (((int)value).ToString (icult));
+ }
+
+ if (value is float) {
+ w.Write (((float)value).ToString (icult));
+ }
+
+ WriteJsonString (w, Convert.ToString (value, icult));
+ }
+
+ public static string SerializeObject (object value)
+ {
+ using (var sw = new System.IO.StringWriter ()) {
+ WriteJsonValue (sw, value);
+ return sw.ToString ();
+ }
+ }
+
+ static object ReadJsonArray (string j, ref int i)
+ {
+ throw new NotImplementedException ();
+ }
+
+ static object ReadJsonObject (string json, ref int i)
+ {
+ var e = json.Length;
+ while (i < e) {
+ while (i < e && char.IsWhiteSpace (json[i]))
+ i++;
+ if (i >= e)
+ throw new Exception ("JSON Unexpected end");
+ var n = e - i;
+ i++;
+ }
+ throw new NotImplementedException ();
+ }
+
+ static object ReadJsonString (string j, ref int i)
+ {
+ throw new NotImplementedException ();
+ }
+
+ static object ReadJsonNumber (string j, ref int i)
+ {
+ throw new NotImplementedException ();
+ }
+
+ static object ReadJsonValue (string json, ref int i)
+ {
+ var e = json.Length;
+ while (i < e && char.IsWhiteSpace (json[i]))
+ i++;
+ if (i >= e)
+ throw new Exception ("JSON Unexpected end");
+ var n = e - i;
+ switch (json[i]) {
+ case '[':
+ return ReadJsonArray (json, ref i);
+ case '{':
+ return ReadJsonObject (json, ref i);
+ case '\"':
+ return ReadJsonString (json, ref i);
+ case 'f':
+ i += 5;
+ return false;
+ case 't':
+ i += 4;
+ return true;
+ default:
+ return ReadJsonNumber (json, ref i);
+ }
+ }
+
+ public static object ReadJsonValue (string json, int startIndex)
+ {
+ var i = startIndex;
+ return ReadJsonValue (json, ref i);
+ }
+ }
+}
diff --git a/Ooui/Message.cs b/Ooui/Message.cs
index 7254abd..0ba8798 100644
--- a/Ooui/Message.cs
+++ b/Ooui/Message.cs
@@ -36,91 +36,6 @@ namespace Ooui
Value = value,
};
- static void WriteJsonString (System.IO.TextWriter w, string s)
- {
- w.Write ('\"');
- for (var i = 0; i < s.Length; i++) {
- var c = s[i];
- if (c == '\"') {
- w.Write ("\\\"");
- }
- else if (c == '\r') {
- w.Write ("\\r");
- }
- else if (c == '\n') {
- w.Write ("\\n");
- }
- else if (c == '\t') {
- w.Write ("\\t");
- }
- else if (c == '\b') {
- w.Write ("\\b");
- }
- else if (c == '\\') {
- w.Write ("\\");
- }
- else {
- w.Write (c);
- }
- }
- w.Write ('\"');
- }
-
- static void WriteJsonValue (System.IO.TextWriter w, object value)
- {
- if (value == null) {
- w.Write ("null");
- return;
- }
- var s = value as string;
- if (s != null) {
- WriteJsonString (w, s);
- return;
- }
-
- var a = value as Array;
- if (a != null) {
- w.Write ('[');
- var head = "";
- foreach (var o in a) {
- w.Write (head);
- WriteJsonValue (w, o);
- head = ",";
- }
- w.Write (']');
- return;
- }
-
- var e = value as EventTarget;
- if (e != null) {
- w.Write ('\"');
- w.Write (e.Id);
- w.Write ('\"');
- return;
- }
-
- if (value is Color) {
- WriteJsonString (w, ((Color)value).ToString ());
- return;
- }
-
- var icult = System.Globalization.CultureInfo.InvariantCulture;
-
- if (value is double) {
- w.Write (((double)value).ToString (icult));
- }
-
- if (value is int) {
- w.Write (((int)value).ToString (icult));
- }
-
- if (value is float) {
- w.Write (((float)value).ToString (icult));
- }
-
- WriteJsonString (w, Convert.ToString (value, icult));
- }
-
public void WriteJson (System.IO.TextWriter w)
{
w.Write ('{');
@@ -139,7 +54,7 @@ namespace Ooui
w.Write (Key);
if (Value != null) {
w.Write ("\",\"v\":");
- WriteJsonValue (w, Value);
+ JsonConvert.WriteJsonValue (w, Value);
w.Write ('}');
}
else {
@@ -154,6 +69,71 @@ namespace Ooui
return sw.ToString ();
}
}
+
+ public static Message FromJson (string json)
+ {
+ var m = new Message ();
+
+ var i = 0;
+ var e = json.Length;
+
+ while (i < e) {
+ while (i < e && (json[i]==',' || json[i]=='{' || char.IsWhiteSpace (json[i])))
+ i++;
+ if (i >= e)
+ throw new Exception ("JSON Unexpected end");
+ var n = e - i;
+ if (json[i] == '}')
+ break;
+ if (n > 4 && json[i] == '\"' && json[i+2] == '\"' && json[i+3] == ':') {
+ switch (json[i + 1]) {
+ case 'm':
+ if (json[i + 4] == '\"' && json[i + 5] == 'e') {
+ m.MessageType = MessageType.Event;
+ }
+ i += 5;
+ while (i < e && json[i] != '\"') i++;
+ i++;
+ break;
+ case 'k': {
+ i += 5;
+ var se = i;
+ while (se < e && json[se] != '\"')
+ se++;
+ m.Key = json.Substring (i, se - i);
+ i = se + 1;
+ }
+ break;
+ case 'v':
+ m.Value = JsonConvert.ReadJsonValue (json, i + 4);
+ break;
+ }
+ }
+ else if (n > 5 && json[i] == '\"' && json[i + 3] == '\"' && json[i + 4] == ':' && json[i+5] == '\"') {
+ switch (json[i + 1]) {
+ case 'i': {
+ i += 6;
+ var se = i;
+ while (se < e && json[se] != '\"')
+ se++;
+ m.TargetId = json.Substring (i, se - i);
+ i = se + 1;
+ }
+ break;
+ }
+ }
+ else {
+ throw new Exception ("JSON Expected property");
+ }
+ }
+
+ return m;
+ }
+
+ public override string ToString ()
+ {
+ return ToJson ();
+ }
}
[JsonConverter (typeof (StringEnumConverter))]
diff --git a/Ooui/UI.cs b/Ooui/UI.cs
index aff0c38..893ef63 100644
--- a/Ooui/UI.cs
+++ b/Ooui/UI.cs
@@ -464,8 +464,9 @@ namespace Ooui
public static byte[] GetData (object obj)
{
- var r = Newtonsoft.Json.JsonConvert.SerializeObject (obj);
- return System.Text.Encoding.UTF8.GetBytes (r);
+ var r = Ooui.JsonConvert.SerializeObject (obj);
+ var e = new UTF8Encoding (false);
+ return e.GetBytes (r);
}
public override void Respond (HttpListenerContext listenerContext, CancellationToken token)
From 881330e49ec8a029663a5b03bb1702355464cc58 Mon Sep 17 00:00:00 2001
From: "Frank A. Krueger"
Date: Thu, 8 Mar 2018 13:30:06 -0800
Subject: [PATCH 18/52] Add wasm build
---
Ooui.Wasm/Dockerfile | 66 ++++++++++++++++++++++++++++++++++
Ooui.Wasm/Makefile | 18 ++++++++++
Ooui.Wasm/System.Core.cs | 16 +++++++++
Ooui.Wasm/hello.cs | 77 ++++++++++++++++++++++++++++++++++++++++
Ooui.Wasm/index.html | 44 +++++++++++++++++++++++
5 files changed, 221 insertions(+)
create mode 100644 Ooui.Wasm/Dockerfile
create mode 100644 Ooui.Wasm/Makefile
create mode 100644 Ooui.Wasm/System.Core.cs
create mode 100644 Ooui.Wasm/hello.cs
create mode 100644 Ooui.Wasm/index.html
diff --git a/Ooui.Wasm/Dockerfile b/Ooui.Wasm/Dockerfile
new file mode 100644
index 0000000..bf88989
--- /dev/null
+++ b/Ooui.Wasm/Dockerfile
@@ -0,0 +1,66 @@
+FROM alpine:3.7
+
+RUN apk update \
+ && apk upgrade \
+ && apk add --no-cache g++ musl-dev make cmake git subversion python
+
+RUN mkdir /mono-wasm \
+ && cd /mono-wasm \
+ && git clone --depth 1 https://github.com/lrz/mono-wasm.git build
+
+# RUN cd /mono-wasm \
+# && svn co --quiet http://llvm.org/svn/llvm-project/llvm/trunk llvm \
+# && cd llvm/tools \
+# && svn co --quiet http://llvm.org/svn/llvm-project/cfe/trunk clang \
+# && svn co --quiet http://llvm.org/svn/llvm-project/lld/trunk lld
+
+RUN cd /mono-wasm \
+ && wget http://releases.llvm.org/5.0.1/llvm-5.0.1.src.tar.xz \
+ && unxz llvm-5.0.1.src.tar.xz \
+ && tar xf llvm-5.0.1.src.tar \
+ && rm llvm-5.0.1.src.tar \
+ && mv llvm-5.0.1.src llvm \
+ && cd llvm/tools \
+ && wget http://releases.llvm.org/5.0.1/cfe-5.0.1.src.tar.xz \
+ && unxz cfe-5.0.1.src.tar.xz \
+ && tar xf cfe-5.0.1.src.tar \
+ && rm cfe-5.0.1.src.tar \
+ && mv cfe-5.0.1.src clang \
+ && wget http://releases.llvm.org/5.0.1/lld-5.0.1.src.tar.xz \
+ && unxz lld-5.0.1.src.tar.xz \
+ && tar xf lld-5.0.1.src.tar \
+ && rm lld-5.0.1.src.tar \
+ && mv lld-5.0.1.src lld
+
+RUN mkdir /mono-wasm/llvm-build \
+ && cd /mono-wasm/llvm-build \
+ && cmake -G "Unix Makefiles" -DCMAKE_C_COMPILER=/usr/bin/gcc -DCMAKE_CXX_COMPILER=/usr/bin/g++ -DLLVM_TARGETS_TO_BUILD=X86 -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=WebAssembly -DCMAKE_BUILD_TYPE=Release ../llvm \
+ && make -j7
+
+RUN cd /mono-wasm \
+ && git clone --depth 1 https://github.com/mono/llvm.git llvm-mono
+
+ENV CXXFLAGS="-fpermissive"
+
+RUN mkdir /mono-wasm/llvm-mono-build \
+ && cd /mono-wasm/llvm-mono-build \
+ && cmake -G "Unix Makefiles" -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" -DLLVM_TARGETS_TO_BUILD=X86 ../llvm-mono \
+ && cp -R ../llvm-mono/include/* include/ \
+ && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/stdio.h \
+ && sed -i "s/#if defined(_LARGEFILE64_SOURCE) || defined(_GNU_SOURCE)/#if 0/" /usr/include/sys/stat.h \
+ # && sed -i "s/namespace llvm/#undef fopen\n#undef fopen64\n#undef fseeko\n#undef fseeko64\n#undef fstat\n#undef fstat64\n#undef ftello\n#undef ftello64\n#undef lstat\n#undef lstat64\n#undef stat\n#undef stat64\n#undef tmpfile\n#undef tmpfile64\n\nnamespace llvm/" /mono-wasm/llvm-mono-build/include/llvm/Target/TargetLibraryInfo.h
+ && make -j7
+
+ENV CXXFLAGS=""
+
+RUN cd /mono-wasm \
+ && git clone --depth 1 https://github.com/lrz/mono-wasm-mono.git mono-compiler
+
+RUN apk add --no-cache bash autoconf automake libtool linux-headers \
+ && cd /mono-wasm/mono-compiler \
+ && ./autogen.sh --with-cross-offsets=offsets-wasm32.h CFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" CXXFLAGS="-DCOMPILE_WASM32 -DMONO_CROSS_COMPILE" --disable-boehm --with-sigaltstack=no --enable-llvm --enable-llvm-runtime --with-llvm=../llvm-mono-build --disable-btls --with-runtime_preset=testing_aot_full
+
+# RUN cd /mono-wasm/mono-compiler/eglib \
+# && make -j7 \
+# && cd ../mono \
+# && make -j7
diff --git a/Ooui.Wasm/Makefile b/Ooui.Wasm/Makefile
new file mode 100644
index 0000000..cd11c33
--- /dev/null
+++ b/Ooui.Wasm/Makefile
@@ -0,0 +1,18 @@
+OOUI_SRCS=$(wildcard ../Ooui/*.cs)
+
+all: hello.exe output/index.wasm output/index.html
+
+hello.exe: hello.cs Ooui.dll Makefile
+ mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -r:Ooui.dll hello.cs -out:hello.exe
+
+Ooui.dll: $(OOUI_SRCS) System.Core.cs Makefile
+ mcs -nostdlib -noconfig -r:mono-wasm-macos/dist/lib/mscorlib.dll -define:NO_PROCESS,NO_SERVER,NO_XML System.Core.cs $(OOUI_SRCS) -out:Ooui.dll
+
+output/index.wasm: hello.exe Makefile
+ mono-wasm-macos/dist/bin/mono-wasm -i hello.exe -o output
+
+output/index.html: index.html
+ cp index.html output
+
+clean:
+ rm -rf build output hello.exe
diff --git a/Ooui.Wasm/System.Core.cs b/Ooui.Wasm/System.Core.cs
new file mode 100644
index 0000000..f8d5d11
--- /dev/null
+++ b/Ooui.Wasm/System.Core.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace System.ComponentModel
+{
+ public interface INotifyPropertyChanged
+ {
+
+ }
+
+ public class PropertyChangedEventArgs : EventArgs
+ {
+
+ }
+
+ public delegate void PropertyChangedEventHandler (object sender, PropertyChangedEventArgs e);
+}
\ No newline at end of file
diff --git a/Ooui.Wasm/hello.cs b/Ooui.Wasm/hello.cs
new file mode 100644
index 0000000..b802c97
--- /dev/null
+++ b/Ooui.Wasm/hello.cs
@@ -0,0 +1,77 @@
+using Mono.WebAssembly;
+using System;
+
+class Hello
+{
+ static int Factorial(int n)
+ {
+ if (n == 0) {
+ return 1;
+ }
+ return n * Factorial(n - 1);
+ }
+
+ // This function is called from the browser by JavaScript.
+ // Here we calculate the factorial of the given number then use the
+ // Mono.WebAssembly API to retrieve the element from the DOM and set its
+ // innerText property to the factorial result.
+ static void FactorialInElement(int n, string element_id)
+ {
+ Console.WriteLine(
+ "Calculating factorial of {0} into DOM element {1}",
+ n, element_id);
+
+ int f = Factorial(n);
+
+ var elem = HtmlPage.Document.GetElementById(element_id);
+ elem.InnerText = f.ToString();
+ }
+
+ static void PrintHtmlElements(HtmlElement elem, int level)
+ {
+ string str = "";
+ for (int i = 0; i < level; i++) {
+ str += " ";
+ }
+
+ str += $"<{elem.TagName}";
+
+ foreach (var name in elem.AttributeNames) {
+ var value = elem.GetAttribute(name);
+ str += $" {name}='{value}'";
+ }
+
+ str += ">";
+
+ Console.WriteLine(str);
+
+ var list = elem.Children;
+ for (int i = 0; i < list.Count; i++) {
+ var child = list[i];
+ PrintHtmlElements(child, level + 1);
+ }
+ }
+
+ static int Main(string[] args)
+ {
+ Ooui.Div odiv = new Ooui.Div ();
+ int f = Factorial(6);
+ HtmlPage.Window.Alert($"Hello world! factorial(6) -> {odiv}");
+
+ var bi = HtmlPage.BrowserInformation;
+ Console.WriteLine($"BrowserInformation: Name {bi.Name} BrowserVersion {bi.BrowserVersion} UserAgent {bi.UserAgent} Platform {bi.Platform} CookiesEnabled {bi.CookiesEnabled} ProductName {bi.ProductName}");
+
+ var d = HtmlPage.Document;
+ Console.WriteLine($"Document Location: {d.Location}");
+
+ PrintHtmlElements(d.DocumentElement, 0);
+
+ var p = d.CreateElement("p");
+ p.InnerText = "This text was added at runtime.";
+ d.Body.AppendChild(p);
+
+ if (args.Length > 0) FactorialInElement(0, ""); // this is a hack so that the linker does not remove the FactorialInElement() method
+
+ return f;
+ }
+}
diff --git a/Ooui.Wasm/index.html b/Ooui.Wasm/index.html
new file mode 100644
index 0000000..121112d
--- /dev/null
+++ b/Ooui.Wasm/index.html
@@ -0,0 +1,44 @@
+
+
+
+
+ WebAssembly Example
+
+
+
+
+
+
+
+
+
+
+