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 # Social media files
/Media /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; var response = context.HttpContext.Response;
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "text/html; charset=utf-8"; response.ContentType = "text/html; charset=utf-8";
response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
if (element.WantsFullScreen) { if (element.WantsFullScreen) {
element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000); 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 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 = static readonly ConcurrentDictionary<string, PendingSession> pendingSessions =
new ConcurrentDictionary<string, ActiveSession> (); new ConcurrentDictionary<string, PendingSession> ();
public static string BeginSession (HttpContext context, Element element) public static string BeginSession (HttpContext context, Element element)
{ {
var id = Guid.NewGuid ().ToString ("N"); var id = Guid.NewGuid ().ToString ("N");
var s = new ActiveSession { var s = new PendingSession {
Element = element, 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"); throw new Exception ("Failed to schedule pending session");
} }
@ -62,19 +62,18 @@ namespace Ooui.AspNetCore
// //
// Clear old sessions // 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) { foreach (var c in toClear) {
activeSessions.TryRemove (c.Key, out var _); pendingSessions.TryRemove (c.Key, out var _);
} }
// //
// Find the pending session // Find the pending session
// //
if (!activeSessions.TryGetValue (id, out var activeSession)) { if (!pendingSessions.TryRemove (id, out var activeSession)) {
BadRequest ("Unknown `id`"); BadRequest ("Unknown `id`");
return; return;
} }
activeSession.LastConnectTimeUtc = DateTime.UtcNow;
// //
// Set the element's dimensions // Set the element's dimensions
@ -98,14 +97,14 @@ namespace Ooui.AspNetCore
// //
var token = CancellationToken.None; var token = CancellationToken.None;
var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui"); 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); await session.RunAsync ().ConfigureAwait (false);
} }
class ActiveSession class PendingSession
{ {
public Element Element; public Element Element;
public DateTime LastConnectTimeUtc; public DateTime CreateTimeUtc;
} }
} }
} }

View File

