From 3c94ad0363aebadbabd13b4f768f4c85cb83554e Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 7 Feb 2018 15:42:45 -0800 Subject: [PATCH 01/52] Improve docs --- README.md | 133 ++++++++++++------------------------------------------ 1 file changed, 30 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index ea5a098..6910c21 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ | Version | Package | Description | | ------- | ------- | ----------- | -| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) | [Ooui](https://www.nuget.org/packages/Ooui) | Core library with HTML elements and a server. | +| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) | [Ooui](https://www.nuget.org/packages/Ooui) | Core library with HTML elements and a server | +| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.AspNetCore.svg)](https://www.nuget.org/packages/Ooui.AspNetCore) | [Ooui.AspNetCore](https://www.nuget.org/packages/Ooui.AspNetCore) | Integration with ASP.NET Core | | [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Forms.svg)](https://www.nuget.org/packages/Ooui.Forms) | [Ooui.Forms](https://www.nuget.org/packages/Ooui.Forms) | Xamarin.Forms backend using Ooui | -| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.AspNetCore.svg)](https://www.nuget.org/packages/Ooui.AspNetCore) | [Ooui.AspNetCore](https://www.nuget.org/packages/Ooui.AspNetCore) | Integration with ASP.NET Core MVC | -Ooui (pronounced *weeee!*) is a small cross-platform UI library for .NET that uses web technologies. +Ooui (pronounced *weee!*) is a small cross-platform UI library for .NET that uses web technologies. It presents a classic object-oriented UI API that controls a dumb browser. With Ooui, you get the full power of your favorite .NET programming language *plus* the ability to interact with your app using any device. @@ -31,11 +31,10 @@ dotnet run --project Samples/Samples.csproj --no-build This will open the default starting page for the Samples. Now point your browser at [http://localhost:8080/shared-button](http://localhost:8080/shared-button) -You should see a button that tracks the number of times it was clicked. -The source code for that button is shown in the example below. +You should see a button that tracks the number of times it was clicked. The source code for that button is shown in the example below. -## Example Use +## Example App Here is the complete source code to a fully collaborative button clicking app. @@ -67,119 +66,47 @@ class Program } ``` -Make sure to add a reference to Ooui before you try running! +Make sure to add a reference to Ooui before you start running! ```bash dotnet add package Ooui +dotnet run ``` -With just that code, the user will be presented with a silly counting button. +With just that code, a web server that serves the HTML and web socket logic necessary for an interactive button will start. -In fact, any number of users can hit that URL and start interacting with the same button. That's right, automatic collaboration! -If you want each user to get their own button, then you will instead `Publish` a **function** to create it: -```csharp -Button MakeButton() -{ - var button = new Button("Click me!"); - var count = 0; - button.Click += (s, e) => { - count++; - button.Text = $"Clicked {count} times"; - }; - return button; -} +## The Many Ways to Ooui -UI.Publish("/button", MakeButton); -``` +Ooui has been broken up into several packages to increase the variety of ways that it can be used. Here are some combinations to help you decide which way is best for you. -Now every user (well, every load of the page) will get their own button. + + + + + + + + + + + + + + + + +
OouiOoui.AspNetCoreOoui.Forms
Write the UI using the web DOM and use the built-in web server
Write the UI using the web DOM and serve it with ASP.NET Core
Write the UI using Xamarin.Forms and serve it with ASP.NET Core
Write the UI using Xamarin.Forms and use the built-in web server
## How it works -When the user requests a page, Ooui will connect to the client using a Web Socket. This socket is used to keep an in-memory model of the UI (the one you work with as a programmer) in sync with the actual UI shown to the user in their browser. This is done using a simple messaging protocol with JSON packets. +When the user requests a page, the page will connect to the server using a web socket. This socket is used to keep the server's in-memory model of the UI (the one you work with as a programmer) in sync with the actual UI shown to the user in their browser. This is done using a simple messaging protocol with JSON packets. When the user clicks or otherwise interacts with the UI, those events are sent back over the web socket so that your code can deal with them. -## Comparison - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
UI LibraryOouiXamarin.FormsASP.NET MVC
How big is it?80 KB850 KB1,300 KB
Where does it run?EverywhereiOS, Android, Mac, WindowsEverywhere
How do I make a button?
new Button()
new Button()
<button />
Does it use native controls?No, HTML5 controlsYes!HTML5 controls
What controls are available?All of those in HTML5Xamarin.Forms controlsAll of those in HTML5
Which architecture will you force me to use?None, you're freeMVVMMVC/MVVM
What's the templating language?C#XAMLRazor
How do I style things?CSS baby!XAML resourcesCSS
Do I need to run a server?NopeHeck noYes
Is it web scale?Yes?What's the web?Yes!
+## Contributing +Ooui is open source and I love merging PRs. Please fork away, and please obey the .editorconfig file. :-) Try to file issues for things that you want to work on *before* you start the work so that there's no duplicated effort. If you just want to help out, check out the issues and dive in! From e90fcc71aaf4413f279d4573d4d4b27907728240 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 7 Feb 2018 16:31:01 -0800 Subject: [PATCH 02/52] Add link to new getting started --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6910c21..1c1f36b 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Ooui has been broken up into several packages to increase the variety of ways th OouiOoui.AspNetCoreOoui.Forms -✓Write the UI using the web DOM and use the built-in web server +✓Write the UI using the web DOM and use the built-in web server ✓✓Write the UI using the web DOM and serve it with ASP.NET Core From 089186c699d37893506e4ccbdc9d62db439e2275 Mon Sep 17 00:00:00 2001 From: Eric Sink Date: Sat, 10 Feb 2018 20:12:33 -0600 Subject: [PATCH 03/52] For XF, add support for list item click. I'm new to the Ooui code, so I won't be surprised if this is not done quite right. Feedback welcome. Nonetheless, it seems to work. --- Ooui.Forms/Renderers/ListViewRenderer.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Ooui.Forms/Renderers/ListViewRenderer.cs b/Ooui.Forms/Renderers/ListViewRenderer.cs index da198fe..96dbe59 100644 --- a/Ooui.Forms/Renderers/ListViewRenderer.cs +++ b/Ooui.Forms/Renderers/ListViewRenderer.cs @@ -82,6 +82,7 @@ namespace Ooui.Forms.Renderers listItem.Style["list-style-type"] = "none"; listItem.AppendChild(cell); + listItem.Click += ListItem_Click; _cells.Add(listItem); } @@ -92,6 +93,13 @@ namespace Ooui.Forms.Renderers } } + private void ListItem_Click(object sender, TargetEventArgs e) + { + var it = (ListItem)sender; + var ndx = _cells.IndexOf(it); + Element.NotifyRowTapped(ndx, null); + } + private void UpdateBackgroundColor() { var backgroundColor = Element.BackgroundColor.ToOouiColor(); From c22e3f84d8893716593742b6d14bfed1a89b18f7 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Tue, 13 Feb 2018 17:34:07 +0000 Subject: [PATCH 04/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 50d6807a9536b6d1aad2235493596368b8f26139 Mon Sep 17 00:00:00 2001 From: Eric Sink Date: Wed, 14 Feb 2018 21:10:58 -0600 Subject: [PATCH 05/52] attempt to properly unsubscribe the click handler for List cells. --- Ooui.Forms/Renderers/ListViewRenderer.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Ooui.Forms/Renderers/ListViewRenderer.cs b/Ooui.Forms/Renderers/ListViewRenderer.cs index 96dbe59..efaf229 100644 --- a/Ooui.Forms/Renderers/ListViewRenderer.cs +++ b/Ooui.Forms/Renderers/ListViewRenderer.cs @@ -48,6 +48,8 @@ namespace Ooui.Forms.Renderers protected override void Dispose(bool disposing) { + UnsubscribeCellClicks(); + base.Dispose(disposing); if (disposing && !_disposed) @@ -56,8 +58,17 @@ namespace Ooui.Forms.Renderers } } + private void UnsubscribeCellClicks() + { + foreach (var c in _cells) + { + c.Click -= ListItem_Click; + } + } + private void UpdateItems() { + UnsubscribeCellClicks(); _cells.Clear(); var items = TemplatedItemsView.TemplatedItems; From 7267bb2687255488318ac0f3dd00cb5e92b92185 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Wed, 21 Feb 2018 15:40:12 -0800 Subject: [PATCH 06/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 07/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 08/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 09/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 10/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 11/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 12/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 13/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 + + + + + +
+

  Loading...

+ +
- - - \ 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 14/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 15/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 16/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 17/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 18/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 19/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 20/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 21/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 22/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 + + + + +

+

+ Number: +

+ Result: ... +

+ +
+

+ + + + + From 9482b65b86ab27c027238375bc53fa48dba969e4 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 18:54:50 -0800 Subject: [PATCH 23/52] Remove AOT wasm --- 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 deletions(-) delete mode 100644 Ooui.Wasm/Dockerfile delete mode 100644 Ooui.Wasm/Makefile delete mode 100644 Ooui.Wasm/System.Core.cs delete mode 100644 Ooui.Wasm/hello.cs delete mode 100644 Ooui.Wasm/index.html diff --git a/Ooui.Wasm/Dockerfile b/Ooui.Wasm/Dockerfile deleted file mode 100644 index bf88989..0000000 --- a/Ooui.Wasm/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index cd11c33..0000000 --- a/Ooui.Wasm/Makefile +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index f8d5d11..0000000 --- a/Ooui.Wasm/System.Core.cs +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index b802c97..0000000 --- a/Ooui.Wasm/hello.cs +++ /dev/null @@ -1,77 +0,0 @@ -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 deleted file mode 100644 index 121112d..0000000 --- a/Ooui.Wasm/index.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - WebAssembly Example - - - - -

-

- Number: -

- Result: ... -

- -
-

- - - - - From 93229cf3c8907cfab8f5bfd2c13c9b9f502601ec Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 18:57:25 -0800 Subject: [PATCH 24/52] Fix merge mistake --- Ooui/Platform.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Ooui/Platform.cs b/Ooui/Platform.cs index d0f3cc9..9884253 100644 --- a/Ooui/Platform.cs +++ b/Ooui/Platform.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.Reflection; +using System.Linq; namespace Ooui { From 5d47c9adef55b8067f772fbab62ca9a017038e16 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 19:11:57 -0800 Subject: [PATCH 25/52] Wasm is working --- Ooui.Wasm/ooui-sample.cs | 4 ++++ Ooui/Client.js | 18 +++++++++++++----- Ooui/WebAssemblySession.cs | 23 ++++++++++++----------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index d53d6ad..bcfb254 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -9,6 +9,10 @@ public class Program var l = new Label { Text = "Hello" }; var b = new Button ("Click Me"); var e = new Div (new Div (l), b); + var c = 0; + b.Click += (s, ee) => { + b.Text = $"Clicked {c} times"; + }; UI.SetGlobalElement ("main", e); return e.ToString (); diff --git a/Ooui/Client.js b/Ooui/Client.js index e89fbe2..028ca5e 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -322,6 +322,16 @@ function fixupValue (v) { // == WASM Support == +window["__oouiReceiveMessages"] = function (sessionId, messages) +{ + console.log ("RCV", messages); + messages.forEach (function (m) { + console.log ('Raw value from server', m.v); + m.v = fixupValue (m.v); + processMessage (m); + }); +}; + var Module = { onRuntimeInitialized: function () { console.log ("Done with WASM module instantiation."); @@ -405,22 +415,20 @@ var MonoRuntime = { 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 { + var rres = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + var res = MonoRuntime.conv_string (rres); 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); + return res; } catch (e) { return e.msg; } diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index 47199ac..7ea2f20 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; namespace Ooui { @@ -17,16 +18,12 @@ namespace Ooui 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 // @@ -36,19 +33,23 @@ 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); - WebAssembly.Runtime.InvokeJS ("alert(" + messagesToSend.Count + ")"); - WebAssembly.Runtime.InvokeJS ("console.log('t end')"); + var sb = new StringBuilder (); + var head = ""; + sb.Append ("["); + foreach (var m in messagesToSend) { + sb.Append (head); + sb.Append (m.ToJson ()); + head = ","; + } + sb.Append ("]"); + var json = sb.ToString (); + WebAssembly.Runtime.InvokeJS ("__oouiReceiveMessages(\"" + id + "\", " + json + ")"); } public void ReceiveMessageJson (string json) From 03781339f4dcd3a3c5ee5e2773e13e1da07a520b Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 19:39:01 -0800 Subject: [PATCH 26/52] Send events back to Ooui from JS --- Ooui.Wasm/ooui-sample.cs | 5 +-- Ooui.Wasm/ooui-sample.html | 1 + Ooui/Client.js | 65 +++++++++++++++++++++++--------------- Ooui/WebAssemblySession.cs | 4 --- 4 files changed, 44 insertions(+), 31 deletions(-) diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index bcfb254..ac57e88 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -9,9 +9,10 @@ public class Program var l = new Label { Text = "Hello" }; var b = new Button ("Click Me"); var e = new Div (new Div (l), b); - var c = 0; + int count = 0; b.Click += (s, ee) => { - b.Text = $"Clicked {c} times"; + count++; + b.Text = $"Clicked {count} times"; }; UI.SetGlobalElement ("main", e); diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index 2c9dc7d..cfb1dfb 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -39,6 +39,7 @@ "System.Reflection.Extensions.dll", "System.Runtime.dll", "System.Runtime.Extensions.dll", + "System.Runtime.Serialization.dll", "System.Runtime.Serialization.Primitives.dll", "System.Xml.dll", "System.Xml.ReaderWriter.dll", diff --git a/Ooui/Client.js b/Ooui/Client.js index 028ca5e..9126093 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -6,6 +6,17 @@ const nodes = {}; const hasText = {}; let socket = null; +let wasmSession = null; + +function send (json) { + if (debug) console.log ("Send", json); + if (socket != null) { + socket.send (json); + } + else if (wasmSession != null) { + WebAssemblyApp.receiveMessagesJson (wasmSession, json); + } +} const mouseEvents = { click: true, @@ -117,8 +128,7 @@ function ooui (rootElementPath) { }; saveSize (em.v); const ems = JSON.stringify (em); - if (socket != null) - socket.send (ems); + send (ems); if (debug) console.log ("Event", em); } }()); @@ -130,10 +140,6 @@ function oouiWasm (mainAsmName, mainNamspace, mainClassName, mainMethodNmae, ass var initialSize = getSize (); - function send (json) { - if (debug) console.log ("Send", json); - } - function resizeHandler() { const em = { m: "event", @@ -269,8 +275,7 @@ function msgListen (m) { }; } const ems = JSON.stringify (em); - if (socket != null) - socket.send (ems); + send (ems); if (debug) console.log ("Event", em); if (em.k === "submit") e.preventDefault (); @@ -324,9 +329,9 @@ function fixupValue (v) { window["__oouiReceiveMessages"] = function (sessionId, messages) { - console.log ("RCV", messages); + if (debug) console.log ("WebAssembly Receive", messages); messages.forEach (function (m) { - console.log ('Raw value from server', m.v); + // console.log ('Raw value from server', m.v); m.v = fixupValue (m.v); processMessage (m); }); @@ -334,13 +339,13 @@ window["__oouiReceiveMessages"] = function (sessionId, messages) var Module = { onRuntimeInitialized: function () { - console.log ("Done with WASM module instantiation."); + if (debug) 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); + if (debug) console.log ("Loading", asm_name); ++pending; fetch ("managed/" + asm_name, { credentials: 'same-origin' }).then (function (response) { if (!response.ok) @@ -357,7 +362,7 @@ var Module = { }, bclLoadingDone: function () { - console.log ("Done loading the BCL."); + if (debug) console.log ("Done loading the BCL."); MonoRuntime.init (); } }; @@ -374,7 +379,7 @@ var MonoRuntime = { this.load_runtime ("managed", 1); - console.log ("Done initializing the runtime."); + if (debug) console.log ("Done initializing the runtime."); WebAssemblyApp.init (); }, @@ -425,38 +430,48 @@ var WebAssemblyApp = { runApp: function (a, b) { try { - var rres = MonoRuntime.call_method (this.add_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); + var sessionId = "main"; + var rres = MonoRuntime.call_method (this.main_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]); var res = MonoRuntime.conv_string (rres); - MonoRuntime.call_method (this.ooui_method, null, [MonoRuntime.mono_string ("main"), MonoRuntime.mono_string ("main")]); + MonoRuntime.call_method (this.ooui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string ("main")]); + wasmSession = sessionId; return res; } catch (e) { return e.msg; } }, + receiveMessagesJson: function (sessionId, json) { + MonoRuntime.call_method (this.ooui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (json)]); + }, + findMethods: function () { - this.ooui_module = MonoRuntime.assembly_load ("Ooui") + 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") + 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) + this.ooui_StartWebAssemblySession_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1); + if (!this.ooui_StartWebAssemblySession_method) throw "Could not find StartWebAssemblySession method"; - this.main_module = MonoRuntime.assembly_load (mainAsmName) + this.ooui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method (this.ooui_class, "ReceiveWebAssemblySessionMessageJson", -1); + if (!this.ooui_ReceiveWebAssemblySessionMessageJson_method) + throw "Could not find ReceiveWebAssemblySessionMessageJson 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) + this.main_class = MonoRuntime.find_class (this.main_module, "", "Program") + if (!this.main_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) + this.main_method = MonoRuntime.find_method (this.main_class, "Main", -1) + if (!this.main_method) throw "Could not find Main method"; }, }; diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index 7ea2f20..df1b3e9 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -55,7 +55,6 @@ namespace Ooui public void ReceiveMessageJson (string json) { try { - Info ("RECEIVED: " + json); var message = Newtonsoft.Json.JsonConvert.DeserializeObject (json); element.Receive (message); } @@ -66,7 +65,6 @@ namespace Ooui public void StartSession () { - WebAssembly.Runtime.InvokeJS ("console.log('was start session 0')"); // // Start watching for changes in the element // @@ -79,9 +77,7 @@ 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 () From 0f085717adedc8b5234ec4c8b477a603b9ffe2b4 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 21:28:39 -0800 Subject: [PATCH 27/52] Add PCL target to Xamarin.Forms --- Ooui.Forms/DisplayAlert.cs | 4 +- Ooui.Forms/Forms.cs | 31 +++++++++++- Ooui.Forms/Ooui.Forms.csproj | 12 ++--- Ooui.Forms/PageExtensions.cs | 2 + Ooui.Forms/Platform.cs | 5 +- Ooui.Forms/Renderers/ImageRenderer.cs | 11 ++++- Ooui.Forms/ResourcesProvider.cs | 9 ++++ Ooui.Wasm/ooui-sample.html | 3 ++ Ooui/UI.cs | 34 +++++-------- Ooui/Utilities.cs | 69 +++++++++++++++++++++++++++ 10 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 Ooui/Utilities.cs diff --git a/Ooui.Forms/DisplayAlert.cs b/Ooui.Forms/DisplayAlert.cs index fae811a..0770cfa 100644 --- a/Ooui.Forms/DisplayAlert.cs +++ b/Ooui.Forms/DisplayAlert.cs @@ -1,4 +1,4 @@ -using System.Web; +using System; using Xamarin.Forms.Internals; namespace Ooui.Forms @@ -31,7 +31,7 @@ namespace Ooui.Forms ClassName = "close" }; - _closeButton.AppendChild(new Span(HttpUtility.HtmlDecode("×"))); + _closeButton.AppendChild(new Span("×")); var h4 = new Heading(4) { diff --git a/Ooui.Forms/Forms.cs b/Ooui.Forms/Forms.cs index c648cfa..e8e22e9 100644 --- a/Ooui.Forms/Forms.cs +++ b/Ooui.Forms/Forms.cs @@ -21,7 +21,7 @@ namespace Xamarin.Forms return; IsInitialized = true; - Log.Listeners.Add (new DelegateLogListener ((c, m) => Trace.WriteLine (m, c))); + Log.Listeners.Add (new DelegateLogListener ((c, m) => System.Diagnostics.Debug.WriteLine (m, c))); Device.SetIdiom (TargetIdiom.Desktop); Device.PlatformServices = new OouiPlatformServices (); @@ -64,7 +64,11 @@ namespace Xamarin.Forms public Assembly[] GetAssemblies () { +#if PCL + return new[] { typeof (Xamarin.Forms.View).GetTypeInfo ().Assembly, typeof (Forms.OouiPlatformServices).GetTypeInfo ().Assembly }; +#else return AppDomain.CurrentDomain.GetAssemblies (); +#endif } public string GetMD5Hash (string input) @@ -140,6 +144,31 @@ namespace Xamarin.Forms } } +#if PCL + + public delegate void TimerCallback(object state); + + public sealed class Timer : CancellationTokenSource, IDisposable + { + public Timer (TimerCallback callback, object state, int dueTime, int period) + { + Task.Run (async () => { + await Task.Delay (dueTime).ConfigureAwait (false); + if (!IsCancellationRequested) + callback (state); + while (!IsCancellationRequested) { + await Task.Delay (period).ConfigureAwait (false); + if (!IsCancellationRequested) + callback (state); + } + }); + } + + public new void Dispose() { base.Cancel(); } + } + +#endif + public void QuitApplication() { } diff --git a/Ooui.Forms/Ooui.Forms.csproj b/Ooui.Forms/Ooui.Forms.csproj index 440e8f2..b439ba6 100644 --- a/Ooui.Forms/Ooui.Forms.csproj +++ b/Ooui.Forms/Ooui.Forms.csproj @@ -9,16 +9,14 @@ 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 + true - - true - - - - true + + PCL + diff --git a/Ooui.Forms/PageExtensions.cs b/Ooui.Forms/PageExtensions.cs index 4ffe01d..b2174cb 100644 --- a/Ooui.Forms/PageExtensions.cs +++ b/Ooui.Forms/PageExtensions.cs @@ -5,6 +5,7 @@ namespace Xamarin.Forms { public static class PageExtensions { +#if !PCL public static void Publish (this Xamarin.Forms.Page page, string path) { Ooui.UI.Publish (path, () => page.CreateElement ()); @@ -15,6 +16,7 @@ namespace Xamarin.Forms var lazyPage = new Lazy ((() => page.CreateElement ()), true); Ooui.UI.Publish (path, () => lazyPage.Value); } +#endif public static Ooui.Element GetOouiElement (this Xamarin.Forms.Page page) { diff --git a/Ooui.Forms/Platform.cs b/Ooui.Forms/Platform.cs index 96197cb..635fe97 100644 --- a/Ooui.Forms/Platform.cs +++ b/Ooui.Forms/Platform.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Ooui.Forms.Renderers; using Xamarin.Forms; using Xamarin.Forms.Internals; -using System.Web; namespace Ooui.Forms { @@ -142,7 +141,7 @@ namespace Ooui.Forms void AddChild (VisualElement view) { if (!Application.IsApplicationOrNull (view.RealParent)) - Console.Error.WriteLine ("Tried to add parented view to canvas directly"); + System.Diagnostics.Debug.WriteLine ("Tried to add parented view to canvas directly"); if (GetRenderer (view) == null) { var viewRenderer = CreateRenderer (view); @@ -152,7 +151,7 @@ namespace Ooui.Forms viewRenderer.SetElementSize (new Size (640, 480)); } else - Console.Error.WriteLine ("Potential view double add"); + System.Diagnostics.Debug.WriteLine ("Potential view double add"); } void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/Ooui.Forms/Renderers/ImageRenderer.cs b/Ooui.Forms/Renderers/ImageRenderer.cs index 3f9e6e2..7b429bc 100644 --- a/Ooui.Forms/Renderers/ImageRenderer.cs +++ b/Ooui.Forms/Renderers/ImageRenderer.cs @@ -139,8 +139,12 @@ namespace Ooui.Forms.Renderers public sealed class FileImageSourceHandler : IImageSourceHandler { +#pragma warning disable 1998 public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) { +#if PCL + return null; +#else string image = null; var filesource = imagesource as FileImageSource; var file = filesource?.File; @@ -155,6 +159,7 @@ namespace Ooui.Forms.Renderers } } return image; +#endif } } @@ -162,6 +167,9 @@ namespace Ooui.Forms.Renderers { public async Task LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) { +#if PCL + return null; +#else string image = null; var streamsource = imagesource as StreamImageSource; if (streamsource?.Stream != null) { @@ -171,7 +179,7 @@ namespace Ooui.Forms.Renderers using (var outputStream = new System.IO.MemoryStream (data)) { await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false); } - var hash = Ooui.UI.Hash (data); + var hash = Ooui.Utilities.Hash (data); var etag = "\"" + hash + "\""; image = "/images/" + hash; if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) { @@ -188,6 +196,7 @@ namespace Ooui.Forms.Renderers System.Diagnostics.Debug.WriteLine ("Could not load image: {0}", streamsource); } return image; +#endif } } diff --git a/Ooui.Forms/ResourcesProvider.cs b/Ooui.Forms/ResourcesProvider.cs index c0c05a3..c71be52 100644 --- a/Ooui.Forms/ResourcesProvider.cs +++ b/Ooui.Forms/ResourcesProvider.cs @@ -40,3 +40,12 @@ namespace Ooui.Forms } } } + +#if PCL +namespace System.Collections.Concurrent +{ + class ConcurrentDictionary : Dictionary + { + } +} +#endif diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index cfb1dfb..f47470c 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -44,6 +44,9 @@ "System.Xml.dll", "System.Xml.ReaderWriter.dll", "System.Xml.XDocument.dll", + "Xamarin.Forms.Core.dll", + "Xamarin.Forms.Platform.dll", + "Xamarin.Forms.Xaml.dll", "Ooui.dll", mainAsmName + ".dll" ]; diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 893ef63..dc87fa9 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -14,10 +14,8 @@ namespace Ooui public const int MaxFps = 30; #if !PCL - static readonly ManualResetEvent started = new ManualResetEvent (false); - [ThreadStatic] - static System.Security.Cryptography.SHA256 sha256; + static readonly ManualResetEvent started = new ManualResetEvent (false); static CancellationTokenSource serverCts; @@ -82,22 +80,7 @@ namespace Ooui clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ()); } } - clientJsEtag = "\"" + Hash (clientJsBytes) + "\""; - } - - public static string Hash (byte[] bytes) - { - var sha = sha256; - if (sha == null) { - sha = System.Security.Cryptography.SHA256.Create (); - sha256 = sha; - } - var data = sha.ComputeHash (bytes); - StringBuilder sBuilder = new StringBuilder (); - for (int i = 0; i < data.Length; i++) { - sBuilder.Append (data[i].ToString ("x2")); - } - return sBuilder.ToString (); + clientJsEtag = "\"" + Utilities.Hash (clientJsBytes) + "\""; } static void Publish (string path, RequestHandler handler) @@ -129,13 +112,13 @@ namespace Ooui if (contentType == null) { contentType = GuessContentType (path, filePath); } - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, contentType)); } public static void PublishFile (string path, byte[] data, string contentType) { - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, contentType)); } @@ -180,7 +163,7 @@ namespace Ooui public static void PublishJson (string path, object value) { var data = JsonHandler.GetData (value); - var etag = "\"" + Hash (data) + "\""; + var etag = "\"" + Utilities.Hash (data) + "\""; Publish (path, new DataHandler (data, etag, JsonHandler.ContentType)); } @@ -379,13 +362,18 @@ namespace Ooui } } + static string EscapeHtml (string text) + { + return text.Replace ("&", "&").Replace ("<", "<"); + } + public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml) { writer.Write (@" "); - writer.Write (title.Replace ("&", "&").Replace ("<", "<")); + writer.Write (EscapeHtml (title)); writer.Write (@" "); diff --git a/Ooui/Utilities.cs b/Ooui/Utilities.cs new file mode 100644 index 0000000..a850099 --- /dev/null +++ b/Ooui/Utilities.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; + +namespace Ooui +{ + public static class Utilities + { +#if PCL + + static readonly uint[] crcTable; + + static Utilities () + { + uint p = 0x04C11DB7; + crcTable = new uint[256]; + for (uint c = 0; c <= 0xFF; c++) { + crcTable[c] = CrcReflect (c, 8) << 24; + for (uint i = 0; i < 8; i++) { + crcTable[c] = (crcTable[c] << 1) ^ (((crcTable[c] & (1u << 31)) != 0) ? p : 0); + } + crcTable[c] = CrcReflect (crcTable[c], 32); + } + } + + static uint CrcReflect (uint r, byte c) + { + uint v = 0; + for (int i = 1; i < (c + 1); i++) { + if ((r & 1) != 0) { + v |= (1u << (c - i)); + } + r >>= 1; + } + return v; + } + + public static string Hash (byte[] bytes) + { + uint crc = 0xffffffffu; + for (var i = 0; i < bytes.Length; i++) { + crc = (crc >> 8) ^ crcTable[(crc & 0xff) ^ bytes[i]]; + } + crc ^= 0xffffffffu; + return crc.ToString ("x8"); + } + +#else + + [ThreadStatic] + static System.Security.Cryptography.SHA256 sha256; + + public static string Hash (byte[] bytes) + { + var sha = sha256; + if (sha == null) { + sha = System.Security.Cryptography.SHA256.Create (); + sha256 = sha; + } + var data = sha.ComputeHash (bytes); + StringBuilder sBuilder = new StringBuilder (); + for (int i = 0; i < data.Length; i++) { + sBuilder.Append (data[i].ToString ("x2")); + } + return sBuilder.ToString (); + } + +#endif + } +} From 78528a0c87f078923b32e9c1831658579ba851c3 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:12:07 -0800 Subject: [PATCH 28/52] BoxViewClock Forms sample working in wasm --- Ooui.Wasm/README.md | 2 +- Ooui.Wasm/ooui-sample.cs | 30 ++++++++++++++++++-------- Ooui.Wasm/ooui-sample.html | 2 ++ Ooui/Session.cs | 2 +- Ooui/WebAssemblySession.cs | 43 +++++++++++++++++++++----------------- 5 files changed, 49 insertions(+), 30 deletions(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index d9af42a..d48b9fb 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/System.Runtime.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/Xamarin.Forms.Core.dll /r:managed/Ooui.dll /r:managed/Ooui.Forms.dll /out:managed/Ooui.Sample.dll ../Samples/ISample.cs ../Samples/BoxViewClockSample.cs ooui-sample.cs ``` diff --git a/Ooui.Wasm/ooui-sample.cs b/Ooui.Wasm/ooui-sample.cs index ac57e88..896bd1c 100644 --- a/Ooui.Wasm/ooui-sample.cs +++ b/Ooui.Wasm/ooui-sample.cs @@ -3,18 +3,30 @@ using Ooui; public class Program { + static Element GetButtonElement () + { + var l = new Label { Text = "Hello" }; + var b = new Button ("Click Me"); + var e = new Div (new Div (l), b); + int count = 0; + b.Click += (s, ee) => { + count++; + b.Text = $"Clicked {count} times"; + }; + return e; + } + + static Element GetBoxViewClockElement () + { + var s = new Samples.BoxViewClockSample (); + return s.CreateElement (); + } + public static string Main (string a0, string a1) { + Xamarin.Forms.Forms.Init (); try { - var l = new Label { Text = "Hello" }; - var b = new Button ("Click Me"); - var e = new Div (new Div (l), b); - int count = 0; - b.Click += (s, ee) => { - count++; - b.Text = $"Clicked {count} times"; - }; - + var e = GetBoxViewClockElement (); UI.SetGlobalElement ("main", e); return e.ToString (); } diff --git a/Ooui.Wasm/ooui-sample.html b/Ooui.Wasm/ooui-sample.html index f47470c..1a610a9 100644 --- a/Ooui.Wasm/ooui-sample.html +++ b/Ooui.Wasm/ooui-sample.html @@ -37,6 +37,7 @@ "System.Threading.Tasks.dll", "System.Reflection.dll", "System.Reflection.Extensions.dll", + "System.Resources.ResourceManager.dll", "System.Runtime.dll", "System.Runtime.Extensions.dll", "System.Runtime.Serialization.dll", @@ -48,6 +49,7 @@ "Xamarin.Forms.Platform.dll", "Xamarin.Forms.Xaml.dll", "Ooui.dll", + "Ooui.Forms.dll", mainAsmName + ".dll" ]; oouiWasm (mainAsmName, "", "Program", "Main", assemblies); diff --git a/Ooui/Session.cs b/Ooui/Session.cs index 029c530..2b8570e 100644 --- a/Ooui/Session.cs +++ b/Ooui/Session.cs @@ -44,7 +44,7 @@ namespace Ooui } } - void QueueMessageLocked (Message message) + protected void QueueMessageLocked (Message message) { // // Make sure all the referenced objects have been created diff --git a/Ooui/WebAssemblySession.cs b/Ooui/WebAssemblySession.cs index df1b3e9..b7d53f4 100644 --- a/Ooui/WebAssemblySession.cs +++ b/Ooui/WebAssemblySession.cs @@ -18,38 +18,43 @@ namespace Ooui protected override void QueueMessage (Message message) { - base.QueueMessage (message); - TransmitQueuedMessages (); - } - - void TransmitQueuedMessages () - { - // - // Dequeue as many messages as we can - // - var messagesToSend = new List (); lock (queuedMessages) { - messagesToSend.AddRange (queuedMessages); + QueueMessageLocked (message); + var max = 1; + var i = 0; + while (i < queuedMessages.Count) { + TransmitQueuedMessagesLocked (queuedMessages, i, max); + i += max; + } + // WebAssembly.Runtime.InvokeJS("console.log('TRANSMITTED'," + queuedMessages.Count + ")"); queuedMessages.Clear (); } + } + void TransmitQueuedMessagesLocked (List messagesToSend, int startIndex, int max) + { if (messagesToSend.Count == 0) return; // // Now actually send the messages // - var sb = new StringBuilder (); + var sb = new System.IO.StringWriter (); + sb.Write ("__oouiReceiveMessages(\""); + sb.Write (id); + sb.Write ("\","); + sb.Write ("["); var head = ""; - sb.Append ("["); - foreach (var m in messagesToSend) { - sb.Append (head); - sb.Append (m.ToJson ()); + int n = 0; + for (var i = startIndex; i < messagesToSend.Count && n < max; i++, n++) { + sb.Write (head); + messagesToSend[i].WriteJson (sb); head = ","; } - sb.Append ("]"); - var json = sb.ToString (); - WebAssembly.Runtime.InvokeJS ("__oouiReceiveMessages(\"" + id + "\", " + json + ")"); + sb.Write ("])"); + var jsonp = sb.ToString (); + // WebAssembly.Runtime.InvokeJS("console.log('TRANSMIT',"+n+")"); + WebAssembly.Runtime.InvokeJS (jsonp); } public void ReceiveMessageJson (string json) From 5f382847f94948294330e912b0061f52b17d39c1 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:13:41 -0800 Subject: [PATCH 29/52] Update README.md --- Ooui.Wasm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index d48b9fb..243f84e 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -23,4 +23,4 @@ csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime. python server.py ``` -Go to `locahost:8000/ooui.html` +Go to `locahost:8000/ooui-sample.html` From ea8cdfa25e4398e322b11d624f601cb5800ed650 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Fri, 9 Mar 2018 23:14:18 -0800 Subject: [PATCH 30/52] Update README.md --- Ooui.Wasm/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui.Wasm/README.md b/Ooui.Wasm/README.md index 243f84e..5bdc6b9 100644 --- a/Ooui.Wasm/README.md +++ b/Ooui.Wasm/README.md @@ -23,4 +23,4 @@ csc /nostdlib /target:library /r:managed/mscorlib.dll /r:managed/System.Runtime. python server.py ``` -Go to `locahost:8000/ooui-sample.html` +Go to [locahost:8000/ooui-sample.html](http://locahost:8000/ooui-sample.html) From 80dc2bfaf99dd730ca814ce6bc3a8ef0cb2fb2cd Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Mon, 12 Mar 2018 19:42:08 -0700 Subject: [PATCH 31/52] Fix json serializing unknown types --- Ooui/JsonConvert.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ooui/JsonConvert.cs b/Ooui/JsonConvert.cs index fdeeed2..fc8e86a 100644 --- a/Ooui/JsonConvert.cs +++ b/Ooui/JsonConvert.cs @@ -86,7 +86,7 @@ namespace Ooui w.Write (((float)value).ToString (icult)); } - WriteJsonString (w, Convert.ToString (value, icult)); + w.Write (Newtonsoft.Json.JsonConvert.SerializeObject (value)); } public static string SerializeObject (object value) From 1f5b1fe13a5ee7846ddc955d918ed6def02cdb3d Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Mon, 12 Mar 2018 20:56:12 -0700 Subject: [PATCH 32/52] Stop using XAML for editor sample --- Samples/Samples.csproj | 6 -- Samples/XamlPreviewPage.xaml | 22 ------- Samples/XamlPreviewPage.xaml.cs | 72 --------------------- Samples/XamlPreviewPageSample.cs | 105 ++++++++++++++++++++++++++++++- 4 files changed, 103 insertions(+), 102 deletions(-) delete mode 100644 Samples/XamlPreviewPage.xaml delete mode 100644 Samples/XamlPreviewPage.xaml.cs diff --git a/Samples/Samples.csproj b/Samples/Samples.csproj index 98ffe10..effd0d6 100644 --- a/Samples/Samples.csproj +++ b/Samples/Samples.csproj @@ -32,9 +32,6 @@ DisplayAlertPage.xaml - - XamlPreviewPage.xaml - @@ -56,9 +53,6 @@ MSBuild:Compile - - MSBuild:UpdateDesignTimeXaml - diff --git a/Samples/XamlPreviewPage.xaml b/Samples/XamlPreviewPage.xaml deleted file mode 100644 index a96fa29..0000000 --- a/Samples/XamlPreviewPage.xaml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/XamlPreviewPage.xaml.cs b/Samples/XamlPreviewPage.xaml.cs deleted file mode 100644 index 21f9318..0000000 --- a/Samples/XamlPreviewPage.xaml.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using Xamarin.Forms; - -namespace Samples -{ - public partial class XamlPreviewPage : ContentPage - { - public XamlPreviewPage () - { - InitializeComponent (); - - editor.Text = @" - - - - - - - - - - -