From 7b9bf654963cca8f565a5594ebdaa14a09231265 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 18:43:23 -0800 Subject: [PATCH 1/5] Use attributes instead of properties --- Ooui.Forms/Renderers/WebViewRenderer.cs | 4 +- Ooui/Anchor.cs | 5 +- Ooui/Button.cs | 4 +- Ooui/Canvas.cs | 10 +- Ooui/Element.cs | 131 ++++++++++++++++++++++-- Ooui/EventTarget.cs | 13 ++- Ooui/Form.cs | 14 ++- Ooui/FormControl.cs | 9 +- Ooui/Iframe.cs | 12 +-- Ooui/Image.cs | 8 +- Ooui/Input.cs | 42 ++++---- Ooui/Label.cs | 5 +- Ooui/Message.cs | 2 + Ooui/Option.cs | 15 ++- Ooui/Select.cs | 9 +- Ooui/TextArea.cs | 9 +- 16 files changed, 197 insertions(+), 95 deletions(-) diff --git a/Ooui.Forms/Renderers/WebViewRenderer.cs b/Ooui.Forms/Renderers/WebViewRenderer.cs index 381e905..496f45a 100644 --- a/Ooui.Forms/Renderers/WebViewRenderer.cs +++ b/Ooui.Forms/Renderers/WebViewRenderer.cs @@ -24,7 +24,7 @@ namespace Ooui.Forms.Renderers if (_iframe != null) { - _iframe.Src = html; + _iframe.Source = html; } } catch (Exception ex) @@ -47,7 +47,7 @@ namespace Ooui.Forms.Renderers if (_iframe != null) { - _iframe.Src = url; + _iframe.Source = url; } } catch (Exception ex) diff --git a/Ooui/Anchor.cs b/Ooui/Anchor.cs index 864011b..6ea2853 100644 --- a/Ooui/Anchor.cs +++ b/Ooui/Anchor.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Anchor : Element { - string href = ""; public string HRef { - get => href; - set => SetProperty (ref href, value ?? "", "href"); + get => GetStringAttribute ("href", ""); + set => SetAttributeProperty ("href", value); } public Anchor () diff --git a/Ooui/Button.cs b/Ooui/Button.cs index 386fcd9..4102dcd 100644 --- a/Ooui/Button.cs +++ b/Ooui/Button.cs @@ -9,8 +9,8 @@ namespace Ooui { ButtonType typ = ButtonType.Submit; public ButtonType Type { - get => typ; - set => SetProperty (ref typ, value, "type"); + get => GetAttribute ("type", ButtonType.Submit); + set => SetAttributeProperty ("type", value); } public Button () diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs index b3dc144..2c009ac 100644 --- a/Ooui/Canvas.cs +++ b/Ooui/Canvas.cs @@ -7,16 +7,14 @@ namespace Ooui CanvasRenderingContext2D context2d = new CanvasRenderingContext2D (); int gotContext2d = 0; - int width = 300; public int Width { - get => width; - set => SetProperty (ref width, value <= 0 ? 150 : value, "width"); + get => GetAttribute ("width", 300); + set => SetAttributeProperty ("width", value < 0 ? 0 : value); } - int height = 150; public int Height { - get => height; - set => SetProperty (ref height, value <= 0 ? 150 : value, "height"); + get => GetAttribute ("height", 150); + set => SetAttributeProperty ("width", value < 0 ? 0 : value); } public Canvas () diff --git a/Ooui/Element.cs b/Ooui/Element.cs index bc08de7..05626be 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -1,28 +1,29 @@ using System; +using System.Collections.Generic; using System.ComponentModel; namespace Ooui { public abstract class Element : Node { - string className = ""; + readonly Dictionary attributes = new Dictionary (); + public string ClassName { - get => className; - set => SetProperty (ref className, value, "className"); + get => GetStringAttribute ("class", ""); + set => SetAttributeProperty ("class", value); } public Style Style { get; private set; } = new Style (); - string title = ""; public string Title { - get => title; - set => SetProperty (ref title, value, "title"); + get => GetStringAttribute ("title", ""); + set => SetAttributeProperty ("title", value); } bool hidden = false; public bool IsHidden { - get => hidden; - set => SetProperty (ref hidden, value, "hidden"); + get => GetBooleanAttribute ("hidden"); + set => SetBooleanAttributeProperty ("hidden", value); } public event TargetEventHandler Click { @@ -102,8 +103,64 @@ namespace Ooui Style.PropertyChanged += HandleStylePropertyChanged; } - public void SetAttribute (string attributeName, string value) + protected bool SetAttributeProperty (string attributeName, object newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { + var old = GetAttribute (attributeName); + if (old != null && old.Equals (newValue)) + return false; + SetAttribute (attributeName, newValue); + OnPropertyChanged (propertyName); + return true; + } + + protected bool SetBooleanAttributeProperty (string attributeName, bool newValue, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + { + var old = GetAttribute (attributeName) != null; + if (old != newValue) + return false; + if (newValue) + SetAttribute (attributeName, string.Empty); + else + RemoveAttribute (attributeName); + OnPropertyChanged (propertyName); + return true; + } + + protected bool UpdateAttributeProperty (string attributeName, object newValue, string propertyName) + { + lock (attributes) { + if (attributes.TryGetValue (attributeName, out var oldValue)) { + if (newValue != null && newValue.Equals (oldValue)) + return false; + } + attributes[attributeName] = newValue; + } + OnPropertyChanged (propertyName); + return true; + } + + protected bool UpdateBooleanAttributeProperty (string attributeName, bool newValue, string propertyName) + { + lock (attributes) { + var oldValue = attributes.ContainsKey (attributeName); + if (newValue == oldValue) + return false; + if (newValue) { + attributes[attributeName] = ""; + } + else { + attributes.Remove (attributeName); + } + } + OnPropertyChanged (propertyName); + return true; + } + + public void SetAttribute (string attributeName, object value) + { + lock (attributes) { + attributes[attributeName] = value; + } Send (new Message { MessageType = MessageType.SetAttribute, TargetId = Id, @@ -112,6 +169,62 @@ namespace Ooui }); } + public object GetAttribute (string attributeName) + { + lock (attributes) { + attributes.TryGetValue (attributeName, out var v); + return v; + } + } + + public T GetAttribute (string attributeName, T defaultValue) + { + lock (attributes) { + attributes.TryGetValue (attributeName, out var v); + if (v is T) { + return (T)v; + } + else { + return defaultValue; + } + } + } + + public bool GetBooleanAttribute (string attributeName) + { + lock (attributes) { + return attributes.TryGetValue (attributeName, out var _); + } + } + + public string GetStringAttribute (string attributeName, string defaultValue) + { + lock (attributes) { + if (attributes.TryGetValue (attributeName, out var v)) { + if (v == null) return "null"; + else return v.ToString (); + } + else { + return defaultValue; + } + } + } + + public void RemoveAttribute (string attributeName) + { + bool removed; + lock (attributes) { + removed = attributes.Remove (attributeName); + } + if (removed) { + Send (new Message { + MessageType = MessageType.RemoveAttribute, + TargetId = Id, + Key = attributeName, + }); + } + } + void HandleStylePropertyChanged (object sender, PropertyChangedEventArgs e) { SendSet ("style." + Style.GetJsName (e.PropertyName), Style[e.PropertyName]); diff --git a/Ooui/EventTarget.cs b/Ooui/EventTarget.cs index e3e26c1..4e8c273 100644 --- a/Ooui/EventTarget.cs +++ b/Ooui/EventTarget.cs @@ -83,12 +83,12 @@ namespace Ooui } } - protected bool SetProperty (ref T backingStore, T newValue, string attributeName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") + protected bool SetProperty (ref T backingStore, T newValue, string jsPropertyName, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { if (EqualityComparer.Default.Equals (backingStore, newValue)) return false; backingStore = newValue; - SendSet (attributeName, newValue); + SendSet (jsPropertyName, newValue); OnPropertyChanged (propertyName); return true; } @@ -120,12 +120,12 @@ namespace Ooui Send (Message.Call (Id, methodName, args)); } - protected void SendSet (string attributeName, object value) + protected void SendSet (string jsPropertyName, object value) { Send (new Message { MessageType = MessageType.Set, TargetId = Id, - Key = attributeName, + Key = jsPropertyName, Value = value, }); } @@ -169,6 +169,11 @@ namespace Ooui state.Add (message); }); break; + case MessageType.RemoveAttribute: + this.UpdateStateMessages (state => { + state.RemoveAll (x => x.MessageType == MessageType.SetAttribute && x.Key == message.Key); + }); + return true; case MessageType.Listen: AddStateMessage (message); break; diff --git a/Ooui/Form.cs b/Ooui/Form.cs index 809397a..897a319 100644 --- a/Ooui/Form.cs +++ b/Ooui/Form.cs @@ -6,20 +6,18 @@ namespace Ooui { string action = ""; public string Action { - get => action; - set => SetProperty (ref action, value ?? "", "action"); + get => GetStringAttribute ("action", ""); + set => SetAttributeProperty ("action", value ?? ""); } - string method = "GET"; public string Method { - get => method; - set => SetProperty (ref method, value ?? "", "method"); + get => GetStringAttribute ("method", "GET"); + set => SetAttributeProperty ("method", value ?? ""); } - string enctype = "application/x-www-form-urlencoded"; public string EncodingType { - get => enctype; - set => SetProperty (ref enctype, value ?? "", "enctype"); + get => GetStringAttribute ("enctype", "application/x-www-form-urlencoded"); + set => SetAttributeProperty ("enctype", value ?? ""); } public event TargetEventHandler Submit { diff --git a/Ooui/FormControl.cs b/Ooui/FormControl.cs index e98607e..461b301 100644 --- a/Ooui/FormControl.cs +++ b/Ooui/FormControl.cs @@ -4,16 +4,15 @@ namespace Ooui { public abstract class FormControl : Element { - string name = ""; public string Name { - get => name; - set => SetProperty (ref name, value, "name"); + get => GetStringAttribute ("name", ""); + set => SetAttributeProperty ("name", value); } bool isDisabled = false; public bool IsDisabled { - get => isDisabled; - set => SetProperty (ref isDisabled, value, "disabled"); + get => GetBooleanAttribute ("disabled"); + set => SetBooleanAttributeProperty ("disabled", value); } public FormControl (string tagName) diff --git a/Ooui/Iframe.cs b/Ooui/Iframe.cs index 587109e..26a917c 100644 --- a/Ooui/Iframe.cs +++ b/Ooui/Iframe.cs @@ -2,17 +2,15 @@ { public class Iframe : Element { - public Iframe() - : base("iframe") + public string Source { - + get => GetStringAttribute ("src", null); + set => SetAttributeProperty ("src", value); } - string src = null; - public string Src + public Iframe () + : base ("iframe") { - get => src; - set => SetProperty(ref src, value, "src"); } } } diff --git a/Ooui/Image.cs b/Ooui/Image.cs index 6bba7ff..24bff04 100644 --- a/Ooui/Image.cs +++ b/Ooui/Image.cs @@ -4,10 +4,10 @@ namespace Ooui { public class Image : Element { - string src = ""; - public string Source { - get => src; - set => SetProperty (ref src, value ?? "", "src"); + public string Source + { + get => GetStringAttribute ("src", null); + set => SetAttributeProperty ("src", value); } public Image () diff --git a/Ooui/Input.cs b/Ooui/Input.cs index 90f73d8..9574d11 100644 --- a/Ooui/Input.cs +++ b/Ooui/Input.cs @@ -7,16 +7,14 @@ namespace Ooui { public class Input : FormControl { - InputType typ = InputType.Text; public InputType Type { - get => typ; - set => SetProperty (ref typ, value, "type"); + get => GetAttribute ("type", InputType.Text); + set => SetAttributeProperty ("type", value); } - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } public double NumberValue { @@ -35,37 +33,33 @@ namespace Ooui remove => RemoveEventListener ("change", value); } - string placeholder = ""; public string Placeholder { - get => placeholder; - set => SetProperty (ref placeholder, value, "placeholder"); + get => GetStringAttribute ("placeholder", ""); + set => SetAttributeProperty ("placeholder", value ?? ""); } - bool isChecked = false; public bool IsChecked { - get => isChecked; + get => GetBooleanAttribute ("checked"); set { - SetProperty (ref isChecked, value, "checked"); - TriggerEventFromMessage (Message.Event (Id, "change", isChecked)); + if (SetBooleanAttributeProperty ("checked", value)) { + TriggerEventFromMessage (Message.Event (Id, "change", IsChecked)); + } } } - double minimum = 0; public double Minimum { - get => minimum; - set => SetProperty (ref minimum, value, "min"); + get => GetAttribute ("min", 0.0); + set => SetAttributeProperty ("min", value); } - double maximum = 100; public double Maximum { - get => maximum; - set => SetProperty (ref maximum, value, "max"); + get => GetAttribute ("max", 100.0); + set => SetAttributeProperty ("max", value); } - double step = 1; public double Step { - get => step; - set => SetProperty (ref step, value, "step"); + get => GetAttribute ("step", 1.0); + set => SetAttributeProperty ("step", value); } public Input () @@ -86,10 +80,10 @@ namespace Ooui if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { // Don't need to notify here because the base implementation will fire the event if (Type == InputType.Checkbox) { - isChecked = message.Value != null ? Convert.ToBoolean (message.Value) : false; + UpdateBooleanAttributeProperty ("checked", message.Value != null ? Convert.ToBoolean (message.Value) : false, "IsChecked"); } else { - val = message.Value != null ? Convert.ToString (message.Value) : ""; + UpdateAttributeProperty ("value", message.Value != null ? Convert.ToString (message.Value) : "", "Value"); } } return base.TriggerEventFromMessage (message); diff --git a/Ooui/Label.cs b/Ooui/Label.cs index 9dcfd7f..ca27a15 100644 --- a/Ooui/Label.cs +++ b/Ooui/Label.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Label : Element { - Element htmlFor = null; public Element For { - get => htmlFor; - set => SetProperty (ref htmlFor, value, "htmlFor"); + get => GetAttribute ("for", null); + set => SetAttributeProperty ("for", value); } public Label () diff --git a/Ooui/Message.cs b/Ooui/Message.cs index 0498437..1e78fc9 100644 --- a/Ooui/Message.cs +++ b/Ooui/Message.cs @@ -48,6 +48,8 @@ namespace Ooui Set, [EnumMember (Value = "setAttr")] SetAttribute, + [EnumMember(Value = "remAttr")] + RemoveAttribute, [EnumMember(Value = "call")] Call, [EnumMember(Value = "listen")] diff --git a/Ooui/Option.cs b/Ooui/Option.cs index 86d918e..22984eb 100644 --- a/Ooui/Option.cs +++ b/Ooui/Option.cs @@ -4,22 +4,19 @@ namespace Ooui { public class Option : Element { - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } - string label = ""; public string Label { - get => label; - set => SetProperty (ref label, value ?? "", "label"); + get => GetStringAttribute ("label", ""); + set => SetAttributeProperty ("label", value ?? ""); } - bool defaultSelected = false; public bool DefaultSelected { - get => defaultSelected; - set => SetProperty (ref defaultSelected, value, "defaultSelected"); + get => GetBooleanAttribute ("selected"); + set => SetBooleanAttributeProperty ("selected", value); } public Option () diff --git a/Ooui/Select.cs b/Ooui/Select.cs index c88eed4..885de9b 100644 --- a/Ooui/Select.cs +++ b/Ooui/Select.cs @@ -4,10 +4,9 @@ namespace Ooui { public class Select : FormControl { - string val = ""; public string Value { - get => val; - set => SetProperty (ref val, value ?? "", "value"); + get => GetStringAttribute ("value", ""); + set => SetAttributeProperty ("value", value ?? ""); } public event TargetEventHandler Change { @@ -35,6 +34,7 @@ namespace Ooui protected override void OnChildInsertedBefore (Node newChild, Node referenceChild) { base.OnChildInsertedBefore (newChild, referenceChild); + var val = Value; if (string.IsNullOrEmpty (val) && newChild is Option o && !string.IsNullOrEmpty (o.Value)) { val = o.Value; } @@ -43,7 +43,8 @@ namespace Ooui protected override bool TriggerEventFromMessage (Message message) { if (message.TargetId == Id && message.MessageType == MessageType.Event && (message.Key == "change" || message.Key == "input")) { - val = message.Value != null ? Convert.ToString (message.Value) : ""; + SetAttribute ("value", message.Value != null ? Convert.ToString (message.Value) : ""); + OnPropertyChanged ("Value"); } return base.TriggerEventFromMessage (message); } diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index a35b962..b7cc341 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -20,16 +20,15 @@ namespace Ooui set => SetProperty (ref val, value ?? "", "value"); } - int rows = 2; public int Rows { - get => rows; - set => SetProperty (ref rows, value, "rows"); + get => GetAttribute ("rows", 2); + set => SetAttributeProperty ("rows", value); } int cols = 20; public int Columns { - get => cols; - set => SetProperty (ref cols, value, "cols"); + get => GetAttribute ("cols", 20); + set => SetAttributeProperty ("cols", value); } public TextArea () From 0c03a0bf49658b9881f391073e27ce5f2a225940 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 18:51:00 -0800 Subject: [PATCH 2/5] Fix Canvas size bugs --- Ooui/Canvas.cs | 2 +- Tests/CanvasTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Ooui/Canvas.cs b/Ooui/Canvas.cs index 2c009ac..66ff2cb 100644 --- a/Ooui/Canvas.cs +++ b/Ooui/Canvas.cs @@ -14,7 +14,7 @@ namespace Ooui public int Height { get => GetAttribute ("height", 150); - set => SetAttributeProperty ("width", value < 0 ? 0 : value); + set => SetAttributeProperty ("height", value < 0 ? 0 : value); } public Canvas () diff --git a/Tests/CanvasTests.cs b/Tests/CanvasTests.cs index ac8def7..dc93a19 100644 --- a/Tests/CanvasTests.cs +++ b/Tests/CanvasTests.cs @@ -58,8 +58,8 @@ namespace Tests Assert.AreEqual (480, c.Height); c.Width = 0; c.Height = -100; - Assert.AreEqual (150, c.Width); - Assert.AreEqual (150, c.Height); + Assert.AreEqual (0, c.Width); + Assert.AreEqual (0, c.Height); } } } From 4148ea17beadc54f94123e051e57b6a98713569b Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 20:18:16 -0800 Subject: [PATCH 3/5] Generate initial static html --- Ooui.AspNetCore/ElementResult.cs | 24 +++---- Ooui.Forms/Renderers/ViewRenderer.cs | 2 + Ooui.Forms/VisualElementRenderer.cs | 2 + Ooui/Client.js | 2 + Ooui/Div.cs | 2 + Ooui/Element.cs | 33 ++++++++++ Ooui/Node.cs | 19 ++++++ Ooui/Style.cs | 2 +- Ooui/TextArea.cs | 7 ++ Ooui/TextNode.cs | 5 ++ Ooui/UI.cs | 9 ++- Tests/WriteHtmlTests.cs | 95 ++++++++++++++++++++++++++++ 12 files changed, 187 insertions(+), 15 deletions(-) create mode 100644 Tests/WriteHtmlTests.cs diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index 23234a6..ae8cc0f 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -1,28 +1,30 @@ using System; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + namespace Ooui.AspNetCore { public class ElementResult : ActionResult - { + { readonly Element element; - readonly string title; - + readonly string title; + public ElementResult (Element element, string title = "") - { + { this.element = element; - this.title = title; + this.title = title; } - public override async Task ExecuteResultAsync (ActionContext context) + public override async Task ExecuteResultAsync (ActionContext context) { var response = context.HttpContext.Response; response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); - var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title); + var initialHtml = element.OuterHtml; + Console.WriteLine(initialHtml); + var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml); var htmlBytes = Encoding.UTF8.GetBytes (html); response.ContentLength = htmlBytes.Length; using (var s = response.Body) { diff --git a/Ooui.Forms/Renderers/ViewRenderer.cs b/Ooui.Forms/Renderers/ViewRenderer.cs index b425349..0344223 100644 --- a/Ooui.Forms/Renderers/ViewRenderer.cs +++ b/Ooui.Forms/Renderers/ViewRenderer.cs @@ -21,6 +21,8 @@ namespace Ooui.Forms.Renderers /// protected virtual bool ManageNativeControlLifetime => true; + protected override bool HtmlNeedsFullEndElement => TagName == "div"; + public ViewRenderer (string tagName = "div") : base (tagName) { diff --git a/Ooui.Forms/VisualElementRenderer.cs b/Ooui.Forms/VisualElementRenderer.cs index 628d863..b436710 100644 --- a/Ooui.Forms/VisualElementRenderer.cs +++ b/Ooui.Forms/VisualElementRenderer.cs @@ -62,6 +62,8 @@ namespace Ooui.Forms } } + protected override bool HtmlNeedsFullEndElement => TagName == "div"; + public VisualElementRenderer (string tagName = "div") : base (tagName) { _propertyChangedHandler = OnElementPropertyChanged; diff --git a/Ooui/Client.js b/Ooui/Client.js index e053d5c..d19f249 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -38,6 +38,8 @@ function getSize () { // Main entrypoint function ooui (rootElementPath) { + return; + var initialSize = getSize (); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; diff --git a/Ooui/Div.cs b/Ooui/Div.cs index d4d8009..f9098c4 100644 --- a/Ooui/Div.cs +++ b/Ooui/Div.cs @@ -5,6 +5,8 @@ namespace Ooui { public class Div : Element { + protected override bool HtmlNeedsFullEndElement => true; + public Div () : base ("div") { diff --git a/Ooui/Element.cs b/Ooui/Element.cs index 05626be..2e32512 100644 --- a/Ooui/Element.cs +++ b/Ooui/Element.cs @@ -243,5 +243,38 @@ namespace Ooui return base.SaveStateMessageIfNeeded (message); } } + + protected virtual bool HtmlNeedsFullEndElement => false; + + public override void WriteOuterHtml (System.Xml.XmlWriter w) + { + w.WriteStartElement (TagName); + w.WriteAttributeString ("id", Id); + var style = Style.ToString (); + if (style.Length > 0) { + w.WriteAttributeString ("style", style); + } + lock (attributes) { + foreach (var a in attributes) { + var value = (a.Value == null) ? "null" : Convert.ToString (a.Value, System.Globalization.CultureInfo.InvariantCulture); + w.WriteAttributeString (a.Key, value); + } + } + WriteInnerHtml (w); + if (HtmlNeedsFullEndElement) { + w.WriteFullEndElement (); + } + else { + w.WriteEndElement (); + } + } + + public virtual void WriteInnerHtml (System.Xml.XmlWriter w) + { + var children = Children; + foreach (var c in children) { + c.WriteOuterHtml (w); + } + } } } diff --git a/Ooui/Node.cs b/Ooui/Node.cs index 18d16a0..374c016 100644 --- a/Ooui/Node.cs +++ b/Ooui/Node.cs @@ -180,5 +180,24 @@ namespace Ooui } return false; } + + public virtual string OuterHtml { + get { + using (var stream = new System.IO.MemoryStream ()) { + var settings = new System.Xml.XmlWriterSettings { + OmitXmlDeclaration = true, + ConformanceLevel = System.Xml.ConformanceLevel.Fragment, + CloseOutput = false, + }; + using (var w = System.Xml.XmlWriter.Create (stream, settings)) { + WriteOuterHtml (w); + } + stream.Position = 0; + return new System.IO.StreamReader (stream).ReadToEnd (); + } + } + } + + public abstract void WriteOuterHtml (System.Xml.XmlWriter w); } } diff --git a/Ooui/Style.cs b/Ooui/Style.cs index 6a6f307..71f3177 100644 --- a/Ooui/Style.cs +++ b/Ooui/Style.cs @@ -399,7 +399,7 @@ namespace Ooui o.Append (head); o.Append (p.Key); o.Append (":"); - o.Append (String.Format (System.Globalization.CultureInfo.InvariantCulture, "{0}", p.Value)); + o.Append (Convert.ToString (p.Value, System.Globalization.CultureInfo.InvariantCulture)); head = ";"; } } diff --git a/Ooui/TextArea.cs b/Ooui/TextArea.cs index b7cc341..f3f2d5f 100644 --- a/Ooui/TextArea.cs +++ b/Ooui/TextArea.cs @@ -31,6 +31,8 @@ namespace Ooui set => SetAttributeProperty ("cols", value); } + protected override bool HtmlNeedsFullEndElement => true; + public TextArea () : base ("textarea") { @@ -52,5 +54,10 @@ namespace Ooui } return base.TriggerEventFromMessage (message); } + + public override void WriteInnerHtml (System.Xml.XmlWriter w) + { + w.WriteString (val ?? ""); + } } } diff --git a/Ooui/TextNode.cs b/Ooui/TextNode.cs index 47441c6..8fccf8c 100644 --- a/Ooui/TextNode.cs +++ b/Ooui/TextNode.cs @@ -20,5 +20,10 @@ namespace Ooui { Text = text; } + + public override void WriteOuterHtml (System.Xml.XmlWriter w) + { + w.WriteString (text); + } } } diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 33c39ac..c865a3c 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -44,7 +44,10 @@ namespace Ooui -
+ +
+@InitialHtml +
@@ -390,9 +393,9 @@ namespace Ooui } } - public static string RenderTemplate (string webSocketPath, string title = "") + public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "") { - return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title); + return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml); } class DataHandler : RequestHandler diff --git a/Tests/WriteHtmlTests.cs b/Tests/WriteHtmlTests.cs new file mode 100644 index 0000000..5d95fb8 --- /dev/null +++ b/Tests/WriteHtmlTests.cs @@ -0,0 +1,95 @@ +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; + +namespace Tests +{ + [TestClass] + public class WriteHtmlTests + { + System.Text.RegularExpressions.Regex idre = new System.Text.RegularExpressions.Regex ("\\sid=\"[^\"]*\""); + + string OuterHtmlWithoutIds (Element e) + { + return idre.Replace (e.OuterHtml, ""); + } + + [TestMethod] + public void TextAreaWithTextStyled () + { + var e = new TextArea { + Value = "Hello World!", + }; + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TextAreaEmptyStyled () + { + var e = new TextArea (); + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void Style () + { + var e = new Div (); + e.Style.BackgroundColor = "#18f"; + Assert.AreEqual ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TwoGrandChildren () + { + var e = new Div (new Div (new Anchor (), new Anchor ()), new Paragraph ()); + Assert.AreEqual ("

", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void Child () + { + var e = new Div (new Anchor ()); + Assert.AreEqual ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void TextChild () + { + var e = new Paragraph ("Hello world!"); + Assert.AreEqual ("

Hello world!

", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void IdIsFirst () + { + var e = new Anchor (); + Assert.IsTrue (e.OuterHtml.StartsWith ("
", OuterHtmlWithoutIds (e)); + } + + [TestMethod] + public void AnchorHRef () + { + var e = new Anchor { + HRef = "http://google.com" + }; + Assert.AreEqual ("", OuterHtmlWithoutIds (e)); + } + } +} From 28c8fac046bc74dec58766ae200486663b453285 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 21:02:59 -0800 Subject: [PATCH 4/5] Store window height in cookies so initial html is the right size --- Ooui.AspNetCore/ElementResult.cs | 23 ++++++++++++++++++++++- Ooui/Client.js | 19 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index ae8cc0f..e918d73 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -1,6 +1,7 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Ooui.AspNetCore @@ -21,9 +22,14 @@ namespace Ooui.AspNetCore var response = context.HttpContext.Response; response.StatusCode = 200; response.ContentType = "text/html; charset=utf-8"; + + if (element.WantsFullScreen) { + element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000); + element.Style.Height = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowHeight", 24, 480, 10000); + } + var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); var initialHtml = element.OuterHtml; - Console.WriteLine(initialHtml); var html = UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId, title: title, initialHtml: initialHtml); var htmlBytes = Encoding.UTF8.GetBytes (html); response.ContentLength = htmlBytes.Length; @@ -31,5 +37,20 @@ namespace Ooui.AspNetCore await s.WriteAsync (htmlBytes, 0, htmlBytes.Length).ConfigureAwait (false); } } + + static double GetCookieDouble (IRequestCookieCollection cookies, string key, double min, double def, double max) + { + if (cookies.TryGetValue (key, out var s)) { + if (double.TryParse (s, out var d)) { + if (d < min) return min; + if (d > max) return max; + return d; + } + return def; + } + else { + return def; + } + } } } diff --git a/Ooui/Client.js b/Ooui/Client.js index d19f249..0abe37b 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -35,12 +35,29 @@ function getSize () { }; } +function setCookie (name, value, days) { + var expires = ""; + if (days) { + var date = new Date (); + date.setTime(date.getTime () + (days*24*60*60*1000)); + expires = "; expires=" + date.toUTCString(); + } + document.cookie = name + "=" + (value || "") + expires + "; path=/"; +} + +function saveSize (s) { + setCookie ("oouiWindowWidth", s.width, 7); + setCookie ("oouiWindowHeight", s.height, 7); +} + // Main entrypoint function ooui (rootElementPath) { + var initialSize = getSize (); + saveSize (initialSize); + return; - var initialSize = getSize (); var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; From 1207194e1e5829bd358808e6ddd966a62ad37e05 Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 1 Feb 2018 21:37:21 -0800 Subject: [PATCH 5/5] Use existing html nodes with socket --- Ooui/Client.js | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/Ooui/Client.js b/Ooui/Client.js index 0abe37b..723a910 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -2,6 +2,7 @@ var debug = false; const nodes = {}; +const hasText = {}; let socket = null; @@ -56,8 +57,6 @@ function ooui (rootElementPath) { var initialSize = getSize (); saveSize (initialSize); - return; - var wsArgs = (rootElementPath.indexOf("?") >= 0 ? "&" : "?") + "w=" + initialSize.width + "&h=" + initialSize.height; @@ -142,12 +141,22 @@ function getNode (id) { } } +function getOrCreateElement (id, tagName) { + var e = document.getElementById (id); + if (e) { + if (e.firstChild && e.firstChild.nodeType == Node.TEXT_NODE) + hasText[e.id] = true; + return e; + } + return document.createElement (tagName); +} + function msgCreate (m) { const id = m.id; const tagName = m.k; const node = tagName === "#text" ? document.createTextNode ("") : - document.createElement (tagName); + getOrCreateElement (id, tagName); if (tagName !== "#text") node.id = id; nodes[id] = node; @@ -183,6 +192,17 @@ function msgSetAttr (m) { if (debug) console.log ("SetAttr", node, m.k, m.v); } +function msgRemAttr (m) { + const id = m.id; + const node = getNode (id); + if (!node) { + console.error ("Unknown node id", m); + return; + } + node.removeAttribute(m.k); + if (debug) console.log ("RemAttr", node, m.k); +} + function msgCall (m) { const id = m.id; const node = getNode (id); @@ -192,6 +212,12 @@ function msgCall (m) { } const isJQuery = m.k.startsWith ("$."); const target = isJQuery ? $(node) : node; + if (m.k === "insertBefore" && m.v[0].nodeType == Node.TEXT_NODE && m.v[1] == null && hasText[id]) { + // Text is already set so it clear it first + if (target.firstChild) + target.removeChild (target.firstChild); + delete hasText[id]; + } const f = isJQuery ? target[m.k.slice(2)] : target[m.k]; if (debug) console.log ("Call", node, f, m.v); const r = f.apply (target, m.v); @@ -246,6 +272,9 @@ function processMessage (m) { case "setAttr": msgSetAttr (m); break; + case "remAttr": + msgRemAttr (m); + break; case "call": msgCall (m); break;