Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overriding WP8 Navigation - crash in PhoneApplicationPage

I'm trying to do something that's arguably a bad idea, but I think it's still possible. I'm trying to override how WP8 handles the Back Button and implement it myself. I theorize that if I:

The Plan

  1. Only ever create one "Frame" and "Page" in the entire application
  2. Always handle PhoneApplicationPage.BackKeyPress myself unless they were about to back out of the application.

The Repro

Here's a sample project that has the crash

The code

..then it should work. However, my attempts are being thwarted by Windows Phone. Here's the code:

// This basically happens on PhoneApplicationService.OnLaunched
_viewModelChanged.StartWith(ViewModel).Where(x => x != null).Subscribe(vm => {
    var page = default(IViewFor);
    var frame = RootVisual as PhoneApplicationFrame;

    // Find the initial PhoneApplicationPage for the app
    page = RxApp.GetService<IViewFor>("InitialPage");

    // Depending on how we're being signalled (i.e. if this is cold start 
    // vs. resume), we need to create the PhoneApplicationFrame ourselves
    if (frame == null) {
        frame = new PhoneApplicationFrame() {
            Content = page,
        };
    }

    page.ViewModel = vm;
    var pg = page as PhoneApplicationPage;
    if (pg != null) {
        pg.BackKeyPress += (o, e) => {
            if (ViewModel.Router.NavigationStack.Count <= 1 ||
                ViewModel.Router.NavigateBack.CanExecute(null)) {
                return;
            }

            e.Cancel = true;
            ViewModel.Router.NavigateBack.Execute(null);
        };
    }

    // Finally, set Application.RootVisual
    RootVisual = frame;
});

Sadness

This works great, until right after this code executes, where a DispatcherItem queued by the framework crashes the app:

System.NullReferenceException occurred
Message: A first chance exception of type 'System.NullReferenceException' occurred in Microsoft.Phone.ni.dll
Microsoft.Phone.ni.dll!Microsoft.Phone.Controls.PhoneApplicationPage.InternalOnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)   Unknown
Microsoft.Phone.ni.dll!Microsoft.Phone.Controls.PhoneApplicationPage.Microsoft.Phone.Controls.IPhoneApplicationPage.InternalOnNavigatedFromX(System.Windows.Navigation.NavigationEventArgs e)   Unknown
Microsoft.Phone.ni.dll!System.Windows.Navigation.NavigationService.RaiseNavigated(object content, System.Uri uri, System.Windows.Navigation.NavigationMode mode, bool isNavigationInitiator, Microsoft.Phone.Controls.IPhoneApplicationPage existingContentPage, Microsoft.Phone.Controls.IPhoneApplicationPage newContentPage) Unknown
Microsoft.Phone.ni.dll!System.Windows.Navigation.NavigationService.CompleteNavigation(System.Windows.DependencyObject content, System.Windows.Navigation.NavigationMode mode)   Unknown
Microsoft.Phone.ni.dll!System.Windows.Navigation.NavigationService.ContentLoader_BeginLoad_Callback(System.IAsyncResult result) Unknown
Microsoft.Phone.ni.dll!System.Windows.Navigation.PageResourceContentLoader.BeginLoad_OnUIThread(System.AsyncCallback userCallback, System.Windows.Navigation.PageResourceContentLoader.PageResourceContentLoaderAsyncResult result) Unknown
Microsoft.Phone.ni.dll!System.Windows.Navigation.PageResourceContentLoader.BeginLoad.AnonymousMethod__0(object args)    Unknown
[Native to Managed Transition]  
mscorlib.ni.dll!System.Delegate.DynamicInvokeImpl(object[] args)    Unknown
System.Windows.ni.dll!System.Windows.Threading.DispatcherOperation.Invoke() Unknown
System.Windows.ni.dll!System.Windows.Threading.Dispatcher.Dispatch(System.Windows.Threading.DispatcherPriority priority)    Unknown
System.Windows.ni.dll!System.Windows.Threading.Dispatcher.OnInvoke(object context)  Unknown
System.Windows.ni.dll!System.Windows.Hosting.CallbackCookie.Invoke(object[] args)   Unknown
System.Windows.RuntimeHost.ni.dll!System.Windows.RuntimeHost.ManagedHost.InvokeDelegate(System.IntPtr pHandle, int nParamCount, System.Windows.Hosting.NativeMethods.ScriptParam* pParams, System.Windows.Hosting.NativeMethods.ScriptParam* pResult)   Unknown
like image 392
Ana Betts Avatar asked Feb 09 '13 20:02

