diff --git a/apps/perf-tests/NavigationTest/app.ts b/apps/perf-tests/NavigationTest/app.ts index a6e38a07f..91949d903 100644 --- a/apps/perf-tests/NavigationTest/app.ts +++ b/apps/perf-tests/NavigationTest/app.ts @@ -1,5 +1,4 @@ import application = require("application"); -import frameModule = require("ui/frame"); import navPageModule = require("../nav-page"); import trace = require("trace"); @@ -7,14 +6,13 @@ trace.enable(); trace.setCategories(trace.categories.concat( trace.categories.NativeLifecycle , trace.categories.Navigation - , trace.categories.ViewHierarchy - , trace.categories.VisualTreeEvents - )); +)); -application.onLaunch = function (context) { - var frame = new frameModule.Frame(); - var pageFactory = function () { +application.mainEntry = { + create: function () { return new navPageModule.NavPage(0); - }; - frame.navigate(pageFactory); -} + } + //backstackVisible: false, + //clearHistory: true +}; +application.start(); diff --git a/apps/perf-tests/nav-page.ts b/apps/perf-tests/nav-page.ts index 7bc70a567..118196462 100644 --- a/apps/perf-tests/nav-page.ts +++ b/apps/perf-tests/nav-page.ts @@ -6,11 +6,14 @@ import labelModule = require("ui/label"); import buttonModule = require("ui/button"); import textFieldModule = require("ui/text-field"); import enums = require("ui/enums"); +import switchModule = require("ui/switch"); export class NavPage extends pagesModule.Page implements definition.ControlsPage { constructor(id: number) { super(); + this.id = "NavPage " + id; + var stackLayout = new stackLayoutModule.StackLayout(); stackLayout.orientation = enums.Orientation.vertical; @@ -21,6 +24,11 @@ export class NavPage extends pagesModule.Page implements definition.ControlsPage }); stackLayout.addChild(goBackButton); + this.on(pagesModule.Page.navigatedToEvent, function () { + //console.log("Navigated to NavPage " + id + "; backStack.length: " + frameModule.topmost().backStack.length); + goBackButton.isEnabled = frameModule.topmost().canGoBack(); + }); + var stateLabel = new labelModule.Label(); stateLabel.text = "NavPage " + id; stackLayout.addChild(stateLabel); @@ -39,13 +47,37 @@ export class NavPage extends pagesModule.Page implements definition.ControlsPage }); stackLayout.addChild(changeStateButton); + var optionsLayout = new stackLayoutModule.StackLayout(); + + var addToBackStackLabel = new labelModule.Label(); + addToBackStackLabel.text = "backStackVisible"; + optionsLayout.addChild(addToBackStackLabel); + + var addToBackStackSwitch = new switchModule.Switch(); + addToBackStackSwitch.checked = true; + optionsLayout.addChild(addToBackStackSwitch); + + var clearHistoryLabel = new labelModule.Label(); + clearHistoryLabel.text = "clearHistory"; + optionsLayout.addChild(clearHistoryLabel); + + var clearHistorySwitch = new switchModule.Switch(); + clearHistorySwitch.checked = false; + optionsLayout.addChild(clearHistorySwitch); + + stackLayout.addChild(optionsLayout); + var forwardButton = new buttonModule.Button(); forwardButton.text = "->"; forwardButton.on(buttonModule.Button.tapEvent, function () { var pageFactory = function () { return new NavPage(id + 1); }; - frameModule.topmost().navigate(pageFactory); + frameModule.topmost().navigate({ + create: pageFactory, + backstackVisible: addToBackStackSwitch.checked, + clearHistory: clearHistorySwitch.checked + }); }); stackLayout.addChild(forwardButton); diff --git a/apps/tests/navigation-tests.ts b/apps/tests/navigation-tests.ts index 5424015a7..bf7bc4385 100644 --- a/apps/tests/navigation-tests.ts +++ b/apps/tests/navigation-tests.ts @@ -29,4 +29,37 @@ export var test_backstackVisible = function() { frame.topmost().goBack(); TKUnit.waitUntilReady(() => { return frame.topmost().currentPage === mainTestPage; }); -} \ No newline at end of file +} + +// Clearing the history messes up the tests app. +//export var test_ClearHistory = function () { +// var pageFactory = function (): pageModule.Page { +// return new pageModule.Page(); +// }; + +// var mainTestPage = frame.topmost().currentPage; +// var currentPage: pageModule.Page; + +// currentPage = frame.topmost().currentPage; +// frame.topmost().navigate({ create: pageFactory }); +// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; }); + +// currentPage = frame.topmost().currentPage; +// frame.topmost().navigate({ create: pageFactory }); +// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; }); + +// currentPage = frame.topmost().currentPage; +// frame.topmost().navigate({ create: pageFactory }); +// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; }); + +// TKUnit.assert(frame.topmost().canGoBack(), "Frame should be able to go back."); +// TKUnit.assert(frame.topmost().backStack.length === 3, "Back stack should have 3 entries."); + +// // Navigate with clear history. +// currentPage = frame.topmost().currentPage; +// frame.topmost().navigate({ create: pageFactory, clearHistory: true }); +// TKUnit.waitUntilReady(() => { return frame.topmost().currentPage !== currentPage; }); + +// TKUnit.assert(!frame.topmost().canGoBack(), "Frame should NOT be able to go back."); +// TKUnit.assert(frame.topmost().backStack.length === 0, "Back stack should have 0 entries."); +//} \ No newline at end of file diff --git a/ui/frame/frame-common.ts b/ui/frame/frame-common.ts index 6450fc2b5..57851256b 100644 --- a/ui/frame/frame-common.ts +++ b/ui/frame/frame-common.ts @@ -184,7 +184,8 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { var entry = this._navigationQueue[0].entry; var currentNavigationPage = entry.resolvedPage; if (page !== currentNavigationPage) { - throw new Error("Corrupted navigation stack."); + console.trace(); + throw new Error(`Corrupted navigation stack; page: ${page.id}; currentNavigationPage: ${currentNavigationPage.id}`); } // remove completed operation. @@ -224,7 +225,10 @@ export class Frame extends view.CustomLayoutView implements definition.Frame { var navContext = navigationContext.entry; this._onNavigatingTo(navContext); - if (this._isEntryBackstackVisible(this._currentEntry)) { + if (navigationContext.entry.entry.clearHistory) { + this._backStack.length = 0; + } + else if (this._isEntryBackstackVisible(this._currentEntry)) { this._backStack.push(this._currentEntry); } diff --git a/ui/frame/frame.android.ts b/ui/frame/frame.android.ts index 1dadc8c69..605815277 100644 --- a/ui/frame/frame.android.ts +++ b/ui/frame/frame.android.ts @@ -15,6 +15,7 @@ var INTENT_EXTRA = "com.tns.activity"; var ANDROID_FRAME = "android_frame"; var BACKSTACK_TAG = "_backstackTag"; var NAV_DEPTH = "_navDepth"; +var CLEARING_HISTORY = "_clearingHistory"; var navDepth = -1; @@ -32,17 +33,23 @@ class PageFragmentBody extends android.app.Fragment { onAttach(activity: android.app.Activity) { super.onAttach(activity); - trace.write(this.getTag() + ".onAttach();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onAttach();", trace.categories.NativeLifecycle); } onCreate(savedInstanceState: android.os.Bundle) { super.onCreate(savedInstanceState); - trace.write(this.getTag() + ".onCreate(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onCreate(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); super.setHasOptionsMenu(true); } onCreateView(inflater: android.view.LayoutInflater, container: android.view.ViewGroup, savedInstanceState: android.os.Bundle): android.view.View { - trace.write(this.getTag() + ".onCreateView(); container: " + container + "; savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onCreateView(); container: " + container + "; savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); + + if (this[CLEARING_HISTORY]) { + trace.write(`${this.toString() } wants to create a view, but we are currently clearing history. Returning null.`, trace.categories.NativeLifecycle); + return null; + } + var entry: definition.BackstackEntry = this.entry; var page: pages.Page = entry.resolvedPage; @@ -58,13 +65,13 @@ class PageFragmentBody extends android.app.Fragment { onFragmentShown(this); } - trace.write(this.getTag() + ".onCreateView(); nativeView: " + page._nativeView, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onCreateView(); nativeView: " + page._nativeView, trace.categories.NativeLifecycle); return page._nativeView; } onHiddenChanged(hidden: boolean) { super.onHiddenChanged(hidden); - trace.write(this.getTag() + ".onHiddenChanged(); hidden: " + hidden, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onHiddenChanged(); hidden: " + hidden, trace.categories.NativeLifecycle); if (hidden) { onFragmentHidden(this); @@ -76,12 +83,12 @@ class PageFragmentBody extends android.app.Fragment { onActivityCreated(savedInstanceState: android.os.Bundle) { super.onActivityCreated(savedInstanceState); - trace.write(this.getTag() + ".onActivityCreated(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onActivityCreated(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); } onSaveInstanceState(outState: android.os.Bundle) { super.onSaveInstanceState(outState); - trace.write(this.getTag() + ".onSaveInstanceState();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onSaveInstanceState();", trace.categories.NativeLifecycle); if (this.isHidden()) { outState.putBoolean(HIDDEN, true); @@ -90,39 +97,39 @@ class PageFragmentBody extends android.app.Fragment { onViewStateRestored(savedInstanceState: android.os.Bundle) { super.onViewStateRestored(savedInstanceState); - trace.write(this.getTag() + ".onViewStateRestored(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onViewStateRestored(); savedInstanceState: " + savedInstanceState, trace.categories.NativeLifecycle); } onStart() { super.onStart(); - trace.write(this.getTag() + ".onStart();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onStart();", trace.categories.NativeLifecycle); } onResume() { super.onResume(); - trace.write(this.getTag() + ".onResume();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onResume();", trace.categories.NativeLifecycle); } onPause() { super.onPause(); - trace.write(this.getTag() + ".onPause();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onPause();", trace.categories.NativeLifecycle); } onStop() { super.onStop(); - trace.write(this.getTag() + ".onStop();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onStop();", trace.categories.NativeLifecycle); } onDestroyView() { super.onDestroyView(); - trace.write(this.getTag() + ".onDestroyView();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onDestroyView();", trace.categories.NativeLifecycle); onFragmentHidden(this); } onDestroy() { super.onDestroy(); - trace.write(this.getTag() + ".onDestroy();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onDestroy();", trace.categories.NativeLifecycle); // Explicitly free resources to allow Java Garbage collector to release resources associated with JavaScript implementations - e.g. large image files. // Although we hint the V8 with the externally allocated memory, synchronization between the two GCs is not deterministic without an explicit call. @@ -132,11 +139,20 @@ class PageFragmentBody extends android.app.Fragment { onDetach() { super.onDetach(); - trace.write(this.getTag() + ".onDetach();", trace.categories.NativeLifecycle); + trace.write(this.toString() + ".onDetach();", trace.categories.NativeLifecycle); + } + + public toString() { + return `${this.getTag() }[${this.entry.resolvedPage.id}]`; } } function onFragmentShown(fragment: PageFragmentBody) { + if (fragment[CLEARING_HISTORY]) { + trace.write(`${fragment.toString() } has been shown, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle); + return null; + } + // TODO: consider putting entry and page in queue so we can safely extract them here. Pass the index of current navigation and extract it from here. // After extracting navigation info - remove this index from navigation stack. var frame = fragment.frame; @@ -152,6 +168,11 @@ function onFragmentShown(fragment: PageFragmentBody) { } function onFragmentHidden(fragment: PageFragmentBody) { + if (fragment[CLEARING_HISTORY]) { + trace.write(`${fragment.toString() } has been hidden, but we are currently clearing history. Returning.`, trace.categories.NativeLifecycle); + return null; + } + var entry: definition.BackstackEntry = fragment.entry; var page: pages.Page = entry.resolvedPage; // This might be a second call if the fragment is hidden and then destroyed. @@ -193,6 +214,11 @@ export class Frame extends frameCommon.Frame { } public _navigateCore(backstackEntry: definition.BackstackEntry) { + trace.write(`_navigateCore; id: ${backstackEntry.resolvedPage.id}; backstackVisible: ${this._isEntryBackstackVisible(backstackEntry)}; clearHistory: ${backstackEntry.entry.clearHistory};`, trace.categories.Navigation); + + //this._printFrameBackStack(); + //this._printNativeBackStack(); + var activity = this._android.activity; if (!activity) { // We do not have an Activity yet associated. In this case we have two execution paths: @@ -207,9 +233,36 @@ export class Frame extends frameCommon.Frame { return; } + var manager = activity.getFragmentManager(); + + // Clear history + if (backstackEntry.entry.clearHistory && !this._isFirstNavigation) { + var i = manager.getBackStackEntryCount() - 1; + var fragment: android.app.Fragment; + while (i >= 0) { + fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); + trace.write(`${fragment.toString()}[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle); + fragment[CLEARING_HISTORY] = true; + } + + // Remember that the current fragment has never been added to the backStack, so mark it as well. + if (this.currentPage) { + fragment = manager.findFragmentByTag(this.currentPage[TAG]); + if (fragment) { + fragment[CLEARING_HISTORY] = true; + trace.write(`${fragment.toString() }[CLEARING_HISTORY] = true;`, trace.categories.NativeLifecycle); + } + } + + var firstEntryName = manager.getBackStackEntryAt(0).getName(); + trace.write(`manager.popBackStack(${firstEntryName}, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE);`, trace.categories.NativeLifecycle); + manager.popBackStack(firstEntryName, android.app.FragmentManager.POP_BACK_STACK_INCLUSIVE); + this._currentEntry = null; + navDepth = -1; + } + navDepth++; - var manager = activity.getFragmentManager(); var fragmentTransaction = manager.beginTransaction(); var newFragmentTag = "fragment" + navDepth; @@ -227,7 +280,7 @@ export class Frame extends frameCommon.Frame { trace.write("fragmentTransaction.add(" + this.containerViewId + ", " + newFragment + ", " + newFragmentTag + ");", trace.categories.NativeLifecycle); } else { - if (this.android.cachePagesOnNavigate) { + if (this.android.cachePagesOnNavigate && !backstackEntry.entry.clearHistory) { var currentFragmentTag = this.currentPage[TAG]; var currentFragment = manager.findFragmentByTag(currentFragmentTag); if (currentFragment) { @@ -246,7 +299,10 @@ export class Frame extends frameCommon.Frame { trace.write("fragmentTransaction.replace(" + this.containerViewId + ", " + newFragment + ", " + newFragmentTag + ");", trace.categories.NativeLifecycle); } - if (this.backStack.length > 0) { + // Add to backStack if needed. + if (this.backStack.length > 0 && + this._currentEntry && + this._isEntryBackstackVisible(this._currentEntry)) { // We add each entry in the backstack to avoid the "Stack corrupted" mismatch var backstackTag = this._currentEntry[BACKSTACK_TAG]; fragmentTransaction.addToBackStack(backstackTag); @@ -271,6 +327,11 @@ export class Frame extends frameCommon.Frame { fragmentTransaction.commit(); trace.write("fragmentTransaction.commit();", trace.categories.NativeLifecycle); + + //setTimeout(() => { + // this._printFrameBackStack(); + // this._printNativeBackStack(); + //}, 100); } public _goBackCore(backstackEntry: definition.BackstackEntry) { @@ -323,6 +384,32 @@ export class Frame extends frameCommon.Frame { public _clearAndroidReference() { // we should keep the reference to underlying native object, since frame can contain many pages. } + + public _printNativeBackStack() { + if (!this._android.activity) { + return; + } + var manager = this._android.activity.getFragmentManager(); + var length = manager.getBackStackEntryCount(); + var i = length - 1; + console.log("---------------------------"); + console.log("Fragment Manager Back Stack (" + length + ")"); + while (i >= 0) { + var fragment = manager.findFragmentByTag(manager.getBackStackEntryAt(i--).getName()); + console.log("[ " + fragment.toString() + " ]"); + } + } + + public _printFrameBackStack() { + var length = this.backStack.length; + var i = length - 1; + console.log("---------------------------"); + console.log("Frame Back Stack (" + length + ")"); + while (i >= 0) { + var backstackEntry = this.backStack[i--]; + console.log("[ " + backstackEntry.resolvedPage.id + " ]"); + } + } } var NativeActivity = { diff --git a/ui/frame/frame.d.ts b/ui/frame/frame.d.ts index c0aaf8274..3468ff24a 100644 --- a/ui/frame/frame.d.ts +++ b/ui/frame/frame.d.ts @@ -151,6 +151,11 @@ declare module "ui/frame" { * If the parameter is set to false then the Page will be displayed but once navigated from it will not be able to be navigated back to. */ backstackVisible?: boolean; + + /** + * True to clear the navigation history, false otherwise. Very useful when navigating away from login pages. + */ + clearHistory?: boolean; } /** diff --git a/ui/frame/frame.ios.ts b/ui/frame/frame.ios.ts index 805d1bb96..d887af61e 100644 --- a/ui/frame/frame.ios.ts +++ b/ui/frame/frame.ios.ts @@ -45,7 +45,7 @@ export class Frame extends frameCommon.Frame { } public _navigateCore(backstackEntry: definition.BackstackEntry) { - var viewController = backstackEntry.resolvedPage.ios; + var viewController: UIViewController = backstackEntry.resolvedPage.ios; if (!viewController) { throw new Error("Required page does have an viewController created."); } @@ -63,30 +63,48 @@ export class Frame extends frameCommon.Frame { this._updateActionBar(backstackEntry.resolvedPage); - if (this._currentEntry && !this._isEntryBackstackVisible(this._currentEntry)) { + // First navigation. + if (!this._currentEntry) { + this._ios.controller.pushViewControllerAnimated(viewController, animated); + trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated(newController) depth = " + navDepth, trace.categories.Navigation); + return; + } + + // We should clear the entire history. + if (backstackEntry.entry.clearHistory) { + viewController.navigationItem.hidesBackButton = true; + var newControllers = NSMutableArray.alloc().initWithCapacity(1); + newControllers.addObject(viewController); + this._ios.controller.setViewControllersAnimated(newControllers, animated); + trace.write("Frame<" + this._domId + ">.setViewControllersAnimated([newController]) depth = " + navDepth, trace.categories.Navigation); + return; + + } + + // We should hide the current entry from the back stack. + if (!this._isEntryBackstackVisible(this._currentEntry)) { var newControllers = NSMutableArray.alloc().initWithArray(this._ios.controller.viewControllers); if (newControllers.count === 0) { throw new Error("Wrong controllers count."); } - var newController: UIViewController = backstackEntry.resolvedPage.ios; - // the code below fixes a phantom animation that appears on the Back button in this case // TODO: investigate why the animation happens at first place before working around it - newController.navigationItem.hidesBackButton = this.backStack.length === 0; + viewController.navigationItem.hidesBackButton = this.backStack.length === 0; // swap the top entry with the new one newControllers.removeLastObject(); - newControllers.addObject(newController); + newControllers.addObject(viewController); // replace the controllers instead of pushing directly this._ios.controller.setViewControllersAnimated(newControllers, animated); + trace.write("Frame<" + this._domId + ">.setViewControllersAnimated([originalControllers - lastController + newController]) depth = " + navDepth, trace.categories.Navigation); + return; } - else { - this._ios.controller.pushViewControllerAnimated(viewController, animated); - } - - trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated depth = " + navDepth, trace.categories.Navigation); + + // General case. + this._ios.controller.pushViewControllerAnimated(viewController, animated); + trace.write("Frame<" + this._domId + ">.pushViewControllerAnimated(newController) depth = " + navDepth, trace.categories.Navigation); } public _goBackCore(backstackEntry: definition.BackstackEntry) {