@ -1,4 +1,4 @@
using System.Web; using System;
using Xamarin.Forms.Internals; using Xamarin.Forms.Internals;
namespace Ooui.Forms namespace Ooui.Forms
@ -31,7 +31,7 @@ namespace Ooui.Forms
ClassName = "close" ClassName = "close"
}; };
_closeButton.AppendChild(new Span(HttpUtility.HtmlDecode("&times;"))); _closeButton.AppendChild(new Span("×"));
var h4 = new Heading(4) var h4 = new Heading(4)
{ {

View File

@ -21,7 +21,7 @@ namespace Xamarin.Forms
return; return;
IsInitialized = true; 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.SetIdiom (TargetIdiom.Desktop);
Device.PlatformServices = new OouiPlatformServices (); Device.PlatformServices = new OouiPlatformServices ();
@ -133,7 +133,7 @@ namespace Xamarin.Forms
{ {
if (timer != null) if (timer != null)
return; return;
var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.Session.MaxFps); var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.MaxFps);
timer = new Timer ((_ => { timer = new Timer ((_ => {
this.SendSignals (); this.SendSignals ();
}), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds); }), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds);

View File

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

View File

@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Ooui.Forms.Renderers; using Ooui.Forms.Renderers;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.Internals; using Xamarin.Forms.Internals;
using System.Web;
namespace Ooui.Forms namespace Ooui.Forms
{ {
@ -142,7 +141,7 @@ namespace Ooui.Forms
void AddChild (VisualElement view) void AddChild (VisualElement view)
{ {
if (!Application.IsApplicationOrNull (view.RealParent)) 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) { if (GetRenderer (view) == null) {
var viewRenderer = CreateRenderer (view); var viewRenderer = CreateRenderer (view);
@ -152,7 +151,7 @@ namespace Ooui.Forms
viewRenderer.SetElementSize (new Size (640, 480)); viewRenderer.SetElementSize (new Size (640, 480));
} }
else 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) void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)

View File

@ -139,6 +139,7 @@ namespace Ooui.Forms.Renderers
public sealed class FileImageSourceHandler : IImageSourceHandler public sealed class FileImageSourceHandler : IImageSourceHandler
{ {
#pragma warning disable 1998
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f) public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
{ {
string image = null; string image = null;
@ -171,7 +172,7 @@ namespace Ooui.Forms.Renderers
using (var outputStream = new System.IO.MemoryStream (data)) { using (var outputStream = new System.IO.MemoryStream (data)) {
await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false); await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false);
} }
var hash = Ooui.UI.Hash (data); var hash = Ooui.Utilities.Hash (data);
var etag = "\"" + hash + "\""; var etag = "\"" + hash + "\"";
image = "/images/" + hash; image = "/images/" + hash;
if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) { 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) protected override void Dispose(bool disposing)
{ {
UnsubscribeCellClicks();
base.Dispose(disposing); base.Dispose(disposing);
if (disposing && !_disposed) 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() private void UpdateItems()
{ {
UnsubscribeCellClicks();
_cells.Clear(); _cells.Clear();
var items = TemplatedItemsView.TemplatedItems; var items = TemplatedItemsView.TemplatedItems;
@ -82,6 +93,7 @@ namespace Ooui.Forms.Renderers
listItem.Style["list-style-type"] = "none"; listItem.Style["list-style-type"] = "none";
listItem.AppendChild(cell); listItem.AppendChild(cell);
listItem.Click += ListItem_Click;
_cells.Add(listItem); _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() private void UpdateBackgroundColor()
{ {
var backgroundColor = Element.BackgroundColor.ToOouiColor(); 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 # Visual Studio 15
VisualStudioVersion = 15.0.27130.2010 VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 15.0.26124.0 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 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 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 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 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlatformSamples", "PlatformSamples", "{12ADF328-BBA8-48FC-9AF1-F11B7921D9EA}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlatformSamples", "PlatformSamples", "{12ADF328-BBA8-48FC-9AF1-F11B7921D9EA}"
EndProject 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OouiTemplates", "Templates\OouiTemplates\OouiTemplates.csproj", "{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OouiTemplates", "Templates\OouiTemplates\OouiTemplates.csproj", "{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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|x64.Build.0 = Release|Any CPU
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

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

View File

@ -36,8 +36,10 @@ namespace Ooui
{ {
if (message.TargetId == Id) { if (message.TargetId == Id) {
switch (message.MessageType) { 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); AddStateMessage (message);
}
break; break;
} }
} }

View File

@ -6,6 +6,17 @@ const nodes = {};
const hasText = {}; const hasText = {};
let socket = null; 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 = { const mouseEvents = {
click: true, click: true,
@ -94,17 +105,26 @@ function ooui (rootElementPath) {
console.log("Web socket created"); console.log("Web socket created");
// Throttled window resize event monitorSizeChanges (1000/10);
(function() { }
window.addEventListener("resize", resizeThrottler, false);
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; var resizeTimeout;
function resizeThrottler() { function resizeThrottler() {
if (!resizeTimeout) { if (!resizeTimeout) {
resizeTimeout = setTimeout(function() { resizeTimeout = setTimeout(function() {
resizeTimeout = null; resizeTimeout = null;
resizeHandler(); resizeHandler();
}, 100); }, millis);
} }
} }
@ -117,11 +137,11 @@ function ooui (rootElementPath) {
}; };
saveSize (em.v); saveSize (em.v);
const ems = JSON.stringify (em); const ems = JSON.stringify (em);
if (socket != null) send (ems);
socket.send (ems);
if (debug) console.log ("Event", em); if (debug) console.log ("Event", em);
} }
}());
window.addEventListener("resize", resizeThrottler, false);
} }
function getNode (id) { function getNode (id) {
@ -244,8 +264,7 @@ function msgListen (m) {
}; };
} }
const ems = JSON.stringify (em); const ems = JSON.stringify (em);
if (socket != null) send (ems);
socket.send (ems);
if (debug) console.log ("Event", em); if (debug) console.log ("Event", em);
if (em.k === "submit") if (em.k === "submit")
e.preventDefault (); e.preventDefault ();
@ -294,3 +313,167 @@ function fixupValue (v) {
} }
return 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); set => SetAttributeProperty ("title", value);
} }
bool hidden = false;
public bool IsHidden { public bool IsHidden {
get => GetBooleanAttribute ("hidden"); get => GetBooleanAttribute ("hidden");
set => SetBooleanAttributeProperty ("hidden", value); set => SetBooleanAttributeProperty ("hidden", value);
@ -232,6 +231,8 @@ namespace Ooui
protected virtual bool HtmlNeedsFullEndElement => false; protected virtual bool HtmlNeedsFullEndElement => false;
#if !NO_XML
public override void WriteOuterHtml (System.Xml.XmlWriter w) public override void WriteOuterHtml (System.Xml.XmlWriter w)
{ {
w.WriteStartElement (TagName); w.WriteStartElement (TagName);
@ -262,5 +263,7 @@ namespace Ooui
c.WriteOuterHtml (w); c.WriteOuterHtml (w);
} }
} }
#endif
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection;
namespace Ooui namespace Ooui
{ {
@ -21,7 +22,7 @@ namespace Ooui
public IReadOnlyList<Message> StateMessages { public IReadOnlyList<Message> StateMessages {
get { get {
lock (stateMessages) { 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) 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 public class Form : Element
{ {
string action = "";
public string Action { public string Action {
get => GetStringAttribute ("action", ""); get => GetStringAttribute ("action", "");
set => SetAttributeProperty ("action", value ?? ""); set => SetAttributeProperty ("action", value ?? "");

View File

@ -9,7 +9,6 @@ namespace Ooui
set => SetAttributeProperty ("name", value); set => SetAttributeProperty ("name", value);
} }
bool isDisabled = false;
public bool IsDisabled { public bool IsDisabled {
get => GetBooleanAttribute ("disabled"); get => GetBooleanAttribute ("disabled");
set => SetBooleanAttributeProperty ("disabled", value); 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") : base ("label")
{ {
} }
public Label (string text)
: this ()
{
Text = text;
}
} }
} }

View File

@ -35,6 +35,105 @@ namespace Ooui
Key = eventType, Key = eventType,
Value = value, 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))] [JsonConverter (typeof (StringEnumConverter))]

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Ooui namespace Ooui
{ {
@ -11,7 +11,7 @@ namespace Ooui
public IReadOnlyList<Node> Children { public IReadOnlyList<Node> Children {
get { get {
lock (children) { lock (children) {
return new List<Node> (children).AsReadOnly (); return new ReadOnlyList<Node> (children);
} }
} }
} }
@ -27,7 +27,13 @@ namespace Ooui
} }
public virtual string Text { 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 { set {
ReplaceAll (new TextNode (value ?? "")); ReplaceAll (new TextNode (value ?? ""));
} }
@ -123,6 +129,7 @@ namespace Ooui
protected override bool SaveStateMessageIfNeeded (Message message) protected override bool SaveStateMessageIfNeeded (Message message)
{ {
if (message.TargetId == Id) { if (message.TargetId == Id) {
var handled = false;
switch (message.MessageType) { switch (message.MessageType) {
case MessageType.Call when message.Key == "insertBefore": case MessageType.Call when message.Key == "insertBefore":
AddStateMessage (message); AddStateMessage (message);
@ -149,9 +156,9 @@ namespace Ooui
} }
}); });
break; break;
default: }
if (!handled) {
base.SaveStateMessageIfNeeded (message); base.SaveStateMessageIfNeeded (message);
break;
} }
return true; return true;
} }
@ -181,6 +188,8 @@ namespace Ooui
return false; return false;
} }
#if !NO_XML
public virtual string OuterHtml { public virtual string OuterHtml {
get { get {
using (var stream = new System.IO.MemoryStream ()) { using (var stream = new System.IO.MemoryStream ()) {
@ -199,5 +208,31 @@ namespace Ooui
} }
public abstract void WriteOuterHtml (System.Xml.XmlWriter w); 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> <PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl> <RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
<TargetFramework>netstandard2.0</TargetFramework> <TargetFrameworks>netstandard2.0</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Client.js" /> <EmbeddedResource Include="Client.js" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Linq;
namespace Ooui namespace Ooui
{ {
@ -130,7 +130,7 @@ namespace Ooui
return t; return t;
} }
static Process StartBrowserProcess (string url) static void StartBrowserProcess (string url)
{ {
// var vs = Environment.GetEnvironmentVariables (); // var vs = Environment.GetEnvironmentVariables ();
// foreach (System.Collections.DictionaryEntry kv in vs) { // foreach (System.Collections.DictionaryEntry kv in vs) {
@ -139,9 +139,12 @@ namespace Ooui
// Console.WriteLine ($"Process.Start {cmd} {args}"); // Console.WriteLine ($"Process.Start {cmd} {args}");
return Environment.OSVersion.Platform == PlatformID.Unix if (Environment.OSVersion.Platform == PlatformID.Unix) {
? Process.Start ("open", url) Process.Start ("open", url);
: Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); }
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; return null;
if (val is string s) if (val is string s)
return 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) if (val is IConvertible c)
return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units; return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
return val.ToString (); return val.ToString ();
} }
@ -431,6 +440,13 @@ namespace Ooui
return num; 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) if (v is IConvertible c)
return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture); return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture);

