Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Eric Sink 2018-03-15 15:38:57 -05:00
commit f0180b7246
45 changed files with 2210 additions and 559 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

@ -0,0 +1,428 @@
# Ooui.Forms Status
### ActivityIndicator
The [ActivityIndicator](https://developer.xamarin.com/api/type/Xamarin.Forms.ActivityIndicator/) control gives a visual clue to the user that something is happening, without information about its progress.
Property | Status
------ | ------
BackgroundColor | Done
IsRunning | Done
Color | Pending
### ActionSheets
The UIActionSheet is a common UI element in iOS. The Xamarin.Forms [DisplayActionSheet](https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/pop-ups/) method lets you include this control in cross-platforms apps, rendering native alternatives.
Property | Status
------ | ------
Title | Pending
Cancel | Pending
Extras | Pending
Destruction | Pending
### BoxView
[BoxView](https://developer.xamarin.com/api/type/Xamarin.Forms.BoxView/) is a useful stand-in for images or custom elements when doing initial prototyping.
Property | Status
------ | ------
BackgroundColor | Done
Color | Done
### Button
A [button](https://developer.xamarin.com/api/type/Xamarin.Forms.Button/) View that reacts to touch events.
Property | Status
------ | ------
BackgroundColor | Done
IsEnabled | Done
Command | Done
CommandParameter | Done
ContentLayout | Done
Text | Done
TextColor | Done
Font | Done
FontFamily | Done
FontAttributes | Done
BorderWidth | Done
BorderColor | Done
BorderRadius | Done
Image | Pending
Clicked | Done
Pressed | Done
Released | Done
### CarouselPage
The Xamarin.Forms [CarouselPage](https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/carousel-page/) is a page that users can swipe from side to side to navigate through pages of content, like a gallery.
Property | Status
------ | ------
ItemsSource | Pending
ItemTemplate | Pending
CurrentPage | Pending
Children | Pending
BackgrondImage | Pending
Icon | Pending
IsBusy | Pending
Padding | Pending
Tittle | Pending
ToolbarItems | Pending
CurrentPageChanged | Pending
PagesChanged | Pending
LayoutChanged | Pending
Appearing | Pending
Disappearing | Pending
### ContentPage
A [ContentPage](https://developer.xamarin.com/api/type/Xamarin.Forms.ContentPage/) is a Page displaying a single View, often a container like a StackLayout or ScrollView.
Property | Status
------ | ------
BackgroundColor | Done
Appearing | Pending
Disappearing | Pending
DatePicker
The visual representation of a [DatePicker](https://developer.xamarin.com/api/type/Xamarin.Forms.DatePicker/) is very similar to the one of Entry, except that a special control for picking a date appears in place of a keyboard.
Property | Status
------ | ------
BackgroundColor | Done
Format | Done
Date | Done
MinimumDate | Pending
MaximumDate | Pending
TextColor | Pending
DateSelected | Pending
### Editor
The [Editor](https://developer.xamarin.com/guides/xamarin-forms/user-interface/text/editor/) control is used to accept multi-line input.
Property | Status
------ | ------
BackgroundColor | Done
IsEnabled | Done
Text | Done
FontFamily | Done
FontSize | Done
FontAttributes | Done
TextColor | Done
TextChanged | Done
Completed | Done
### Entry
[Entry](https://developer.xamarin.com/api/type/Xamarin.Forms.Entry/) is a single line text entry. It is best used for collecting small discrete pieces of information, like usernames and passwords.
Property | Status
------ | ------
BackgroundColor | Done
IsEnabled | Done
Placeholder | Done
PlaceholderColor | Pending
Text | Done
TextColor | Done
FontFamily | Done
FontSize | Done
FontAttributes | Done
TextColor | Done
TextChanged | Done
Completed | Done
### Frame
[Frame](https://developer.xamarin.com/api/type/Xamarin.Forms.Frame/) is an element containing a single child, with some framing options.
Property | Status
------ | ------
BackgroundColor | Done
OutlineColor | Done
HasShadow | Pending
### Image
[View](https://developer.xamarin.com/api/type/Xamarin.Forms.Image/) that holds an image.
Property | Status
------ | ------
BackgroundColor | Done
Source | Done
Aspect | Pending
IsOpaque | Pending
IsLoading | Done
Supported FileSource, StreamSource and UriSource.
### Label
A [Label](https://developer.xamarin.com/api/type/Xamarin.Forms.Label/) is used to display single-line text elements as well as multi-line blocks of text.
Property | Status
------ | ------
BackgroundColor | Done
IsEnabled | Done
FormattedText | Done
HorizontalTextAlignment | Done
LineBreakMode | Pending
Text | Done
TextColor | Done
VerticalTextAlignment | Done
FontAttributes | Done
FontFamily | Done
FontSize | Done
XAlign | Done
YAlign | Done
### ListView
An [ItemsView](https://developer.xamarin.com/api/type/Xamarin.Forms.ListView/) that displays a collection of data as a vertical list.
Property | Status
------ | ------
IsPullToRefreshEnabled | Pending
IsRefreshing | Pending
ItemsSource | Done
RefreshCommand | Pending
Header | Pending
HeaderTemplate | Pending
Footer | Pending
FooterTemplate | Pending
SelectedItem | Done
HasUnevenRows | Pending
RowHeight | Pending
GroupHeaderTemplate | Done
IsGroupingEnabled | Pending
SeparatorVisibility | Pending
SeparatorColor | Pending
ContextActions | Pending
ScrollTo | Pending
ItemAppearing | Pending
ItemDisappearing | Pending
ItemSelected | Pending
ItemTapped | Done
Refresing | Pending
### MasterDetailPage
A [Page](https://developer.xamarin.com/api/type/Xamarin.Forms.MasterDetailPage/) that manages two panes of information: A master page that presents data at a high level, and a detail page that displays low-level details about information in the master.
Property | Status
------ | ------
Detail | Pending
IsGestureEnabled | Pending
IsPresented | Pending
Master | Pending
MasterBehavior | Pending
ShouldShowToolBarButton | Pending
IsPresentedChanged | Pending
### Map
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Maps.Map/) that shows a map provided by a platform-specific service.
Property | Status
------ | ------
Pins | Pending
MapType | Pending
IsShowingUser | Pending
HasScrollEnabled | Pending
HasZoomEnabled | Pending
VisibleRegion | Pending
MoveToRegion | Pending
### NavigationPage
A [Page](https://developer.xamarin.com/api/type/Xamarin.Forms.NavigationPage/) that manages the navigation and user-experience of a stack of other pages.
Property | Status
------ | ------
Animate | Pending
Add | Pending
Remove | Pending
### OpenGLView
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.OpenGLView/) that displays OpenGL content.
Property | Status
------ | ------
HasRenderLoop | Pending
### Picker
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Picker/) control for picking an element in a list.
Property | Status
------ | ------
Title | Pending
SelectedIndex | Pending
ItemsSource | Pending
SelectedItem | Pending
Items | Pending
itemDisplayBinding | Pending
SelectedIndexChanged | Pending
### Progress
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.ProgressBar/) control that displays progress.
Property | Status
------ | ------
BackgroundColor | Pending
Progress | Done
ProgressTo | Pending
### ScrollView
An [element](https://developer.xamarin.com/api/type/Xamarin.Forms.ScrollView/) capable of scrolling if its Content requires.
Property | Status
------ | ------
BackgroundColor | Pending
Orientation | Pending
ScrollX | Pending
ScrollY | Pending
Content | Pending
ContentSize | Pending
ScrollToAsync | Pending
Scrolled | Pending
### SearchBar
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.SearchBar/) control that provides a search box.
Property | Status
------ | ------
BackgroundColor | Done
SearchCommand | Done
SearchCommandParameter | Done
CancelButtonColor | Pending
Placeholder | Done
FontFamily | Pending
FontSize | Pending
FontAtttributes | Pending
HorizontalTextAlignment | Done
TextColor | Done
PlaceholderColor | Pending
FontFamily | Pending
SearchButtonPressed | Done
TextChanged | Done
### Slider
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Slider/) control that inputs a linear value.
Property | Status
------ | ------
BackgroundColor | Done
Minimum | Done
Maximum | Done
Value | Done
ValueChanged | Done
### Stepper
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Stepper/) control that inputs a discrete value, constrained to a range.
Property | Status
------ | ------
BackgroundColor | Pending
Minimum | Pending
Maximum | Pending
Value | Pending
Increment | Pending
ValueChanged | Pending
### Switch
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Switch/) control that provides a toggled value.
Property | Status
------ | ------
BackgroundColor | Done
IsToggled | Done
Toggled | Done
### TabbedPage
[Displays](https://developer.xamarin.com/api/type/Xamarin.Forms.TabbedPage/) an array of tabs across the top of the screen, each of which loads content onto the screen.
Property | Status
------ | ------
BarBackgroundColor | Pending
BarTextColor | Pending
ItemsSource | Pending
ItemTemplate | Pending
SelectedItem | Pending
GetIndex | Pending
GetPageByIndex | Pending
SetIndex | Pending
Appearing | Pending
Disappearing | Pending
CurrentPageChanged | Pending
PagesChanged | Pending
### TableView
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.TableView/) that holds rows of Cell elements.
Property | Status
------ | ------
BackgroundColor | Pending
HasUnevenRows | Pending
TableIntent | Pending
RowHeight | Pending
Root | Pending
ContextActions | Pending
### TimePicker
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.TimePicker/) control that provides time picking.
Property | Status
------ | ------
BackgroundColor | Done
Format | Done
TextColor | Done
Time | Done
### WebView
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.WebView/) that presents HTML content.
Property | Status
------ | ------
CanGoBack | Pending
CamGoForward | Pending
Source | Done
Eval | Pending
GoBack | Pending
GoForward | Pending
Navigated | Pending
Navigating | Pending
### VisualElement
Property | Status
------ | ------
BackgroundColor | Done
AnchorX | Done
AnchorY | Done
IsEnabled | Done
IsFocused | Done
IsVisible | Done
Opacity | Done
RotationX | Pending
RotationY | Pending
Scale | Done
TranslationX | Done
TranslationY | Done

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

@ -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 ?? ""));
}
@ -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);
@ -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
{
@ -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 ();
}
}
}
}

