Merge remote-tracking branch 'refs/remotes/praeclarum/master'

This commit is contained in:
Javier Suárez Ruiz 2018-03-15 19:40:02 +01:00
commit 0bd26e7519
45 changed files with 1824 additions and 655 deletions

6
.gitignore vendored
View File

@ -1,3 +1,9 @@
# Wasm SDK
/Ooui.Wasm/*.nupkg
/Ooui.Wasm.Test
/Ooui.Wasm.Old
# Social media files
/Media

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "Ooui.Wasm.Build.Tasks/linker"]
path = Ooui.Wasm.Build.Tasks/linker
url = git@github.com:mono/linker.git

View File

@ -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);

View File

@ -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<string, ActiveSession> activeSessions =
new ConcurrentDictionary<string, ActiveSession> ();
static readonly ConcurrentDictionary<string, PendingSession> pendingSessions =
new ConcurrentDictionary<string, PendingSession> ();
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
@ -98,14 +97,14 @@ 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);
}
class ActiveSession
class PendingSession
{
public Element Element;
public DateTime LastConnectTimeUtc;
public DateTime CreateTimeUtc;
}
}
}

View File

@ -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("&times;")));
_closeButton.AppendChild(new Span("×"));
var h4 = new Heading(4)
{

View File

@ -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 ();
@ -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);

View File

@ -9,16 +9,14 @@
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType></DebugType>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<PropertyGroup Condition=" $(TargetFramework) == 'netstandard1.0' ">
<DefineConstants>PCL</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Xamarin.Forms" Version="2.5.0.122203" />
</ItemGroup>

View File

@ -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)

View File

@ -139,6 +139,7 @@ namespace Ooui.Forms.Renderers
public sealed class FileImageSourceHandler : IImageSourceHandler
{
#pragma warning disable 1998
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{
string image = null;
@ -171,7 +172,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) {

View File

@ -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;
@ -82,6 +93,7 @@ namespace Ooui.Forms.Renderers
listItem.Style["list-style-type"] = "none";
listItem.AppendChild(cell);
listItem.Click += ListItem_Click;
_cells.Add(listItem);
}
@ -92,6 +104,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();

View File

@ -0,0 +1,295 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Mono.Cecil;
using Mono.Linker;
using Mono.Linker.Steps;
namespace Ooui.Wasm.Build.Tasks
{
public class BuildDistTask : Task
{
const string SdkUrl = "https://jenkins.mono-project.com/job/test-mono-mainline-webassembly/62/label=highsierra/Azure/processDownloadRequest/62/highsierra/sdks/wasm/mono-wasm-ddf4e7be31b.zip";
[Required]
public string Assembly { get; set; }
[Required]
public string OutputPath { get; set; }
public string ReferencePath { get; set; }
public override bool Execute ()
{
try {
InstallSdk ();
GetBcl ();
CreateDist ();
CopyRuntime ();
LinkAssemblies ();
ExtractClientJs ();
DiscoverEntryPoint ();
GenerateHtml ();
return true;
}
catch (Exception ex) {
Log.LogErrorFromException (ex);
return false;
}
}
string sdkPath;
void InstallSdk ()
{
var sdkName = Path.GetFileNameWithoutExtension (new Uri (SdkUrl).AbsolutePath.Replace ('/', Path.DirectorySeparatorChar));
Log.LogMessage ("SDK: " + sdkName);
sdkPath = Path.Combine (Path.GetTempPath (), sdkName);
Log.LogMessage ("SDK Path: " + sdkPath);
if (Directory.Exists (sdkPath))
return;
var client = new WebClient ();
var zipPath = sdkPath + ".zip";
Log.LogMessage ($"Downloading {sdkName} to {zipPath}");
client.DownloadFile (SdkUrl, zipPath);
ZipFile.ExtractToDirectory (zipPath, sdkPath);
Log.LogMessage ($"Extracted {sdkName} to {sdkPath}");
}
string bclPath;
Dictionary<string, string> bclAssemblies;
void GetBcl ()
{
bclPath = Path.Combine (sdkPath, "bcl");
var reals = Directory.GetFiles (bclPath, "*.dll");
var facades = Directory.GetFiles (Path.Combine (bclPath, "Facades"), "*.dll");
var allFiles = reals.Concat (facades);
bclAssemblies = allFiles.ToDictionary (x => Path.GetFileName (x));
}
string distPath;
string managedPath;
void CreateDist ()
{
var outputPath = Path.GetFullPath (OutputPath);
distPath = Path.Combine (outputPath, "dist");
managedPath = Path.Combine (distPath, "managed");
Directory.CreateDirectory (managedPath);
}
void CopyRuntime ()
{
var rtPath = Path.Combine (sdkPath, "release");
var files = new[] { "mono.wasm", "mono.js" };
foreach (var f in files) {
var src = Path.Combine (rtPath, f);
var dest = Path.Combine (distPath, f);
Log.LogMessage ($"Runtime {src} -> {dest}");
File.Copy (src, dest, true);
}
File.Copy (Path.Combine (sdkPath, "server.py"), Path.Combine (distPath, "server.py"), true);
}
List<string> linkedAsmPaths;
List<string> refpaths;
void LinkAssemblies ()
{
var references = ReferencePath.Split (';').Select (x => x.Trim ()).Where (x => x.Length > 0).ToList ();
refpaths = new List<string> ();
foreach (var r in references) {
var name = Path.GetFileName (r);
if (bclAssemblies.ContainsKey (name)) {
refpaths.Add (bclAssemblies[name]);
}
else {
refpaths.Add (r);
}
}
var asmPath = Path.GetFullPath (Assembly);
var pipeline = GetLinkerPipeline ();
using (var context = new LinkContext (pipeline)) {
context.CoreAction = AssemblyAction.CopyUsed;
context.UserAction = AssemblyAction.CopyUsed;
context.OutputDirectory = managedPath;
pipeline.PrependStep (new ResolveFromAssemblyStep (asmPath, ResolveFromAssemblyStep.RootVisibility.Any));
var refdirs = refpaths.Select (x => Path.GetDirectoryName (x)).Distinct ().ToList ();
refdirs.Insert (0, Path.Combine (bclPath, "Facades"));
refdirs.Insert (0, bclPath);
foreach (var d in refdirs.Distinct ()) {
context.Resolver.AddSearchDirectory (d);
}
pipeline.AddStepAfter (typeof (LoadReferencesStep), new LoadI18nAssemblies (I18nAssemblies.None));
foreach (var dll in Directory.GetFiles (managedPath, "*.dll")) {
File.Delete (dll);
}
pipeline.Process (context);
}
linkedAsmPaths = Directory.GetFiles (managedPath, "*.dll").OrderBy (x => Path.GetFileName (x)).ToList ();
}
class PreserveUsingAttributesStep : ResolveStep
{
readonly HashSet<string> ignoreAsmNames;
public PreserveUsingAttributesStep (IEnumerable<string> ignoreAsmNames)
{
this.ignoreAsmNames = new HashSet<string> (ignoreAsmNames);
}
protected override void Process ()
{
var asms = Context.GetAssemblies ();
foreach (var a in asms.Where (x => !ignoreAsmNames.Contains (x.Name.Name))) {
foreach (var m in a.Modules) {
foreach (var t in m.Types) {
PreserveTypeIfRequested (t);
}
}
}
}
void PreserveTypeIfRequested (TypeDefinition type)
{
var typePreserved = IsTypePreserved (type);
if (IsTypePreserved (type)) {
MarkAndPreserveAll (type);
}
else {
foreach (var m in type.Methods.Where (IsMethodPreserved)) {
Annotations.AddPreservedMethod (type, m);
}
foreach (var t in type.NestedTypes) {
PreserveTypeIfRequested (t);
}
}
}
static bool IsTypePreserved (TypeDefinition m)
{
return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null;
}
static bool IsMethodPreserved (MethodDefinition m)
{
return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null;
}
void MarkAndPreserveAll (TypeDefinition type)
{
Annotations.MarkAndPush (type);
Annotations.SetPreserve (type, TypePreserve.All);
if (!type.HasNestedTypes) {
Tracer.Pop ();
return;
}
foreach (TypeDefinition nested in type.NestedTypes)
MarkAndPreserveAll (nested);
Tracer.Pop ();
}
}
Pipeline GetLinkerPipeline ()
{
var p = new Pipeline ();
p.AppendStep (new LoadReferencesStep ());
p.AppendStep (new PreserveUsingAttributesStep (bclAssemblies.Values.Select (Path.GetFileNameWithoutExtension)));
p.AppendStep (new BlacklistStep ());
p.AppendStep (new TypeMapStep ());
p.AppendStep (new MarkStep ());
p.AppendStep (new SweepStep ());
p.AppendStep (new CleanStep ());
p.AppendStep (new RegenerateGuidStep ());
p.AppendStep (new OutputStep ());
return p;
}
void ExtractClientJs ()
{
var oouiPath = refpaths.FirstOrDefault (x => Path.GetFileName (x).Equals ("Ooui.dll", StringComparison.InvariantCultureIgnoreCase));
if (oouiPath == null) {
Log.LogError ("Ooui.dll not included in the project");
return;
}
var oouiAsm = AssemblyDefinition.ReadAssembly (oouiPath);
var clientJs = oouiAsm.MainModule.Resources.FirstOrDefault (x => x.Name.EndsWith ("Client.js", StringComparison.InvariantCultureIgnoreCase)) as EmbeddedResource;
if (clientJs == null) {
Log.LogError ("Ooui.dll missing client javascript");
return;
}
var dest = Path.Combine (distPath, "ooui.js");
using (var srcs = clientJs.GetResourceStream ()) {
using (var dests = new FileStream (dest, FileMode.Create, FileAccess.Write)) {
srcs.CopyTo (dests);
}
}
Log.LogMessage ($"Client JS {dest}");
}
MethodDefinition entryPoint;
void DiscoverEntryPoint ()
{
var asm = AssemblyDefinition.ReadAssembly (Assembly);
entryPoint = asm.EntryPoint;
if (entryPoint == null) {
throw new Exception ($"{Path.GetFileName (Assembly)} is missing an entry point");
}
}
void GenerateHtml ()
{
var htmlPath = Path.Combine (distPath, "index.html");
using (var w = new StreamWriter (htmlPath, false, new UTF8Encoding (false))) {
w.Write (@"<!DOCTYPE html>
<html>
<head>
<meta charset=""utf-8"" />
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
<link rel=""stylesheet"" href=""https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"">
</head>
<body>
<div id=""ooui-body"" class=""container-fluid"">
<p id=""loading""><i class=""fa fa-refresh fa-spin"" style=""font-size:14px;margin-right:0.5em;""></i> Loading...</p>
</div>
<script defer type=""text/javascript"" src=""ooui.js""></script>
<script type=""text/javascript"">
var assemblies = [");
var head = "";
foreach (var l in linkedAsmPaths.Select (x => Path.GetFileName (x))) {
w.Write (head);
w.Write ('\"');
w.Write (l);
w.Write ('\"');
head = ",";
}
w.WriteLine ($@"];
document.addEventListener(""DOMContentLoaded"", function(event) {{
oouiWasm(""{entryPoint.DeclaringType.Module.Assembly.Name.Name}"", ""{entryPoint.DeclaringType.Namespace}"", ""{entryPoint.DeclaringType.Name}"", ""{entryPoint.Name}"", assemblies);
}});
</script>
<script defer type=""text/javascript"" src=""mono.js""></script>
</body>
</html>");
}
Log.LogMessage ($"HTML {htmlPath}");
}
}
}

View File

@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<DefineConstants>NET_CORE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build" Version="15.3.409" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.3.409" />
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.3.409" />
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="15.3.409" />
</ItemGroup>
<ItemGroup>
<Compile Remove="linker\**" />
<Compile Include="linker\linker\Linker\**" />
<Compile Include="linker\linker\Linker.Steps\**" />
<Compile Include="linker\cecil\Mono.Cecil\**" />
<Compile Include="linker\cecil\Mono.Cecil.Cil\**" />
<Compile Include="linker\cecil\Mono.Cecil.Metadata\**" />
<Compile Include="linker\cecil\Mono.Cecil.PE\**" />
<Compile Include="linker\cecil\Mono.Collections.Generic\**" />
<Compile Include="linker\cecil\Mono\**" />
<Compile Remove="linker\linker\Linker\Driver.cs" />
<Compile Remove="linker\linker\Linker\AssemblyInfo.cs" />
<Compile Remove="linker\cecil\Mono.Cecil\AssemblyInfo.cs" />
</ItemGroup>
</Project>

@ -0,0 +1 @@
Subproject commit 1dcc9afa256c8e94050b6a21f03b503508e47f05

4
Ooui.Wasm/Makefile Normal file
View File

@ -0,0 +1,4 @@
all:
msbuild /p:Configuration=Release /t:Restore ../Ooui.Wasm.Build.Tasks/Ooui.Wasm.Build.Tasks.csproj
msbuild /p:Configuration=Release ../Ooui.Wasm.Build.Tasks/Ooui.Wasm.Build.Tasks.csproj
nuget pack

View File

@ -0,0 +1,25 @@
<?xml version="1.0"?>
<package >
<metadata>
<id>Ooui.Wasm</id>
<version>1.0.0</version>
<title>Ooui.Wasm</title>
<authors>praeclarum</authors>
<owners>praeclarum</owners>
<licenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</licenseUrl>
<projectUrl>https://github.com/praeclarum/Ooui</projectUrl>
<iconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>WebAssembly support for Ooui apps</description>
<tags>Ooui UI CrossPlatform WebAssembly Wasm</tags>
<dependencies>
<group>
<dependency id="Ooui" version="1.0.0" />
</group>
</dependencies>
</metadata>
<files>
<file src="Ooui.Wasm.targets" target="build/netstandard2.0/Ooui.Wasm.targets" />
<file src="../Ooui.Wasm.Build.Tasks/bin/Release/netstandard2.0/Ooui.Wasm.Build.Tasks.dll" target="build/netstandard2.0/Ooui.Wasm.Build.Tasks.dll" />
</files>
</package>

View File

@ -0,0 +1,23 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<UsingTask TaskName="Ooui.Wasm.Build.Tasks.BuildDistTask" AssemblyFile="$(MSBuildThisFileDirectory)Ooui.Wasm.Build.Tasks.dll" />
<!-- BuildDist -->
<PropertyGroup>
<CompileDependsOn>
$(CompileDependsOn);
BuildDist;
</CompileDependsOn>
</PropertyGroup>
<Target Name="BuildDist" AfterTargets="AfterBuild" Condition="'$(_BuildDistAlreadyExecuted)'!='true'">
<PropertyGroup>
<_BuildDistAlreadyExecuted>true</_BuildDistAlreadyExecuted>
</PropertyGroup>
<BuildDistTask
Assembly = "$(IntermediateOutputPath)$(TargetFileName)"
OutputPath = "$(OutputPath)"
ReferencePath = "@(ReferencePath)" />
</Target>
</Project>

View File

@ -2,22 +2,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 15.0.26124.0
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui", "Ooui\Ooui.csproj", "{DFDFD036-BF48-4D3A-BF99-88CA1EA8E4B9}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui", "Ooui\Ooui.csproj", "{DFDFD036-BF48-4D3A-BF99-88CA1EA8E4B9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{CDF8BB01-40BB-402F-8446-47AA6F1628F3}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{CDF8BB01-40BB-402F-8446-47AA6F1628F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{78F6E9E7-4322-4F87-8CE9-1EEF1B16D268}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{78F6E9E7-4322-4F87-8CE9-1EEF1B16D268}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui.Forms", "Ooui.Forms\Ooui.Forms.csproj", "{DB819A2F-91E1-40FB-8D48-6544169966B8}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.Forms", "Ooui.Forms\Ooui.Forms.csproj", "{DB819A2F-91E1-40FB-8D48-6544169966B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui.AspNetCore", "Ooui.AspNetCore\Ooui.AspNetCore.csproj", "{2EDF0328-698B-458A-B10C-AB1B4786A6CA}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.AspNetCore", "Ooui.AspNetCore\Ooui.AspNetCore.csproj", "{2EDF0328-698B-458A-B10C-AB1B4786A6CA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlatformSamples", "PlatformSamples", "{12ADF328-BBA8-48FC-9AF1-F11B7921D9EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreMvc", "PlatformSamples\AspNetCoreMvc\AspNetCoreMvc.csproj", "{7C6D477C-3378-4A86-9C31-AAD51204120B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreMvc", "PlatformSamples\AspNetCoreMvc\AspNetCoreMvc.csproj", "{7C6D477C-3378-4A86-9C31-AAD51204120B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OouiTemplates", "Templates\OouiTemplates\OouiTemplates.csproj", "{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.Wasm.Build.Tasks", "Ooui.Wasm.Build.Tasks\Ooui.Wasm.Build.Tasks.csproj", "{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -112,6 +114,18 @@ Global
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x64.Build.0 = Release|Any CPU
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.ActiveCfg = Release|Any CPU
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.Build.0 = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x64.ActiveCfg = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x64.Build.0 = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x86.ActiveCfg = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x86.Build.0 = Debug|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|Any CPU.Build.0 = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x64.ActiveCfg = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x64.Build.0 = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x86.ActiveCfg = Release|Any CPU
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -7,7 +7,6 @@ namespace Ooui
{
public class Button : FormControl
{
ButtonType typ = ButtonType.Submit;
public ButtonType Type {
get => GetAttribute ("type", ButtonType.Submit);
set => SetAttributeProperty ("type", value);

View File

@ -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)):
case MessageType.Call:
if (message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0))) {
AddStateMessage (message);
}
break;
}
}

View File

@ -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,
@ -94,17 +105,26 @@ function ooui (rootElementPath) {
console.log("Web socket created");
// Throttled window resize event
(function() {
window.addEventListener("resize", resizeThrottler, false);
monitorSizeChanges (1000/10);
}
function oouiWasm (mainAsmName, mainNamespace, mainClassName, mainMethodName, assemblies)
{
Module.entryPoint = { "a": mainAsmName, "n": mainNamespace, "t": mainClassName, "m": mainMethodName };
Module.assemblies = assemblies;
monitorSizeChanges (1000/30);
}
function monitorSizeChanges (millis)
{
var resizeTimeout;
function resizeThrottler() {
if (!resizeTimeout) {
resizeTimeout = setTimeout(function() {
resizeTimeout = null;
resizeHandler();
}, 100);
}, millis);
}
}
@ -117,11 +137,11 @@ 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);
}
}());
window.addEventListener("resize", resizeThrottler, false);
}
function getNode (id) {
@ -244,8 +264,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 ();
@ -294,3 +313,167 @@ function fixupValue (v) {
}
return v;
}
// == WASM Support ==
window["__oouiReceiveMessages"] = function (sessionId, messages)
{
if (debug) console.log ("WebAssembly Receive", messages);
if (wasmSession != null) {
messages.forEach (function (m) {
// console.log ('Raw value from server', m.v);
m.v = fixupValue (m.v);
processMessage (m);
});
}
};
var Module = {
onRuntimeInitialized: function () {
if (debug) console.log ("Done with WASM module instantiation.");
Module.FS_createPath ("/", "managed", true, true);
var pending = 0;
this.assemblies.forEach (function(asm_name) {
if (debug) 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 () {
if (debug) 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);
if (debug) 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.findMethods ();
this.runApp ("1", "2");
this.loading.hidden = true;
},
runApp: function (a, b) {
try {
var sessionId = "main";
if (!!this.ooui_DisableServer_method) {
MonoRuntime.call_method (this.ooui_DisableServer_method, null, []);
}
MonoRuntime.call_method (this.main_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]);
wasmSession = sessionId;
if (!!this.ooui_StartWebAssemblySession_method) {
var initialSize = getSize ();
MonoRuntime.call_method (this.ooui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (""), MonoRuntime.mono_string (Math.round(initialSize.width) + " " + Math.round(initialSize.height))]);
}
} catch (e) {
console.error(e);
}
},
receiveMessagesJson: function (sessionId, json) {
if (!!this.ooui_ReceiveWebAssemblySessionMessageJson_method) {
MonoRuntime.call_method (this.ooui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (json)]);
}
},
findMethods: function () {
this.main_module = MonoRuntime.assembly_load (Module.entryPoint.a);
if (!this.main_module)
throw "Could not find Main Module " + Module.entryPoint.a + ".dll";
this.main_class = MonoRuntime.find_class (this.main_module, Module.entryPoint.n, Module.entryPoint.t)
if (!this.main_class)
throw "Could not find Program class in main module";
this.main_method = MonoRuntime.find_method (this.main_class, Module.entryPoint.m, -1)
if (!this.main_method)
throw "Could not find Main method";
this.ooui_module = MonoRuntime.assembly_load ("Ooui");
if (!!this.ooui_module) {
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_DisableServer_method = MonoRuntime.find_method (this.ooui_class, "DisableServer", -1);
if (!this.ooui_DisableServer_method)
throw "Could not find DisableServer 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.ooui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method (this.ooui_class, "ReceiveWebAssemblySessionMessageJson", -1);
if (!this.ooui_ReceiveWebAssemblySessionMessageJson_method)
throw "Could not find ReceiveWebAssemblySessionMessageJson method";
}
},
};

View File

@ -20,7 +20,6 @@ namespace Ooui
set => SetAttributeProperty ("title", value);
}
bool hidden = false;
public bool IsHidden {
get => GetBooleanAttribute ("hidden");
set => SetBooleanAttributeProperty ("hidden", value);
@ -232,6 +231,8 @@ namespace Ooui
protected virtual bool HtmlNeedsFullEndElement => false;
#if !NO_XML
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteStartElement (TagName);
@ -262,5 +263,7 @@ namespace Ooui
c.WriteOuterHtml (w);
}
}
#endif
}
}

View File

@ -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<Message> StateMessages {
get {
lock (stateMessages) {
return new List<Message> (stateMessages).AsReadOnly ();
return new ReadOnlyList<Message> (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 ());
}
}

View File

@ -4,7 +4,6 @@ namespace Ooui
{
public class Form : Element
{
string action = "";
public string Action {
get => GetStringAttribute ("action", "");
set => SetAttributeProperty ("action", value ?? "");

View File

@ -9,7 +9,6 @@ namespace Ooui
set => SetAttributeProperty ("name", value);
}
bool isDisabled = false;
public bool IsDisabled {
get => GetBooleanAttribute ("disabled");
set => SetBooleanAttributeProperty ("disabled", value);

161
Ooui/JsonConvert.cs Normal file
View File

@ -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));
}
w.Write (Newtonsoft.Json.JsonConvert.SerializeObject (value));
}
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);
}
}
}

View File

@ -13,5 +13,12 @@ namespace Ooui
: base ("label")
{
}
public Label (string text)
: this ()
{
Text = text;
}
}
}

View File

@ -35,6 +35,105 @@ namespace Ooui
Key = eventType,
Value = value,
};
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\":");
JsonConvert.WriteJsonValue (w, Value);
w.Write ('}');
}
else {
w.Write ("\"}");
}
}
public string ToJson ()
{
using (var sw = new System.IO.StringWriter ()) {
WriteJson (sw);
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))]

View File

@ -1,6 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Ooui
{
@ -11,7 +11,7 @@ namespace Ooui
public IReadOnlyList<Node> Children {
get {
lock (children) {
return new List<Node> (children).AsReadOnly ();
return new ReadOnlyList<Node> (children);
}
}
}
@ -27,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 ?? ""));
}
@ -84,7 +90,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;
@ -123,6 +129,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);
@ -131,7 +138,7 @@ namespace Ooui
UpdateStateMessages (state => {
var mchild = ma.GetValue (0);
Node nextChild = null;
for (var i = 0; i < state.Count; ) {
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
@ -149,9 +156,9 @@ namespace Ooui
}
});
break;
default:
}
if (!handled) {
base.SaveStateMessageIfNeeded (message);
break;
}
return true;
}
@ -181,6 +188,8 @@ namespace Ooui
return false;
}
#if !NO_XML
public virtual string OuterHtml {
get {
using (var stream = new System.IO.MemoryStream ()) {
@ -199,5 +208,31 @@ namespace Ooui
}
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
#endif
}
class ReadOnlyList<T> : IReadOnlyList<T>
{
readonly List<T> list;
public ReadOnlyList (List<T> items)
{
list = new List<T> (items);
}
T IReadOnlyList<T>.this[int index] => list[index];
int IReadOnlyCollection<T>.Count => list.Count;
IEnumerator<T> IEnumerable<T>.GetEnumerator ()
{
return ((IEnumerable<T>)list).GetEnumerator ();
}
IEnumerator IEnumerable.GetEnumerator ()
{
return ((IEnumerable)list).GetEnumerator ();
}
}
}

View File

@ -8,12 +8,15 @@
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Client.js" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,7 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Linq;
namespace Ooui
{
@ -62,10 +62,10 @@ namespace Ooui
}
if (presenterWebView == null) {
throw new ArgumentException ("Presenter must be a WebView", nameof(presenter));
throw new ArgumentException ("Presenter must be a WebView", nameof (presenter));
}
var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public|BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof(string) }, null);
var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof (string) }, null);
m.Invoke (presenterWebView, new object[] { url });
}
@ -130,7 +130,7 @@ namespace Ooui
return t;
}
static Process StartBrowserProcess (string url)
static void StartBrowserProcess (string url)
{
// var vs = Environment.GetEnvironmentVariables ();
// foreach (System.Collections.DictionaryEntry kv in vs) {
@ -139,9 +139,12 @@ 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 });
}
}
}
}

View File

@ -0,0 +1,8 @@
using System;
namespace Ooui
{
class PreserveAttribute : Attribute
{
}
}

95
Ooui/Session.cs Normal file
View File

@ -0,0 +1,95 @@
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<string> createdIds;
protected readonly List<Message> queuedMessages = new List<Message> ();
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<string> {
"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);
}
}
}
protected 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)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine ("{0}: {1}", message, ex);
Console.ResetColor ();
}
protected void Info (string message)
{
Console.WriteLine (message);
}
}
}

View File

@ -412,8 +412,17 @@ 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 (val is IConvertible c)
return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
return val.ToString ();
}
@ -431,6 +440,13 @@ 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 (v is IConvertible c)
return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture);

View File

@ -25,7 +25,6 @@ namespace Ooui
set => SetAttributeProperty ("rows", value);
}
int cols = 20;
public int Columns {
get => GetAttribute ("cols", 20);
set => SetAttributeProperty ("cols", value);
@ -55,9 +54,13 @@ namespace Ooui
return base.TriggerEventFromMessage (message);
}
#if !NO_XML
public override void WriteInnerHtml (System.Xml.XmlWriter w)
{
w.WriteString (val ?? "");
}
#endif
}
}

View File

@ -21,9 +21,13 @@ namespace Ooui
Text = text;
}
#if !NO_XML
public override void WriteOuterHtml (System.Xml.XmlWriter w)
{
w.WriteString (text);
}
#endif
}
}

View File

@ -6,52 +6,30 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net;
using System.Net.WebSockets;
using System.Runtime.InteropServices;
namespace Ooui
{
public static class UI
{
static readonly ManualResetEvent started = new ManualResetEvent (false);
public const int MaxFps = 30;
[ThreadStatic]
static System.Security.Cryptography.SHA256 sha256;
static readonly ManualResetEvent started = new ManualResetEvent (false);
static CancellationTokenSource serverCts;
static readonly Dictionary<string, RequestHandler> publishedPaths =
new Dictionary<string, RequestHandler> ();
static readonly Dictionary<string, Style> styles =
new Dictionary<string, Style> ();
static readonly StyleSelectors rules = new StyleSelectors ();
public static StyleSelectors Styles => rules;
static readonly byte[] clientJsBytes;
static readonly string clientJsEtag;
public static byte[] ClientJsBytes => clientJsBytes;
public static string ClientJsEtag => clientJsEtag;
public static string Template { get; set; } = $@"<!DOCTYPE html>
<html>
<head>
<title>@Title</title>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
<style>@Styles</style>
</head>
<body>
<div id=""ooui-body"" class=""container-fluid"">
@InitialHtml
</div>
<script src=""/ooui.js""></script>
<script>ooui(""@WebSocketPath"");</script>
</body>
</html>";
public static string HeadHtml { get; set; } = @"<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />";
public static string BodyHeaderHtml { get; set; } = @"";
public static string BodyFooterHtml { get; set; } = @"";
static string host = "*";
public static string Host {
@ -87,6 +65,12 @@ namespace Ooui
}
}
[Preserve]
static void DisableServer ()
{
ServerEnabled = false;
}
static UI ()
{
var asm = typeof(UI).Assembly;
@ -101,27 +85,12 @@ 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)
{
Console.WriteLine ($"PUBLISH {path} {handler}");
//Console.WriteLine ($"PUBLISH {path} {handler}");
lock (publishedPaths) publishedPaths[path] = handler;
Start ();
}
@ -148,13 +117,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));
}
@ -199,7 +168,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));
}
@ -392,7 +361,45 @@ 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 ();
}
}
static string EscapeHtml (string text)
{
return text.Replace ("&", "&amp;").Replace ("<", "&lt;");
}
public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml)
{
writer.Write (@"<!DOCTYPE html>
<html>
<head>
<title>");
writer.Write (EscapeHtml (title));
writer.Write (@"</title>
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
");
writer.WriteLine (HeadHtml);
writer.WriteLine (@" <style>");
writer.WriteLine (rules.ToString ());
writer.WriteLine (@" </style>
</head>
<body>");
writer.WriteLine (BodyHeaderHtml);
writer.WriteLine (@"<div id=""ooui-body"" class=""container-fluid"">");
writer.WriteLine (initialHtml);
writer.Write (@"</div>
<script src=""/ooui.js""></script>
<script>ooui(""");
writer.Write (webSocketPath);
writer.WriteLine (@""");</script>");
writer.WriteLine (BodyFooterHtml);
writer.WriteLine (@"</body>
</html>");
}
class DataHandler : RequestHandler
@ -450,8 +457,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)
@ -521,8 +529,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;
@ -560,10 +568,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) {
@ -581,215 +589,50 @@ namespace Ooui
Console.ResetColor ();
}
public class Session
static readonly Dictionary<string, WebAssemblySession> globalElementSessions = new Dictionary<string, WebAssemblySession> ();
[Preserve]
public static void StartWebAssemblySession (string sessionId, string elementPath, string initialSize)
{
readonly WebSocket webSocket;
readonly Element element;
readonly Action<Message> handleElementMessageSent;
readonly CancellationTokenSource sessionCts = new CancellationTokenSource ();
readonly CancellationTokenSource linkedCts;
readonly CancellationToken token;
readonly HashSet<string> createdIds;
readonly List<Message> queuedMessages = new List<Message> ();
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<string> {
"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 ();
Element element;
RequestHandler handler;
lock (publishedPaths) {
publishedPaths.TryGetValue (elementPath, out handler);
}
};
}
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<byte>(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 ();
if (handler is ElementHandler eh) {
element = eh.GetElement ();
}
else {
var size = receiveResult.Count;
while (!receiveResult.EndOfMessage) {
if (size >= receiveBuffer.Length) {
await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false);
element = new Div ();
}
var ops = initialSize.Split (' ');
var initialWidth = double.Parse (ops[0]);
var initialHeight = double.Parse (ops[1]);
var g = new WebAssemblySession (sessionId, element, initialWidth, initialHeight);
lock (globalElementSessions) {
globalElementSessions[sessionId] = g;
}
g.StartSession ();
}
[Preserve]
public static void ReceiveWebAssemblySessionMessageJson (string sessionId, string json)
{
WebAssemblySession g;
lock (globalElementSessions) {
if (!globalElementSessions.TryGetValue (sessionId, out g))
return;
}
receiveResult = await webSocket.ReceiveAsync (new ArraySegment<byte>(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<Message> (receivedString);
element.Receive (message);
}
catch (Exception ex) {
Error ("Failed to process received message", ex);
}
}
}
}
finally {
element.MessageSent -= handleElementMessageSent;
}
g.ReceiveMessageJson (json);
}
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);
}
}
}
static readonly Dictionary<string, Style> styles =
new Dictionary<string, Style> ();
static readonly StyleSelectors rules = new StyleSelectors ();
//
// 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<Message> ();
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<byte> (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 ();
}
}
}
public static StyleSelectors Styles => rules;
public class StyleSelectors
{

26
Ooui/Utilities.cs Normal file
View File

@ -0,0 +1,26 @@
using System;
using System.Text;
namespace Ooui
{
public static class Utilities
{
[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 ();
}
}
}

107
Ooui/WebAssemblySession.cs Normal file
View File

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Ooui
{
public class WebAssemblySession : Session
{
readonly string id;
readonly Action<Message> handleElementMessageSent;
public WebAssemblySession (string id, Element element, double initialWidth, double initialHeight)
: base (element, initialWidth, initialHeight)
{
this.id = id;
handleElementMessageSent = QueueMessage;
}
protected override void QueueMessage (Message message)
{
lock (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<Message> messagesToSend, int startIndex, int max)
{
if (messagesToSend.Count == 0)
return;
//
// Now actually send the messages
//
var sb = new System.IO.StringWriter ();
sb.Write ("__oouiReceiveMessages(\"");
sb.Write (id);
sb.Write ("\",");
sb.Write ("[");
var head = "";
int n = 0;
for (var i = startIndex; i < messagesToSend.Count && n < max; i++, n++) {
sb.Write (head);
messagesToSend[i].WriteJson (sb);
head = ",";
}
sb.Write ("])");
var jsonp = sb.ToString ();
// WebAssembly.Runtime.InvokeJS("console.log('TRANSMIT',"+n+")");
WebAssembly.Runtime.InvokeJS (jsonp);
}
public void ReceiveMessageJson (string json)
{
try {
var message = Newtonsoft.Json.JsonConvert.DeserializeObject<Message> (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;
}
}
}
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 _);
}
}
}

157
Ooui/WebSocketSession.cs Normal file
View File

@ -0,0 +1,157 @@
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<Message> 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<byte> (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<byte> (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<Message> (receivedString);
element.Receive (message);
}
catch (Exception ex) {
Error ("Failed to process received message", ex);
}
}
}
}
finally {
element.MessageSent -= handleElementMessageSent;
}
}
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<Message> ();
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<byte> (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 ();
}
}
}
}

149
README.md
View File

@ -2,11 +2,12 @@
| 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 |
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Wasm.svg)](https://www.nuget.org/packages/Ooui.Wasm) | [Ooui.Wasm](https://www.nuget.org/packages/Ooui.Wasm) | Package your app into a web assembly |
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.
@ -15,6 +16,8 @@ It presents a classic object-oriented UI API that controls a dumb browser. With
Head on over to [http://ooui.mecha.parts](http://ooui.mecha.parts) to tryout the samples.
You can also load [https://s3.amazonaws.com/praeclarum.org/wasm/ooui-sample.html](https://s3.amazonaws.com/praeclarum.org/wasm/ooui-sample.html) to try the WebAssembly mode of Ooui running Xamarin.Forms. (That's Xamarin.Forms running right in your browser!)
## Try the Samples Locally
@ -31,11 +34,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.
@ -58,7 +60,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
@ -67,119 +69,60 @@ 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.
<table>
<thead><tr><th>Ooui</th><th>Ooui.AspNetCore</th><th>Ooui.Forms</th><th>Ooui.Wasm</th><th></th></tr></thead>
<tr>
<td>&check;</td><td></td><td></td><td></td><td><a href="https://github.com/praeclarum/Ooui/wiki/Write-the-UI-using-the-web-DOM-and-use-the-built-in-web-server">Web DOM with the built-in web server</a></td>
</tr>
<tr>
<td>&check;</td><td>&check;</td><td></td><td></td><td>Web DOM with ASP.NET Core</td>
</tr>
<tr>
<td>&check;</td><td>&check;</td><td>&check;</td><td></td><td>Xamarin.Forms with ASP.NET Core</td>
</tr>
<tr>
<td>&check;</td><td></td><td>&check;</td><td></td><td>Xamarin.Forms with the built-in web server</td>
</tr>
<tr>
<td>&check;</td><td></td><td></td><td>&check;</td><td>Web DOM with Web Assembly</td>
</tr>
<tr>
<td>&check;</td><td></td><td>&check;</td><td>&check;</td><td>Xamarin.Forms with Web Assembly</td>
</tr>
</table>
## 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
<table>
<thead><tr><th>UI Library</th><th>Ooui</th><th>Xamarin.Forms</th><th>ASP.NET MVC</th></tr></thead>
<tr>
<th>How big is it?</th>
<td>80 KB</td>
<td>850 KB</td>
<td>1,300 KB</td>
</tr>
<tr>
<th>Where does it run?</th>
<td>Everywhere</td>
<td>iOS, Android, Mac, Windows</td>
<td>Everywhere</td>
</tr>
<tr>
<th>How do I make a button?</th>
<td><pre>new Button()</pre></td>
<td><pre>new Button()</pre></td>
<td><pre>&lt;button /&gt;</pre></td>
</tr>
<tr>
<th>Does it use native controls?</th>
<td>No, HTML5 controls</td>
<td>Yes!</td>
<td>HTML5 controls</td>
</tr>
<tr>
<th>What controls are available?</th>
<td>All of those in HTML5</td>
<td>Xamarin.Forms controls</td>
<td>All of those in HTML5</td>
</tr>
<tr>
<th>Which architecture will you force me to use?</th>
<td>None, you're free</td>
<td>MVVM</td>
<td>MVC/MVVM</td>
</tr>
<tr>
<th>What's the templating language?</th>
<td>C#</td>
<td>XAML</td>
<td>Razor</td>
</tr>
<tr>
<th>How do I style things?</th>
<td>CSS baby!</td>
<td>XAML resources</td>
<td>CSS</td>
</tr>
<tr>
<th>Do I need to run a server?</th>
<td>Nope</td>
<td>Heck no</td>
<td>Yes</td>
</tr>
<tr>
<th>Is it web scale?</th>
<td>Yes?</td>
<td>What's the web?</td>
<td>Yes!</td>
</tr>
In the case of web assembly, this same dataflow takes place. However, sockets are not used as all communication is done locally in the browser process.
## Contributing
</table>
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!

View File

@ -32,9 +32,6 @@
<Compile Update="DisplayAlertPage.xaml.cs">
<DependentUpon>DisplayAlertPage.xaml</DependentUpon>
</Compile>
<Compile Update="XamlPreviewPage.xaml.cs">
<DependentUpon>XamlPreviewPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
@ -56,9 +53,6 @@
<EmbeddedResource Update="WeatherApp\WeatherPage.xaml">
<Generator>MSBuild:Compile</Generator>
</EmbeddedResource>
<EmbeddedResource Update="XamlPreviewPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Samples.XamlPreviewPage">
<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackLayout Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<Label Text="Xamarin.Forms XAML Editor" FontSize="24" FontAttributes="Bold" Margin="8,8,8,0" />
<Label Text="Edit the XAML below to see a live preview on the right" Margin="8,0,8,8" />
</StackLayout>
<Editor x:Name="editor" FontFamily="monospace" FontSize="12" Grid.Row="1" Grid.Column="0" />
<ContentView x:Name="results" Grid.Row="1" Grid.Column="1" BackgroundColor="White" />
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@ -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 = @"<ContentView
xmlns=""http://xamarin.com/schemas/2014/forms""
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml"">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=""*"" />
<RowDefinition Height=""*"" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=""*"" />
<ColumnDefinition Width=""*"" />
</Grid.ColumnDefinitions>
<StackLayout Grid.Row=""0"" Grid.Column=""0"">
<Label Text=""Top Left"" />
<Entry Placeholder=""I'm ready for some text"" />
<Button Text=""I'm a button, but I don't do anything"" />
</StackLayout>
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#c5000b"" />
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" TextColor=""Black"" BackgroundColor=""#ffd320"" />
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#008000"" />
</Grid>
</ContentView>";
editor.TextChanged += (sender, e) => DisplayXaml ();
DisplayXaml ();
}
CancellationTokenSource lastCts = null;
public void DisplayXaml ()
{
try {
var cts = new CancellationTokenSource ();
var token = cts.Token;
lastCts?.Cancel ();
lastCts = cts;
var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly;
var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader");
var loadArgTypes = new[] { typeof (object), typeof (string) };
var loadMethod = xamlLoaderType.GetMethod ("Load", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, System.Reflection.CallingConventions.Any, loadArgTypes, null);
var contentView = new ContentView ();
loadMethod.Invoke (null, new object[] { contentView, editor.Text });
if (!token.IsCancellationRequested) {
results.Content = contentView;
}
}
catch (OperationCanceledException) {
}
catch (Exception ex) {
results.Content = new Label {
TextColor = Color.DarkRed,
FontSize = 12,
Text = ex.ToString (),
};
}
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Threading;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Samples
{
@ -10,8 +11,108 @@ namespace Samples
public Ooui.Element CreateElement ()
{
var page = new XamlPreviewPage ();
var page = new XamlEditorPage ();
return page.GetOouiElement ();
}
}
public partial class XamlEditorPage : ContentPage
{
Editor editor;
ContentView results;
public XamlEditorPage ()
{
InitializeComponent ();
editor.Text = @"<ContentView
xmlns=""http://xamarin.com/schemas/2014/forms""
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml"">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=""*"" />
<RowDefinition Height=""*"" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=""*"" />
<ColumnDefinition Width=""*"" />
</Grid.ColumnDefinitions>
<StackLayout Grid.Row=""0"" Grid.Column=""0"">
<Label Text=""Top Left"" />
<Entry Placeholder=""I'm ready for some text"" />
<Button Text=""I'm a button, but I don't do anything"" />
</StackLayout>
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#c5000b"" />
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" TextColor=""Black"" BackgroundColor=""#ffd320"" />
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#008000"" />
</Grid>
</ContentView>";
editor.TextChanged += (sender, e) => DisplayXaml ();
DisplayXaml ();
}
void InitializeComponent ()
{
var grid = new Grid ();
grid.RowDefinitions.Add (new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add (new RowDefinition { Height = GridLength.Star });
grid.ColumnDefinitions.Add (new ColumnDefinition { Width = GridLength.Star });
grid.ColumnDefinitions.Add (new ColumnDefinition { Width = GridLength.Star });
editor = new Editor {
FontSize = 12,
FontFamily = "monospace",
};
editor.SetValue (Grid.ColumnProperty, 0);
editor.SetValue (Grid.RowProperty, 1);
results = new ContentView ();
results.SetValue (Grid.ColumnProperty, 1);
results.SetValue (Grid.RowProperty, 1);
var title = new Label {
Text = "XAML Editor",
FontSize = 24,
FontAttributes = FontAttributes.Bold,
Margin = new Thickness (8),
};
title.SetValue (Grid.ColumnProperty, 0);
title.SetValue (Grid.RowProperty, 0);
grid.Children.Add (title);
grid.Children.Add (editor);
grid.Children.Add (results);
Content = grid;
}
CancellationTokenSource lastCts = null;
public void DisplayXaml ()
{
try {
var cts = new CancellationTokenSource ();
var token = cts.Token;
lastCts?.Cancel ();
lastCts = cts;
var contentView = new ContentView ();
contentView.LoadFromXaml (editor.Text);
if (!token.IsCancellationRequested) {
results.Content = contentView;
}
}
catch (OperationCanceledException) {
}
catch (Exception ex) {
results.Content = new Label {
TextColor = Color.DarkRed,
FontSize = 12,
Text = ex.ToString (),
};
}
}
}
}

54
Tests/JsonTests.cs Normal file
View File

@ -0,0 +1,54 @@
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) => { };
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]
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 ()));
}
}
}