Merge pull request #168 from limefrogyank/minimal

(mostly) Implementation of NavigationPage
This commit is contained in:
Frank A. Krueger 2018-08-25 12:43:06 -04:00 committed by GitHub
commit 6a2ee75782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 403 additions and 2 deletions

View File

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

View File

@ -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<NavigationPage>
{
private Stack<DefaultRenderer> backElementStack = new Stack<DefaultRenderer>();
private Stack<DefaultRenderer> forwardElementStack = new Stack<DefaultRenderer>();
// 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<NavigationPage> 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<string> 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;
}
}
}

View File

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

View File

@ -0,0 +1,15 @@
<?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.Navigation.NavigationFirstPage"
Title="The First Page">
<ContentPage.Content>
<StackLayout>
<Label Text="This is the first page."
VerticalOptions="Center"
HorizontalOptions="CenterAndExpand" />
<Button Clicked="Button_Clicked"
Text="Navigate to Second Page" />
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Samples.Navigation
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NavigationFirstPage : ContentPage
{
public NavigationFirstPage ()
{
InitializeComponent ();
}
private void Button_Clicked(object sender, EventArgs e)
{
(this.Parent as NavigationPage).PushAsync(new Navigation.NavigationSecondPage());
}
}
}

View File

@ -0,0 +1,18 @@
<?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.Navigation.NavigationSecondPage"
Title="The Second Page">
<ContentPage.Content>
<StackLayout>
<Label Text="This is the second page."
VerticalOptions="Center"
HorizontalOptions="CenterAndExpand" />
<Button Clicked="BackButton_Clicked"
Text="Go back to First Page" />
<Button Clicked="Button_Clicked"
Text="Navigate to Third Page" />
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Samples.Navigation
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NavigationSecondPage : ContentPage
{
public NavigationSecondPage ()
{
InitializeComponent ();
}
private void BackButton_Clicked(object sender, EventArgs e)
{
(this.Parent as NavigationPage).PopAsync();
}
private void Button_Clicked(object sender, EventArgs e)
{
(this.Parent as NavigationPage).PushAsync(new Navigation.NavigationThirdPage());
}
}
}

View File

@ -0,0 +1,17 @@
<?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.Navigation.NavigationThirdPage"
Title="The Third Page">
<ContentPage.Content>
<StackLayout>
<Label Text="This is the third page."
VerticalOptions="Center"
HorizontalOptions="CenterAndExpand" />
<Button Clicked="BackButton_Clicked"
Text="Go back to Second Page" />
<Button Clicked="RootButton_Clicked"
Text="Go back to Root (1st page)" />
</StackLayout>
</ContentPage.Content>
</ContentPage>

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Samples.Navigation
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class NavigationThirdPage : ContentPage
{
public NavigationThirdPage ()
{
InitializeComponent ();
}
private void BackButton_Clicked(object sender, EventArgs e)
{
(this.Parent as NavigationPage).PopAsync(false);
}
private void RootButton_Clicked(object sender, EventArgs e)
{
(this.Parent as NavigationPage).PopToRootAsync(false);
}
}
}

View File

@ -0,0 +1,18 @@

using Ooui;
using Xamarin.Forms;
namespace Samples.Navigation
{
public class NavigationSample : ISample
{
public string Title => "Xamarin.Forms Navigation XAML";
public Ooui.Element CreateElement()
{
var page = new Navigation.NavigationFirstPage();
var root = new NavigationPage(page);
return root.GetOouiElement();
}
}
}