View File

@ -4,7 +4,8 @@
| ------- | ------- | ----------- |
| [![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 ([Status](Documentation/OouiFormsStatus.md)) |
| [![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 *weee!*) is a small cross-platform UI library for .NET that uses web technologies.
@ -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/index.html](https://s3.amazonaws.com/praeclarum.org/wasm/index.html) to try the WebAssembly mode of Ooui running Xamarin.Forms. (That's Xamarin.Forms running right in your browser!)
## Try the Samples Locally
@ -57,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
@ -82,19 +85,30 @@ With just that code, a web server that serves the HTML and web socket logic nece
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.
<table>
<thead><tr><th>Ooui</th><th>Ooui.AspNetCore</th><th>Ooui.Forms</th><th></th></tr></thead>
<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><a href="https://github.com/praeclarum/Ooui/wiki/Write-the-UI-using-the-web-DOM-and-use-the-built-in-web-server">Write the UI using the web DOM and use the built-in web server</a></td>
<td>&check;</td><td></td><td></td><td></td><td><a href="https://github.com/praeclarum/Ooui/wiki/Web-DOM-with-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>Write the UI using the web DOM and serve it with ASP.NET Core</td>
<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>Write the UI using Xamarin.Forms and serve it with ASP.NET Core</td>
<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>Write the UI using Xamarin.Forms and use the built-in web server</td>
<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><a href="https://github.com/praeclarum/Ooui/wiki/Web DOM-with-Web-Assembly">Web DOM with Web Assembly</a></td>
</tr>
<tr>
<td>&check;</td><td></td><td>&check;</td><td>&check;</td><td><a href="https://github.com/praeclarum/Ooui/wiki/Xamarin.Forms-with-Web-Assembly">Xamarin.Forms with Web Assembly</a></td>
</tr>
</table>
@ -106,6 +120,8 @@ When the user requests a page, the page will connect to the server using a web s
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.
## Contributing

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