Ana Betts


2 Answers

So, I've solved this - my code was problematic because I didn't grok how WP8 works :) Here's what I understand now, which may also be wrong but I'll write it anyways

How your WP8 app is initialized:

  1. The OS creates your App class via rehydrating App.xaml.cs
  2. This means, your constructor gets run, and as part of that, you create a PhoneApplicationFrame
  3. Creating a PhoneApplicationFrame seems to also set a global static variable (same thing happens with creating PhoneApplicationService in the App.xaml, it sets PhoneApplicationService.Current).
  4. NavigationService then attempts to recreate a XAML View via a resource string (i.e. '/MainPage.xaml'). Either it will recreate the one that was previously tombstoned, or if not, it defaults to the one in your WMAppManifest (this is the part I didn't understand).
  5. PhoneApplicationFrame.Navigated gets called by NavigationService - this is where you can actually start doing stuff, including most importantly, setting Application.RootVisual, which will send the Loading... screen away
  6. PhoneApplicationService.Launched or PhoneApplicationService.Activated finally fires, once basically everything is set up, depending how your app was woken up.
like image 104
Ana Betts Avatar answered Dec 04 '22 03:12

Ana Betts


Found the issue. Well, the tip of the iceberg.

The code of the InternalOnNavigatedFrom method is:

internal override void InternalOnNavigatedFrom(NavigationEventArgs e)
{
    PhoneApplicationPage content = e.Content as PhoneApplicationPage;
    string str = ((content == null) || (content.Title == null)) ? string.Empty : content.Title;
    PerfUtil.BeginLogMarker(MarkerEvents.TH_ONNAVIGATEDFROM_PAGE, string.Format("{0},{1},{2}", (base.Title == null) ? "" : base.Title, e.NavigationMode, str));
    this.OnNavigatedFrom(e);
    PerfUtil.EndLogMarker(MarkerEvents.TH_ONNAVIGATEDFROM_PAGE, string.Format("{0},{1},{2}", (base.Title == null) ? "" : base.Title, e.NavigationMode, str));
    DeviceStatus.KeyboardDeployedChanged -= new EventHandler(this.OnKeyboardDeployedChanged);
    Task rootTask = ApplicationHost.Current.RootTask;
    rootTask.OnVisibleRegionChange = (ITask.VisibleRegionChanged) Delegate.Remove(rootTask.OnVisibleRegionChange, new ITask.VisibleRegionChanged(this.OnVisibleRegionChange));
    Task task2 = ApplicationHost.Current.RootTask;
    task2.OnSipVisibilityChange = (ITask.SipVisibilityChange) Delegate.Remove(task2.OnSipVisibilityChange, new ITask.SipVisibilityChange(this.OnSipVisibilityChange));
    this._lastSipHeight = 0.0;
    this._dictionary = null;
}

After a bit of debugging, I concluded that neither e or Application.Current.RootTask were null. After scratching my head, I looked at the code of the KeyboardDeployedChanged event handler:

public static  event EventHandler KeyboardDeployedChanged
{
    [SecuritySafeCritical] add
    {
        if (KeyboardDeployedSubscription == null)
        {
            KeyboardDeployedSubscription = new SubscriptionHandler(DeviceTypes.KeyBoard);
        }
        KeyboardDeployedSubscription.Changed += value;
    }
    [SecuritySafeCritical] remove
    {
        KeyboardDeployedSubscription.Changed -= value;
    }
}

This code is poorly written. If the remove part of the handler is called before the add, KeyboardDeployedSubscription will be null and an exception will be raised. To test my theory, I subscribed to the event in App's constructor:

    public App()
    {
        // Global handler for uncaught exceptions.
        UnhandledException += Application_UnhandledException;

        DeviceStatus.KeyboardDeployedChanged += (sender, e) => { };

And sure enough, the exception was gone. Now, to understand why your code is triggering this issue, I backtraced to which part of the framework is supposed to subscribe to the event. The only candidate is the InternalOnNavigatedTo method.

Therefore, your issue is that OnNavigatedFrom is called even though OnNavigatedTo was never called.

like image 45
Kevin Gosse Avatar answered Dec 04 '22 05:12

Kevin Gosse