Apache Cordova
  1. Apache Cordova
  2. CB-520

WP7 Certification and the Back Button

    Details

    • Type: Bug Bug
    • Status: Closed
    • Priority: Critical Critical
    • Resolution: Fixed
    • Affects Version/s: 1.6.1
    • Fix Version/s: 1.8.0
    • Component/s: WP7
    • Labels:
      None
    • Environment:

      VS.NET 2010 and WP7.1 emulator

      Description

      I tried submitting my PG 1.5 app to the Windows Marketplace and it was rejected due to WP7's requirements for the Back Button. I upgraded to PG 1.6.1 and I am inspecting how it works with the hardware back button. It seems to work much better, but my app is designed so that it has a soft back button in the app in various places, and on WP7 the user can always tap the hardware back button. I am trying to use navigator.app.historyBack and it appears to work, but it does something slightly different than actually tapping the hardware back button does. I have also tried using window.history.back and that works different yet. I am using JQueryMobile 1.1.0 and so I wind up doing quite a lot of $.mobile.changePage calls to #Page id's, and because I use multiple .html files I also need to do some rel="external" links or window.location.href= calls. I think my needs are similar or the same as those of other WP7 developers. Here is what currently happens with PG 1.6.1 in a simple Page1/Page2 JQueryMobile app when using the hardware back button versus using navigator.app.historyBack, vs. window.history.back:

      SCENARIO #1 - using hardware BackButton only
      Page 1 links to Page 2 using $.mobile.changePage("#Page2");
      BackButton tap - goes back to Page1 but page is requested again and reloaded from scratch which is slow and the user loses any form data they had entered.
      BackButton tap - exits app (great!)

      SCENARIO #2 - hardware BackButton and navigator.app.backHistory
      Page 1 links to Page 2 using $.mobile.changePage("#Page2");
      navigator.app.backHistory(); goes back to Page1 served from cache, which is fast and form data is preserved.
      BackButton tap: nothing happens
      BackButton tap: Page1 is reloaded from scratch
      BackButton tap: exits app

      SCENARIO #3 - hardware backButton and window.history.back
      Page 1 links to Page 2 using $.mobile.changePage("#Page2");
      window.history.back(); goes back to Page1 served from cache.
      BackButton tap: Page1 is reloaded from scratch (DOH!)
      BackButton tap: exits app

      My Observations:

      1) The hardware back button does not use the cached page - it reloads/re-requests the page. This is kind of a drag but I think we have to just go with this because it is the behavior that the Marketplace testers will be expecting and validating.

      2) window.history.back() is giving better results than navigator.app.backHistory, but still not the same as the hardware back button. I think apps will fail Marketplace certification if they use either of these approaches for soft back buttons.

      3) When going back to an external page (as opposed to a JQueryMobile #pageID), window.history.back works but navigator.app.backHistory does not seem to do anything at all. I say window.history.back "works" but it is still the same result as in Scenario #3 above, which is not good.

        Activity

        Hide
        Alan Neveu added a comment - - edited

        I wound up modifying CordovaView.xaml.cs in WP7CordovaClassLib. It turns out WP7 apps really need to be able to have more control over the history stack in order to comply with Marketplace tech cert requirements. Here is the source I wound up settling on for CordovaView.xaml.cs... Note the enhancements to void GapBrowser_ScriptNotify, and also to void page_BackKeyPress.

        /*
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
        */

        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Net;
        using System.Windows;
        using System.Windows.Controls;
        using System.Windows.Documents;
        using System.Windows.Input;
        using System.Windows.Media;
        using System.Windows.Media.Animation;
        using System.Windows.Shapes;
        using Microsoft.Phone.Controls;
        using System.IO.IsolatedStorage;
        using System.Windows.Resources;
        using System.Windows.Interop;
        using System.Runtime.Serialization.Json;
        using System.IO;
        using System.ComponentModel;
        using System.Xml.Linq;
        using WP7CordovaClassLib.Cordova.Commands;
        using System.Diagnostics;
        using System.Text;
        using WP7CordovaClassLib.Cordova;
        using System.Threading;
        using Microsoft.Phone.Shell;

        namespace WP7CordovaClassLib
        {
        public partial class CordovaView : UserControl
        {

        /// <summary>
        /// Indicates whether web control has been loaded and no additional initialization is needed.
        /// Prevents data clearing during page transitions.
        /// </summary>
        private bool IsBrowserInitialized = false;

        /// <summary>
        /// Set when the user attaches a back button handler inside the WebBrowser
        /// </summary>
        private bool OverrideBackButton = false;

        /// <summary>
        /// Used for keeping track of our history
        /// </summary>
        private Stack<Uri> history = new Stack<Uri>();
        private bool IsBackButtonPressed = false;

        private static string AppRoot = "/app/";

        /// <summary>
        /// Handles native api calls
        /// </summary>
        private NativeExecution nativeExecution;

        protected DOMStorageHelper domStorageHelper;
        protected OrientationHelper orientationHelper;

        public System.Windows.Controls.Grid _LayoutRoot
        {
        get

        { return ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot"))); }

        }

        public WebBrowser Browser
        {
        get

        { return CordovaBrowser; }

        }

        /*

        • Setting StartPageUri only has an effect if called before the view is loaded.
          **/
          protected Uri _startPageUri = null;
          public Uri StartPageUri
          {
          get
          Unknown macro: { if (_startPageUri == null) { // default return new Uri( AppRoot + "www/index.html", UriKind.Relative); } else { return _startPageUri; } }

          set

          Unknown macro: { if (!this.IsBrowserInitialized) { _startPageUri = value; } }

          }

        public CordovaView()
        {

        InitializeComponent();

        if (DesignerProperties.IsInDesignTool)

        { return; }


        StartupMode mode = PhoneApplicationService.Current.StartupMode;

        if (mode == StartupMode.Launch)
        { PhoneApplicationService service = PhoneApplicationService.Current; service.Activated += new EventHandler<Microsoft.Phone.Shell.ActivatedEventArgs>(AppActivated); service.Launching += new EventHandler<LaunchingEventArgs>(AppLaunching); service.Deactivated += new EventHandler<DeactivatedEventArgs>(AppDeactivated); service.Closing += new EventHandler<ClosingEventArgs>(AppClosing); }
        else
        {

        }

        // initializes native execution logic
        this.nativeExecution = new NativeExecution(ref this.CordovaBrowser);
        }



        void AppClosing(object sender, ClosingEventArgs e)
        { Debug.WriteLine("AppClosing"); }

        void AppDeactivated(object sender, DeactivatedEventArgs e)
        {
        Debug.WriteLine("AppDeactivated");

        try
        {
        CordovaBrowser.InvokeScript("CordovaCommandResult", new string[] { "pause" });
        }
        catch (Exception)
        { Debug.WriteLine("Pause event error"); }
        }

        void AppLaunching(object sender, LaunchingEventArgs e)
        { Debug.WriteLine("AppLaunching"); }

        void AppActivated(object sender, Microsoft.Phone.Shell.ActivatedEventArgs e)
        {
        Debug.WriteLine("AppActivated");
        try
        {
        CordovaBrowser.InvokeScript("CordovaCommandResult", new string[] { "resume" });
        }
        catch (Exception)
        { Debug.WriteLine("Resume event error"); }
        }

        void GapBrowser_Loaded(object sender, RoutedEventArgs e)
        {
        if (DesignerProperties.IsInDesignTool)
        { return; }

        // prevents refreshing web control to initial state during pages transitions
        if (this.IsBrowserInitialized) return;

        this.domStorageHelper = new DOMStorageHelper(this.CordovaBrowser);

        try
        {

        // Before we possibly clean the ISO-Store, we need to grab our generated UUID, so we can rewrite it after.
        string deviceUUID = "";

        using (IsolatedStorageFile appStorage = IsolatedStorageFile.GetUserStoreForApplication())
        {
        try
        {
        IsolatedStorageFileStream fileStream = new IsolatedStorageFileStream("DeviceID.txt", FileMode.Open, FileAccess.Read, appStorage);

        using (StreamReader reader = new StreamReader(fileStream))

        { deviceUUID = reader.ReadLine(); }

        }
        catch (Exception /ex/)

        { deviceUUID = Guid.NewGuid().ToString(); }

        Debug.WriteLine("Updating IsolatedStorage for APP:DeviceID :: " + deviceUUID);
        IsolatedStorageFileStream file = new IsolatedStorageFileStream("DeviceID.txt", FileMode.Create, FileAccess.Write, appStorage);
        using (StreamWriter writeFile = new StreamWriter(file))

        { writeFile.WriteLine(deviceUUID); writeFile.Close(); }

        }

        StreamResourceInfo streamInfo = Application.GetResourceStream(new Uri("CordovaSourceDictionary.xml", UriKind.Relative));

        if (streamInfo != null)
        {
        StreamReader sr = new StreamReader(streamInfo.Stream);
        //This will Read Keys Collection for the xml file

        XDocument document = XDocument.Parse(sr.ReadToEnd());

        var files = from results in document.Descendants("FilePath")
        select new

        { path = (string)results.Attribute("Value") }

        ;
        StreamResourceInfo fileResourceStreamInfo;

        using (IsolatedStorageFile appStorage = IsolatedStorageFile.GetUserStoreForApplication())
        {

        foreach (var file in files)
        {
        fileResourceStreamInfo = Application.GetResourceStream(new Uri(file.path, UriKind.Relative));

        if (fileResourceStreamInfo != null)
        {
        using (BinaryReader br = new BinaryReader(fileResourceStreamInfo.Stream))
        {
        byte[] data = br.ReadBytes((int)fileResourceStreamInfo.Stream.Length);

        string strBaseDir = AppRoot + file.path.Substring(0, file.path.LastIndexOf(System.IO.Path.DirectorySeparatorChar));

        if(!appStorage.DirectoryExists(strBaseDir))

        { //Debug.WriteLine("Creating Directory :: " + strBaseDir); appStorage.CreateDirectory(strBaseDir); }

        // This will truncate/overwrite an existing file, or
        using (IsolatedStorageFileStream outFile = appStorage.OpenFile(AppRoot + file.path, FileMode.Create))
        {
        Debug.WriteLine("Writing data for " + AppRoot + file.path + " and length = " + data.Length);
        using (var writer = new BinaryWriter(outFile))

        { writer.Write(data); }

        }
        }
        }
        else

        { Debug.WriteLine("Failed to write file :: " + file.path + " did you forget to add it to the project?"); }

        }
        }
        }

        CordovaBrowser.Navigate(StartPageUri);
        IsBrowserInitialized = true;
        AttachHardwareButtonHandlers();
        }
        catch (Exception ex)
        {
        Debug.WriteLine("Exception in GapBrowser_Loaded ::

        {0}

        ", ex.Message);
        }
        }

        void AttachHardwareButtonHandlers()
        {
        PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame;
        if (frame != null)
        {
        PhoneApplicationPage page = frame.Content as PhoneApplicationPage;

        if (page != null)

        { page.BackKeyPress += new EventHandler<CancelEventArgs>(page_BackKeyPress); this.orientationHelper = new OrientationHelper(this.CordovaBrowser, page); }

        }
        }

        void page_BackKeyPress(object sender, CancelEventArgs e)
        {
        if (OverrideBackButton)
        {
        try
        {
        CordovaBrowser.InvokeScript("CordovaCommandResult", new string[]

        { "backbutton" }

        );
        e.Cancel = true;
        }
        catch (Exception ex)

        { Console.WriteLine("Exception while invoking backbutton into cordova view: " + ex.Message); }

        }
        else
        {
        if (history.Count > 1)
        {
        Uri current = history.Peek();
        history.Pop();
        Uri next = history.Peek();
        Debug.WriteLine("current = " + current + " next = " + next);
        int curHash = current.ToString().IndexOf("#");
        int nextHash = next.ToString().IndexOf("#");
        if (curHash > 0 && nextHash > 0 && curHash == nextHash)
        {
        Debug.WriteLine("doing window.history.back()");
        IsBackButtonPressed = false;
        CordovaBrowser.InvokeScript("eval", "window.history.back()");
        if (next.ToString().IndexOf("_wpBackReload") > -1)

        { CordovaBrowser.InvokeScript("eval", "window.location.reload()"); }

        }
        else
        {
        Debug.WriteLine("Doing Navigate");
        if (next.ToString().IndexOf("_wpBackReload") > -1)

        { Uri last = history.Last(); history.Clear(); history.Push(last); }

        IsBackButtonPressed = true;
        CordovaBrowser.Navigate(next);
        }
        e.Cancel = true;
        }
        }
        }

        void GapBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
        {
        this.CordovaBrowser.Opacity = 1;

        string nativeReady = "(function()

        { cordova.require('cordova/channel').onNativeReady.fire()}

        )();";

        try
        {
        CordovaBrowser.InvokeScript("execScript", new string[]

        { nativeReady }

        );
        }
        catch (Exception ex)

        { Debug.WriteLine("Error calling js to fire nativeReady event. Did you include cordova-x.x.x.js in your html script tag?"); }

        }

        void GapBrowser_Navigating(object sender, NavigatingEventArgs e)
        {
        if (!IsBackButtonPressed)

        { history.Push(e.Uri); }

        else

        { IsBackButtonPressed = false; }

        Debug.WriteLine("GapBrowser_Navigating to :: " + e.Uri.ToString());
        // TODO: tell any running plugins to stop doing what they are doing.
        // TODO: check whitelist / blacklist
        // NOTE: Navigation can be cancelled by setting : e.Cancel = true;

        }

        /*

        • This method does the work of routing commands
        • NotifyEventArgs.Value contains a string passed from JS
        • If the command already exists in our map, we will just attempt to call the method(action) specified, and pass the args along
        • Otherwise, we create a new instance of the command, add it to the map, and call it ...
        • This method may also receive JS error messages caught by window.onerror, in any case where the commandStr does not appear to be a valid command
        • it is simply output to the debugger output, and the method returns.
        • **/
          void GapBrowser_ScriptNotify(object sender, NotifyEventArgs e)
          {
          string commandStr = e.Value;

        if (commandStr.IndexOf("DOMStorage") == 0)

        { this.domStorageHelper.HandleStorageCommand(commandStr); return; }

        else if (commandStr.IndexOf("Orientation") == 0)

        { this.orientationHelper.HandleCommand(commandStr); return; }

        CordovaCommandCall commandCallParams = CordovaCommandCall.Parse(commandStr);

        if (commandCallParams == null)

        { // ERROR Debug.WriteLine("ScriptNotify :: " + commandStr); }

        else if (commandCallParams.Service == "CoreEvents")
        {
        switch (commandCallParams.Action.ToLower())

        { case "overridebackbutton": string args = commandCallParams.Args; this.OverrideBackButton = (args != null && args.Length > 0 && args.ToLower() == "true"); break; }

        if (commandCallParams.Action.ToLower() == "softbackbutton")
        {
        try

        { CordovaBrowser.InvokeScript("eval", "window.history.back()"); history.Pop(); }

        catch (Exception ex)

        { //do nothing, must have been called from first page, not our fault here. }
        }
        if (commandCallParams.Action.ToLower() == "historystackpop")
        {
        try
        { history.Pop(); }
        catch (Exception ex)
        { //do nothing, must have been called from first page, not our fault here. }

        }
        if (commandCallParams.Action.ToLower() == "historystackclear")
        {
        try

        { history.Clear(); }

        catch (Exception ex)

        { //do nothing, must have been called from first page, not our fault here. }

        }
        }
        else

        { //Debug.WriteLine("ProcessCommand :: " + commandStr); this.nativeExecution.ProcessCommand(commandCallParams); }

        }

        private void GapBrowser_Unloaded(object sender, RoutedEventArgs e)
        {

        }

        private void GapBrowser_NavigationFailed(object sender, System.Windows.Navigation.NavigationFailedEventArgs e)

        { Debug.WriteLine("GapBrowser_NavigationFailed :: " + e.Uri.ToString()); }

        private void GapBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)

        { Debug.WriteLine("GapBrowser_Navigated :: " + e.Uri.ToString()); }

        }
        }

        Show
        Alan Neveu added a comment - - edited I wound up modifying CordovaView.xaml.cs in WP7CordovaClassLib. It turns out WP7 apps really need to be able to have more control over the history stack in order to comply with Marketplace tech cert requirements. Here is the source I wound up settling on for CordovaView.xaml.cs... Note the enhancements to void GapBrowser_ScriptNotify, and also to void page_BackKeyPress. /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using Microsoft.Phone.Controls; using System.IO.IsolatedStorage; using System.Windows.Resources; using System.Windows.Interop; using System.Runtime.Serialization.Json; using System.IO; using System.ComponentModel; using System.Xml.Linq; using WP7CordovaClassLib.Cordova.Commands; using System.Diagnostics; using System.Text; using WP7CordovaClassLib.Cordova; using System.Threading; using Microsoft.Phone.Shell; namespace WP7CordovaClassLib { public partial class CordovaView : UserControl { /// <summary> /// Indicates whether web control has been loaded and no additional initialization is needed. /// Prevents data clearing during page transitions. /// </summary> private bool IsBrowserInitialized = false; /// <summary> /// Set when the user attaches a back button handler inside the WebBrowser /// </summary> private bool OverrideBackButton = false; /// <summary> /// Used for keeping track of our history /// </summary> private Stack<Uri> history = new Stack<Uri>(); private bool IsBackButtonPressed = false; private static string AppRoot = "/app/"; /// <summary> /// Handles native api calls /// </summary> private NativeExecution nativeExecution; protected DOMStorageHelper domStorageHelper; protected OrientationHelper orientationHelper; public System.Windows.Controls.Grid _LayoutRoot { get { return ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot"))); } } public WebBrowser Browser { get { return CordovaBrowser; } } /* Setting StartPageUri only has an effect if called before the view is loaded. **/ protected Uri _startPageUri = null; public Uri StartPageUri { get Unknown macro: { if (_startPageUri == null) { // default return new Uri( AppRoot + "www/index.html", UriKind.Relative); } else { return _startPageUri; } } set Unknown macro: { if (!this.IsBrowserInitialized) { _startPageUri = value; } } } public CordovaView() { InitializeComponent(); if (DesignerProperties.IsInDesignTool) { return; } StartupMode mode = PhoneApplicationService.Current.StartupMode; if (mode == StartupMode.Launch) { PhoneApplicationService service = PhoneApplicationService.Current; service.Activated += new EventHandler<Microsoft.Phone.Shell.ActivatedEventArgs>(AppActivated); service.Launching += new EventHandler<LaunchingEventArgs>(AppLaunching); service.Deactivated += new EventHandler<DeactivatedEventArgs>(AppDeactivated); service.Closing += new EventHandler<ClosingEventArgs>(AppClosing); } else { } // initializes native execution logic this.nativeExecution = new NativeExecution(ref this.CordovaBrowser); } void AppClosing(object sender, ClosingEventArgs e) { Debug.WriteLine("AppClosing"); } void AppDeactivated(object sender, DeactivatedEventArgs e) { Debug.WriteLine("AppDeactivated"); try { CordovaBrowser.InvokeScript("CordovaCommandResult", new string[] { "pause" }); } catch (Exception) { Debug.WriteLine("Pause event error"); } } void AppLaunching(object sender, LaunchingEventArgs e) { Debug.WriteLine("AppLaunching"); } void AppActivated(object sender, Microsoft.Phone.Shell.ActivatedEventArgs e) { Debug.WriteLine("AppActivated"); try { CordovaBrowser.InvokeScript("CordovaCommandResult", new string[] { "resume" }); } catch (Exception) { Debug.WriteLine("Resume event error"); } } void GapBrowser_Loaded(object sender, RoutedEventArgs e) { if (DesignerProperties.IsInDesignTool) { return; } // prevents refreshing web control to initial state during pages transitions if (this.IsBrowserInitialized) return; this.domStorageHelper = new DOMStorageHelper(this.CordovaBrowser); try { // Before we possibly clean the ISO-Store, we need to grab our generated UUID, so we can rewrite it after. string deviceUUID = ""; using (IsolatedStorageFile appStorage = IsolatedStorageFile.GetUserStoreForApplication()) { try { IsolatedStorageFileStream fileStream = new IsolatedStorageFileStream("DeviceID.txt", FileMode.Open, FileAccess.Read, appStorage); using (StreamReader reader = new StreamReader(fileStream)) { deviceUUID = reader.ReadLine(); } } catch (Exception / ex /) { deviceUUID = Guid.NewGuid().ToString(); } Debug.WriteLine("Updating IsolatedStorage for APP:DeviceID :: " + deviceUUID); IsolatedStorageFileStream file = new IsolatedStorageFileStream("DeviceID.txt", FileMode.Create, FileAccess.Write, appStorage); using (StreamWriter writeFile = new StreamWriter(file)) { writeFile.WriteLine(deviceUUID); writeFile.Close(); } } StreamResourceInfo streamInfo = Application.GetResourceStream(new Uri("CordovaSourceDictionary.xml", UriKind.Relative)); if (streamInfo != null) { StreamReader sr = new StreamReader(streamInfo.Stream); //This will Read Keys Collection for the xml file XDocument document = XDocument.Parse(sr.ReadToEnd()); var files = from results in document.Descendants("FilePath") select new { path = (string)results.Attribute("Value") } ; StreamResourceInfo fileResourceStreamInfo; using (IsolatedStorageFile appStorage = IsolatedStorageFile.GetUserStoreForApplication()) { foreach (var file in files) { fileResourceStreamInfo = Application.GetResourceStream(new Uri(file.path, UriKind.Relative)); if (fileResourceStreamInfo != null) { using (BinaryReader br = new BinaryReader(fileResourceStreamInfo.Stream)) { byte[] data = br.ReadBytes((int)fileResourceStreamInfo.Stream.Length); string strBaseDir = AppRoot + file.path.Substring(0, file.path.LastIndexOf(System.IO.Path.DirectorySeparatorChar)); if(!appStorage.DirectoryExists(strBaseDir)) { //Debug.WriteLine("Creating Directory :: " + strBaseDir); appStorage.CreateDirectory(strBaseDir); } // This will truncate/overwrite an existing file, or using (IsolatedStorageFileStream outFile = appStorage.OpenFile(AppRoot + file.path, FileMode.Create)) { Debug.WriteLine("Writing data for " + AppRoot + file.path + " and length = " + data.Length); using (var writer = new BinaryWriter(outFile)) { writer.Write(data); } } } } else { Debug.WriteLine("Failed to write file :: " + file.path + " did you forget to add it to the project?"); } } } } CordovaBrowser.Navigate(StartPageUri); IsBrowserInitialized = true; AttachHardwareButtonHandlers(); } catch (Exception ex) { Debug.WriteLine("Exception in GapBrowser_Loaded :: {0} ", ex.Message); } } void AttachHardwareButtonHandlers() { PhoneApplicationFrame frame = Application.Current.RootVisual as PhoneApplicationFrame; if (frame != null) { PhoneApplicationPage page = frame.Content as PhoneApplicationPage; if (page != null) { page.BackKeyPress += new EventHandler<CancelEventArgs>(page_BackKeyPress); this.orientationHelper = new OrientationHelper(this.CordovaBrowser, page); } } } void page_BackKeyPress(object sender, CancelEventArgs e) { if (OverrideBackButton) { try { CordovaBrowser.InvokeScript("CordovaCommandResult", new string[] { "backbutton" } ); e.Cancel = true; } catch (Exception ex) { Console.WriteLine("Exception while invoking backbutton into cordova view: " + ex.Message); } } else { if (history.Count > 1) { Uri current = history.Peek(); history.Pop(); Uri next = history.Peek(); Debug.WriteLine("current = " + current + " next = " + next); int curHash = current.ToString().IndexOf("#"); int nextHash = next.ToString().IndexOf("#"); if (curHash > 0 && nextHash > 0 && curHash == nextHash) { Debug.WriteLine("doing window.history.back()"); IsBackButtonPressed = false; CordovaBrowser.InvokeScript("eval", "window.history.back()"); if (next.ToString().IndexOf("_wpBackReload") > -1) { CordovaBrowser.InvokeScript("eval", "window.location.reload()"); } } else { Debug.WriteLine("Doing Navigate"); if (next.ToString().IndexOf("_wpBackReload") > -1) { Uri last = history.Last(); history.Clear(); history.Push(last); } IsBackButtonPressed = true; CordovaBrowser.Navigate(next); } e.Cancel = true; } } } void GapBrowser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e) { this.CordovaBrowser.Opacity = 1; string nativeReady = "(function() { cordova.require('cordova/channel').onNativeReady.fire()} )();"; try { CordovaBrowser.InvokeScript("execScript", new string[] { nativeReady } ); } catch (Exception ex) { Debug.WriteLine("Error calling js to fire nativeReady event. Did you include cordova-x.x.x.js in your html script tag?"); } } void GapBrowser_Navigating(object sender, NavigatingEventArgs e) { if (!IsBackButtonPressed) { history.Push(e.Uri); } else { IsBackButtonPressed = false; } Debug.WriteLine("GapBrowser_Navigating to :: " + e.Uri.ToString()); // TODO: tell any running plugins to stop doing what they are doing. // TODO: check whitelist / blacklist // NOTE: Navigation can be cancelled by setting : e.Cancel = true; } /* This method does the work of routing commands NotifyEventArgs.Value contains a string passed from JS If the command already exists in our map, we will just attempt to call the method(action) specified, and pass the args along Otherwise, we create a new instance of the command, add it to the map, and call it ... This method may also receive JS error messages caught by window.onerror, in any case where the commandStr does not appear to be a valid command it is simply output to the debugger output, and the method returns. **/ void GapBrowser_ScriptNotify(object sender, NotifyEventArgs e) { string commandStr = e.Value; if (commandStr.IndexOf("DOMStorage") == 0) { this.domStorageHelper.HandleStorageCommand(commandStr); return; } else if (commandStr.IndexOf("Orientation") == 0) { this.orientationHelper.HandleCommand(commandStr); return; } CordovaCommandCall commandCallParams = CordovaCommandCall.Parse(commandStr); if (commandCallParams == null) { // ERROR Debug.WriteLine("ScriptNotify :: " + commandStr); } else if (commandCallParams.Service == "CoreEvents") { switch (commandCallParams.Action.ToLower()) { case "overridebackbutton": string args = commandCallParams.Args; this.OverrideBackButton = (args != null && args.Length > 0 && args.ToLower() == "true"); break; } if (commandCallParams.Action.ToLower() == "softbackbutton") { try { CordovaBrowser.InvokeScript("eval", "window.history.back()"); history.Pop(); } catch (Exception ex) { //do nothing, must have been called from first page, not our fault here. } } if (commandCallParams.Action.ToLower() == "historystackpop") { try { history.Pop(); } catch (Exception ex) { //do nothing, must have been called from first page, not our fault here. } } if (commandCallParams.Action.ToLower() == "historystackclear") { try { history.Clear(); } catch (Exception ex) { //do nothing, must have been called from first page, not our fault here. } } } else { //Debug.WriteLine("ProcessCommand :: " + commandStr); this.nativeExecution.ProcessCommand(commandCallParams); } } private void GapBrowser_Unloaded(object sender, RoutedEventArgs e) { } private void GapBrowser_NavigationFailed(object sender, System.Windows.Navigation.NavigationFailedEventArgs e) { Debug.WriteLine("GapBrowser_NavigationFailed :: " + e.Uri.ToString()); } private void GapBrowser_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e) { Debug.WriteLine("GapBrowser_Navigated :: " + e.Uri.ToString()); } } }
        Hide
        Alan Neveu added a comment -

        Update... My app just passed Marketplace certification. Here is a functional summary of what I needed that the 1.6.1 build did not provide:

        1) Because I use JQueryMobile, I needed the Back Button to be able to know if the previous page in the history was just a hash tag navigation, and if so, just do a window.history.back.

        2) There are some instances in which my app needed the page to then also be reloaded, and so I had to put a parameter on the second page to indicate this, which is &_wpBackReload=1 .

        3) The WP7 Marketplace requirements are very strict when it comes to hitting Back on the first page of the app and that it must exit the app. If you wind up deep in the history and the tester thinks he is stuck in your app and no amount of hitting the Back button will exit the app, your app will be failed. So the best thing is to be able to tell Cordova to clear the stack whenever your app navigates to the Home page. That can be done using the historystackclear action.

        4) Sometimes my app needed to just do a simple historystackpop and remove whatever is the top item in the stack. It is hard to explain why, other than perhaps a bug in WP7's embeddable browser control. So that is why I needed the action called historystackpop.

        5) My standard Back buttons for other platforms that do not really go back but instead just reload the home page needed to become "soft" back buttons if the running platform is WP7. So that is why I needed the action called "softbackbutton". This way my navigation looks 100% identical across WP7, iOS, Android, and BlackBerry, but the implementation is slightly different in that WP7 calls the "softbackbutton" action whereas the others navigate (forward) to the home page. The if statement for that lives in my javascript.

        I hope this helps, it's kind of a mess I know. Thanks for listening!

        Show
        Alan Neveu added a comment - Update... My app just passed Marketplace certification. Here is a functional summary of what I needed that the 1.6.1 build did not provide: 1) Because I use JQueryMobile, I needed the Back Button to be able to know if the previous page in the history was just a hash tag navigation, and if so, just do a window.history.back. 2) There are some instances in which my app needed the page to then also be reloaded, and so I had to put a parameter on the second page to indicate this, which is &_wpBackReload=1 . 3) The WP7 Marketplace requirements are very strict when it comes to hitting Back on the first page of the app and that it must exit the app. If you wind up deep in the history and the tester thinks he is stuck in your app and no amount of hitting the Back button will exit the app, your app will be failed. So the best thing is to be able to tell Cordova to clear the stack whenever your app navigates to the Home page. That can be done using the historystackclear action. 4) Sometimes my app needed to just do a simple historystackpop and remove whatever is the top item in the stack. It is hard to explain why, other than perhaps a bug in WP7's embeddable browser control. So that is why I needed the action called historystackpop. 5) My standard Back buttons for other platforms that do not really go back but instead just reload the home page needed to become "soft" back buttons if the running platform is WP7. So that is why I needed the action called "softbackbutton". This way my navigation looks 100% identical across WP7, iOS, Android, and BlackBerry, but the implementation is slightly different in that WP7 calls the "softbackbutton" action whereas the others navigate (forward) to the home page. The if statement for that lives in my javascript. I hope this helps, it's kind of a mess I know. Thanks for listening!
        Hide
        Jesse MacFadyen added a comment -

        Commit is here:
        https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-wp7.git;a=commit;h=2fc1089ae5936ecaf627fad48dbfee9a2d5f3e08

        Backbutton presses are passed to js, window.history.back()
        If the page changes as a result of the js call, then the event is cancelled.
        When the history stack is empty, the page does not change as a result of history.back(), so the default implementation of exiting the app is performed.

        OverrideBackButton is still available if you need greater control over history management.

        Show
        Jesse MacFadyen added a comment - Commit is here: https://git-wip-us.apache.org/repos/asf?p=incubator-cordova-wp7.git;a=commit;h=2fc1089ae5936ecaf627fad48dbfee9a2d5f3e08 Backbutton presses are passed to js, window.history.back() If the page changes as a result of the js call, then the event is cancelled. When the history stack is empty, the page does not change as a result of history.back(), so the default implementation of exiting the app is performed. OverrideBackButton is still available if you need greater control over history management.

          People

          • Assignee:
            Jesse MacFadyen
            Reporter:
            Alan Neveu
          • Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved:

              Development