View File

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

View File

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

View File

@ -6,52 +6,30 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Net; using System.Net;
using System.Net.WebSockets; using System.Runtime.InteropServices;
namespace Ooui namespace Ooui
{ {
public static class UI public static class UI
{ {
static readonly ManualResetEvent started = new ManualResetEvent (false); public const int MaxFps = 30;
[ThreadStatic] static readonly ManualResetEvent started = new ManualResetEvent (false);
static System.Security.Cryptography.SHA256 sha256;
static CancellationTokenSource serverCts; static CancellationTokenSource serverCts;
static readonly Dictionary<string, RequestHandler> publishedPaths = static readonly Dictionary<string, RequestHandler> publishedPaths =
new Dictionary<string, RequestHandler> (); 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 byte[] clientJsBytes;
static readonly string clientJsEtag; static readonly string clientJsEtag;
public static byte[] ClientJsBytes => clientJsBytes; public static byte[] ClientJsBytes => clientJsBytes;
public static string ClientJsEtag => clientJsEtag; public static string ClientJsEtag => clientJsEtag;
public static string Template { get; set; } = $@"<!DOCTYPE html> public static string HeadHtml { get; set; } = @"<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />";
<html> public static string BodyHeaderHtml { get; set; } = @"";
<head> public static string BodyFooterHtml { get; set; } = @"";
<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>";
static string host = "*"; static string host = "*";
public static string Host { public static string Host {
@ -87,6 +65,12 @@ namespace Ooui
} }
} }
[Preserve]
static void DisableServer ()
{
ServerEnabled = false;
}
static UI () static UI ()
{ {
var asm = typeof(UI).Assembly; var asm = typeof(UI).Assembly;
@ -101,27 +85,12 @@ namespace Ooui
clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ()); clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ());
} }
} }
clientJsEtag = "\"" + Hash (clientJsBytes) + "\""; clientJsEtag = "\"" + Utilities.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 ();
} }
static void Publish (string path, RequestHandler handler) static void Publish (string path, RequestHandler handler)
{ {
Console.WriteLine ($"PUBLISH {path} {handler}"); //Console.WriteLine ($"PUBLISH {path} {handler}");
lock (publishedPaths) publishedPaths[path] = handler; lock (publishedPaths) publishedPaths[path] = handler;
Start (); Start ();
} }
@ -148,13 +117,13 @@ namespace Ooui
if (contentType == null) { if (contentType == null) {
contentType = GuessContentType (path, filePath); contentType = GuessContentType (path, filePath);
} }
var etag = "\"" + Hash (data) + "\""; var etag = "\"" + Utilities.Hash (data) + "\"";
Publish (path, new DataHandler (data, etag, contentType)); Publish (path, new DataHandler (data, etag, contentType));
} }
public static void PublishFile (string path, byte[] data, string 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)); Publish (path, new DataHandler (data, etag, contentType));
} }
@ -199,7 +168,7 @@ namespace Ooui
public static void PublishJson (string path, object value) public static void PublishJson (string path, object value)
{ {
var data = JsonHandler.GetData (value); var data = JsonHandler.GetData (value);
var etag = "\"" + Hash (data) + "\""; var etag = "\"" + Utilities.Hash (data) + "\"";
Publish (path, new DataHandler (data, etag, JsonHandler.ContentType)); Publish (path, new DataHandler (data, etag, JsonHandler.ContentType));
} }
@ -392,7 +361,45 @@ namespace Ooui
public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "") 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 class DataHandler : RequestHandler
@ -450,8 +457,9 @@ namespace Ooui
public static byte[] GetData (object obj) public static byte[] GetData (object obj)
{ {
var r = Newtonsoft.Json.JsonConvert.SerializeObject (obj); var r = Ooui.JsonConvert.SerializeObject (obj);
return System.Text.Encoding.UTF8.GetBytes (r); var e = new UTF8Encoding (false);
return e.GetBytes (r);
} }
public override void Respond (HttpListenerContext listenerContext, CancellationToken token) public override void Respond (HttpListenerContext listenerContext, CancellationToken token)
@ -521,8 +529,8 @@ namespace Ooui
// //
// Connect the web socket // Connect the web socket
// //
WebSocketContext webSocketContext = null; System.Net.WebSockets.WebSocketContext webSocketContext = null;
WebSocket webSocket = null; System.Net.WebSockets.WebSocket webSocket = null;
try { try {
webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false); webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false);
webSocket = webSocketContext.WebSocket; webSocket = webSocketContext.WebSocket;
@ -560,10 +568,10 @@ namespace Ooui
// Create a new session and let it handle everything from here // Create a new session and let it handle everything from here
// //
try { try {
var session = new Session (webSocket, element, w, h, serverToken); var session = new WebSocketSession (webSocket, element, w, h, serverToken);
await session.RunAsync ().ConfigureAwait (false); 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. // The remote party closed the WebSocket connection without completing the close handshake.
} }
catch (Exception ex) { catch (Exception ex) {
@ -581,215 +589,50 @@ namespace Ooui
Console.ResetColor (); 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; Element element;
readonly Element element; RequestHandler handler;
readonly Action<Message> handleElementMessageSent; lock (publishedPaths) {
publishedPaths.TryGetValue (elementPath, out handler);
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 ();
} }
}; if (handler is ElementHandler eh) {
} element = eh.GetElement ();
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 { else {
var size = receiveResult.Count; element = new Div ();
while (!receiveResult.EndOfMessage) { }
if (size >= receiveBuffer.Length) {
await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false); 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; return;
} }
receiveResult = await webSocket.ReceiveAsync (new ArraySegment<byte>(receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false); g.ReceiveMessageJson (json);
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;
}
} }
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) static readonly Dictionary<string, Style> styles =
{ new Dictionary<string, Style> ();
// static readonly StyleSelectors rules = new StyleSelectors ();
// 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);
}
}
}
// public static StyleSelectors Styles => rules;
// 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 class StyleSelectors 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 | | 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.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. 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. 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 ## 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) 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. 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.
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. 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 // 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); UI.Publish ("/shared-button", button);
// Don't exit the app until someone hits return // 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 ```bash
dotnet add package Ooui 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 ## The Many Ways to Ooui
Button MakeButton()
{
var button = new Button("Click me!");
var count = 0;
button.Click += (s, e) => {
count++;
button.Text = $"Clicked {count} times";
};
return button;
}
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 ## 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. 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.
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.
## 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>
## 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"> <Compile Update="DisplayAlertPage.xaml.cs">
<DependentUpon>DisplayAlertPage.xaml</DependentUpon> <DependentUpon>DisplayAlertPage.xaml</DependentUpon>
</Compile> </Compile>
<Compile Update="XamlPreviewPage.xaml.cs">
<DependentUpon>XamlPreviewPage.xaml</DependentUpon>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -56,9 +53,6 @@
<EmbeddedResource Update="WeatherApp\WeatherPage.xaml"> <EmbeddedResource Update="WeatherApp\WeatherPage.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Update="XamlPreviewPage.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
</ItemGroup> </ItemGroup>
<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;
using System.Threading;
using Xamarin.Forms; using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Samples namespace Samples
{ {
@ -10,8 +11,108 @@ namespace Samples
public Ooui.Element CreateElement () public Ooui.Element CreateElement ()
{ {
var page = new XamlPreviewPage (); var page = new XamlEditorPage ();
return page.GetOouiElement (); 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 ()));
}
}
}