-1

Can anyone help me understand why my call to dialogservice executes after the CanNavigateAway function has returned its value? (My goal is to warn the user they are about to navigate away from a view without saving their changes. If they click OK, the navigation is allowed. I'm using MVVM Light.

When I step through the code, it does reach the dialog service, but then proceeds to the end of CanNavigateAway before creating the dialog. The CanNavigateAway method is called by OnNavigatingFrom.

public bool CanNavigateAway()
    {
        if (!changesSaved && Model.IsModified && !continueNavigation)
        {             
              dialogService.ShowMessage("Are you sure you want to continue?",
              "Confirmation",
              buttonConfirmText: "Continue", buttonCancelText: "Discard",
              afterHideCallback: (confirmed) =>
              {
                  if (confirmed)
                  {
                      // User has pressed the "confirm" button.
                      // ...
                      continueNavigation = true;
                  }
                  else
                  {
                      // User has pressed the "cancel" button
                      // (or has discared the dialog box).
                      // ...
                      continueNavigation = false;
                  }
              });
            return continueNavigation;
        }
}

Here is the OnNavigatingFrom method from the MVVM Light Bindable Page class:

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
    {
        var navigableViewModel = this.DataContext as INavigable;

        if (navigableViewModel != null)
        {
            if (!navigableViewModel.CanNavigateAway())
            {
                e.Cancel = true;
            }
        }
    }

I tried this a different way to get the dialog service out of the mix, but showConfirmationDialogAsync still does not seem to execute in time:

public bool CanNavigateAway()
{
    continueNavigation = false;
    if (!changesSaved && Model.IsModified && !continueNavigation)
    {
        showConfirmationDialogAsync();
        return continueNavigation;
    }  

private async void showConfirmationDialogAsync()
{
    continueNavigation = false;
    ContentDialog noSaveConfirmation = new ContentDialog
    {
        Title = "Warning",
        Content = "You have unsaved changes. Are you sure you want to leave this page without saving?",
        PrimaryButtonText = "Leave without saving",
        SecondaryButtonText = "Stay and finish"
    };

    ContentDialogResult result = await noSaveConfirmation.ShowAsync();

    if (result == ContentDialogResult.Primary)
    {
        continueNavigation = true;
    }
    else if (result == ContentDialogResult.Secondary)
    {
        continueNavigation = false;
    }
}        
Matt
  • 145
  • 1
  • 13
  • You're only passing delegates to the `ShowMessage` function, they don't get immediately called. – DavidG Feb 28 '18 at 19:10
  • Impossible to say for sure since you didn't provide the type of dialogService, but it's probably queuing on the message pump, and therefore effectively async. One way to handle it is change the callback to fire an event "OnNavigatingFromConfirmed" and make the return type void instead of bool. But there's other approaches – zzxyz Feb 28 '18 at 19:25
  • A callback is a pattern to deal with async programming. The lambda `afterHideCallback` will be invoked after the dialog is hidden but the method `CanNavigateAway()` will return immediately in a non-blocking fashion. Unfortunately, I can't comment on [uwp] or [mvvm-light]. – maxbeaudoin Feb 28 '18 at 21:08
  • Thanks all. I have refactored to try and simply. This version doesn't use the MVVM Light dialog service. I'm still having the same issue, though. I'm really stumped on this one. – Matt Feb 28 '18 at 22:12
  • "ShowAsync" is a re-entrancy bug waiting to happen. But easy to diagnose, you see it back in the debugger's stack trace. https://stackoverflow.com/a/5183623/17034 – Hans Passant Feb 28 '18 at 22:30

1 Answers1

1

None of the solutions will work if you require a response from the user. The problem is that when the code is inside the navigation event handler, it is running on the UI thread and the user prompt runs asynchronously, so that the UI is free to present the dialog to the user. This however means that the event handler finishes before the user has a chance to respond.

However, you can use a workaround solution. Add a flag bool field like forceNavigation. Then inside the OnNavigatingFrom display the dialog to the user and set Cancel to true right away and display the user the confirmation dialog. If the user says yes, then set forceNavigaiton to true and trigger the navigation manually again. Now it will skip the confirmation part and navigate right away.

protected async override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{ 
    //if navigation is forced, skip all logic
    if ( !forceNavigation )
    {
        var navigableViewModel = this.DataContext as INavigable;

        if (navigableViewModel != null)
        {
            e.Cancel = true;
            //display the dialog to the user, if he says yes, set
            //forceNavigation = true; and repeat the navigation (e.g. GoBack, ... )
        }
    }
}
Martin Zikmund
  • 38,440
  • 7
  • 70
  • 91