2018-05-20 20:42:22 +00:00
using System ;
using System.Collections.Generic ;
using System.Collections.Specialized ;
using System.Net ;
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 )
{
2019-03-21 22:01:16 +00:00
#pragma warning disable CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
e . Realize = true ;
2019-03-21 22:01:16 +00:00
#pragma warning restore CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
}
private void OnPopRequested ( object sender , NavigationRequestedEventArgs e )
{
2019-03-21 22:01:16 +00:00
#pragma warning disable CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
e . Realize = true ;
2019-03-21 22:01:16 +00:00
#pragma warning restore CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
}
// This is where you would draw the new contents.
private void OnPushRequested ( object sender , NavigationRequestedEventArgs e )
{
2019-03-21 22:01:16 +00:00
#pragma warning disable CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
e . Realize = true ;
2019-03-21 22:01:16 +00:00
#pragma warning restore CS0618 // Type or member is obsolete
2018-05-20 20:42:22 +00:00
}
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 ;
}
}
}