From 3b1587f7235e370b0a5a17f8b767b8d109c95d9e Mon Sep 17 00:00:00 2001 From: Lee McPherson Date: Sun, 20 May 2018 13:42:22 -0700 Subject: [PATCH] (mostly) Implementation of NavigationPage --- Ooui.Forms/Exports.cs | 1 + .../Renderers/NavigationPageRenderer.cs | 196 ++++++++++++++++++ Ooui/Client.js | 49 ++++- Samples/Navigation/NavigationFirstPage.xaml | 15 ++ .../Navigation/NavigationFirstPage.xaml.cs | 29 +++ Samples/Navigation/NavigationSecondPage.xaml | 18 ++ .../Navigation/NavigationSecondPage.xaml.cs | 31 +++ Samples/Navigation/NavigationThirdPage.xaml | 17 ++ .../Navigation/NavigationThirdPage.xaml.cs | 31 +++ Samples/NavigationSample.cs | 18 ++ 10 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 Ooui.Forms/Renderers/NavigationPageRenderer.cs create mode 100644 Samples/Navigation/NavigationFirstPage.xaml create mode 100644 Samples/Navigation/NavigationFirstPage.xaml.cs create mode 100644 Samples/Navigation/NavigationSecondPage.xaml create mode 100644 Samples/Navigation/NavigationSecondPage.xaml.cs create mode 100644 Samples/Navigation/NavigationThirdPage.xaml create mode 100644 Samples/Navigation/NavigationThirdPage.xaml.cs create mode 100644 Samples/NavigationSample.cs diff --git a/Ooui.Forms/Exports.cs b/Ooui.Forms/Exports.cs index f928395..f075b31 100644 --- a/Ooui.Forms/Exports.cs +++ b/Ooui.Forms/Exports.cs @@ -25,6 +25,7 @@ using Xamarin.Forms; [assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))] [assembly: ExportRenderer (typeof (TimePicker), typeof (TimePickerRenderer))] [assembly: ExportRenderer (typeof (WebView), typeof (WebViewRenderer))] +[assembly: ExportRenderer(typeof(NavigationPage), typeof(NavigationPageRenderer))] [assembly: ExportImageSourceHandler (typeof (FileImageSource), typeof (FileImageSourceHandler))] [assembly: ExportImageSourceHandler (typeof (StreamImageSource), typeof (StreamImagesourceHandler))] [assembly: ExportImageSourceHandler (typeof (UriImageSource), typeof (ImageLoaderSourceHandler))] diff --git a/Ooui.Forms/Renderers/NavigationPageRenderer.cs b/Ooui.Forms/Renderers/NavigationPageRenderer.cs new file mode 100644 index 0000000..cc6a40c --- /dev/null +++ b/Ooui.Forms/Renderers/NavigationPageRenderer.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Net; +using System.Text; +using Xamarin.Forms; +using Xamarin.Forms.Internals; +using System.Linq; + +namespace Ooui.Forms.Renderers +{ + public class NavigationPageRenderer : VisualElementRenderer + { + private Stack backElementStack = new Stack(); + private Stack forwardElementStack = new Stack(); + // required hack to make sure a browser nav event (back and forward buttons) don't trigger the commands to pushState or go back on the browser AGAIN + bool ignoreNavEventFlag = false; + + private string ns; + private string assemblyName; + + protected override void OnElementChanged(ElementChangedEventArgs e) + { + base.OnElementChanged(e); + + if (e.OldElement != null) + { + e.OldElement.PushRequested -= OnPushRequested; + e.OldElement.PopRequested -= OnPopRequested; + e.OldElement.PopToRootRequested -= OnPopToRootRequested; + e.OldElement.InternalChildren.CollectionChanged -= OnChildrenChanged; + e.OldElement.PropertyChanged -= OnElementPropertyChanged; + } + if (e.NewElement != null) + { + e.NewElement.PushRequested += OnPushRequested; + e.NewElement.PopRequested += OnPopRequested; + e.NewElement.PopToRootRequested += OnPopToRootRequested; + e.NewElement.InternalChildren.CollectionChanged += OnChildrenChanged; + e.NewElement.PropertyChanged += OnElementPropertyChanged; + + GetAssemblyInfoForRootPage(); + } + } + + protected override bool TriggerEventFromMessage(Message message) + { + if (message.TargetId == "window" && message.Key == "hashchange" && message.Value is Newtonsoft.Json.Linq.JObject k) + { + ProcessHash((string)k["hash"]); + return true; + } + else + return base.TriggerEventFromMessage(message); + } + + //signaling doesn't seem to work until child has been inserted into document, so wait for this and then adjust the hash for direct navigation + protected override void OnChildInsertedBefore(Node newChild, Node referenceChild) + { + base.OnChildInsertedBefore(newChild, referenceChild); + + var index = this.Children.IndexOf(newChild); + if (index - 1 >= 0) + (this.Children[index - 1] as Element).Style.Display = "none"; + + this.backElementStack.Push(newChild as DefaultRenderer); + + if (this.ignoreNavEventFlag) + this.ignoreNavEventFlag = false; + else + { + this.forwardElementStack.Clear(); + this.NativeView.Document.Window.Call("history.pushState", null, null, GenerateFullHash()); + } + } + + protected override void OnChildRemoved(Node child) + { + base.OnChildRemoved(child); + + if (this.Children.Count > 0) + (this.Children.Last() as Element).Style.Display = "block"; + + DefaultRenderer popped = this.backElementStack.Pop(); + this.forwardElementStack.Push(child as DefaultRenderer); + + if (this.ignoreNavEventFlag) + this.ignoreNavEventFlag = false; + else + this.NativeView.Document.Window.Call("history.back"); + } + + //only called when user types url manually OR clicks forward or back in the browser OR very beginning of navigation (backStack has first element already though) + private void ProcessHash(string fullHash) + { + IEnumerable browserPageArray; + if (fullHash.Length > 0) + browserPageArray = fullHash.Split('/').Select(x => x == "#" ? this.backElementStack.Last().Element.GetType().Name : x); + else + browserPageArray = new string[] { this.backElementStack.Last().Element.GetType().Name }; + + // if current hash doesn't match up with the current page displayed (which is the first/most recent one in the backHashStack) + if (backElementStack.First().Element.GetType().Name != browserPageArray.Last()) + { + // see if the last hash item is in the backStack (nav backwards) OR in the forward stack (nav forwards) OR not there at all (regenerate stack) + var lastPageFromBackStack = backElementStack.FirstOrDefault(x => x.Element.GetType().Name == browserPageArray.Last());//.Cast<(Page page, string hash)?>().FirstOrDefault(); + var lastPageFromForwardStack = forwardElementStack.FirstOrDefault(x => x.Element.GetType().Name == browserPageArray.Last());//.Cast<(Page page, string hash)?>().FirstOrDefault(); + + // just need to adjust internal stack and page view + // case clicked back button + if (lastPageFromBackStack != null) + { + var peeked = this.backElementStack.Peek(); + this.ignoreNavEventFlag = true; + this.Element.PopAsync(); + } + // case for clicking forward + else if (lastPageFromForwardStack != null) + { + var popped = this.forwardElementStack.Pop(); + this.ignoreNavEventFlag = true; + + // *** This should work, but the result is a page that doesn't format correctly. Instead, we'll have to create new pages from scratch. + //this.Element.PushAsync(popped.Element as Page); + this.Element.PushAsync((Page)Activator.CreateInstance(Type.GetType($"{this.ns}.{popped.Element.GetType().Name}, {this.assemblyName}"))); + } + // case for someone typing in url from scratch with hash + else + { + //assume someone put the url into the browser manually from scratch + //first replace the current state with # only. + this.Document.Window.Call("history.replaceState", null, null, GenerateFullHash()); + foreach (var pageName in browserPageArray.Where(x=> x != backElementStack.Last().Element.GetType().Name)) + { + var pageTypeName = WebUtility.HtmlDecode(pageName); + var pageInstance = Activator.CreateInstance(Type.GetType($"{this.ns}.{pageTypeName}, {this.assemblyName}")); + this.Element.PushAsync(pageInstance as Page); + } + } + } + } + + // Reflection needs more than just the name to create an instance from a string. + private void GetAssemblyInfoForRootPage() + { + if (this.Element.Navigation.NavigationStack.Count > 0) + { + var page = this.Element.Navigation.NavigationStack.Last(); + var type = page.GetType(); + this.ns = type.Namespace; + this.assemblyName = type.Assembly.FullName; + } + } + + // This is where you would normally set back button state based on what's in the stack. + private void OnChildrenChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // Not needed for browser... back button is always available. + } + + private void OnPopToRootRequested(object sender, NavigationRequestedEventArgs e) + { + e.Realize = true; + } + + private void OnPopRequested(object sender, NavigationRequestedEventArgs e) + { + e.Realize = true; + } + + // This is where you would draw the new contents. + private void OnPushRequested(object sender, NavigationRequestedEventArgs e) + { + e.Realize = true; + } + + private string GenerateFullHash() + { + string hashString = ""; + bool started = false; + foreach (var i in this.backElementStack.Reverse()) + { + if (started) + hashString += "/" + i.Element.GetType().Name; + else + { + started = true; + hashString += "#"; + } + } + return hashString; + } + + } +} diff --git a/Ooui/Client.js b/Ooui/Client.js index c651674..8ee333c 100644 --- a/Ooui/Client.js +++ b/Ooui/Client.js @@ -59,6 +59,20 @@ function saveSize (s) { setCookie ("oouiWindowHeight", s.height, 7); } +function initializeNavigation() { + monitorHashChanged(); + const em = { + m: "event", + id: "window", + k: "hashchange", + v: window.location + }; + saveSize(em.v); + const ems = JSON.stringify(em); + send(ems); + if (debug) console.log("Event", em); +} + // Main entrypoint function ooui (rootElementPath) { @@ -77,6 +91,7 @@ function ooui (rootElementPath) { socket.addEventListener ("open", function (event) { console.log ("Web socket opened"); + initializeNavigation(); }); socket.addEventListener ("error", function (event) { @@ -109,9 +124,27 @@ function oouiWasm (mainAsmName, mainNamespace, mainClassName, mainMethodName, as Module.entryPoint = { "a": mainAsmName, "n": mainNamespace, "t": mainClassName, "m": mainMethodName }; Module.assemblies = assemblies; + initializeNavigation(); monitorSizeChanges (1000/30); } +function monitorHashChanged() { + function hashChangeHandler() { + const em = { + m: "event", + id: "window", + k: "hashchange", + v: window.location + }; + saveSize(em.v); + const ems = JSON.stringify(em); + send(ems); + if (debug) console.log("Event", em); + } + + window.addEventListener("hashchange", hashChangeHandler, false); +} + function monitorSizeChanges (millis) { var resizeTimeout; @@ -213,6 +246,17 @@ function msgRemAttr (m) { if (debug) console.log ("RemAttr", node, m.k); } +function getCallerProperty(target, accessorStr) { + const arr = accessorStr.split('.'); + var caller = target; + var property = target; + arr.forEach(function (v) { + caller = property; + property = caller[v]; + }); + return [caller, property]; +} + function msgCall (m) { const id = m.id; const node = getNode (id); @@ -227,9 +271,10 @@ function msgCall (m) { target.removeChild (target.firstChild); delete hasText[id]; } - const f = target[m.k]; + //const f = target[m.k]; + const f = getCallerProperty(target, m.k); if (debug) console.log ("Call", node, f, m.v); - const r = f.apply (target, m.v); + const r = f[1].apply (f[0], m.v); if (typeof m.rid === 'string' || m.rid instanceof String) { nodes[m.rid] = r; } diff --git a/Samples/Navigation/NavigationFirstPage.xaml b/Samples/Navigation/NavigationFirstPage.xaml new file mode 100644 index 0000000..577480b --- /dev/null +++ b/Samples/Navigation/NavigationFirstPage.xaml @@ -0,0 +1,15 @@ + + + + +