Saturday, November 27, 2010

“Navigation is already in progress” exception on Windows Phone 7.

It is a common situation when you need to navigate to a page in Windows Phone 7 application. Usual approach is to have a button with Click event handler attached to it. In the event handler we need to use NavigationService.Navigate() method and pass a URI of the page we want navigate to:

private void btnPage1_Click(object sender, EventArgs e)
{
   NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
} 
Most of the time it works perfectly but sometimes, if we managed to click the button several times, most likely we will get “Navigation is already in progress” exception. Especially this is a case if the page which we are navigation to is performing some long-time operations in the UI thread.
To resolve the problem we need:
  1. Move all long-time operations (calculations, data layer access etc.) away from the UI thread and run them in a separate thread.
  2. Ensure that NavigationService.Navigate() can be called only once.
While 1) is very specific 2) is quite common and one of the possible solutions is described below.
What you need to do is just to introduce new local variable and to handle Navigating and Navigated events of NavigationService:

private bool _navigationInProgress;

private void btnPage1_Click(object sender, EventArgs e)
{
   NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
}

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
   if (_navigationInProgress)
      e.Cancel = true;
   _navigationInProgress = true;
   base.OnNavigatingFrom(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
   base.OnNavigatedFrom(e);
   _navigationInProgress = false;
}

UPD: Thanks to Ilya Troitsky, who suggested to handle this issue in UI thread without even calling Navigate method if previous navigation hasn't finished yet:

private bool _navigationInProgress;

private void btnPage1_Click(object sender, EventArgs e)
{
   if (_navigationInProgress)
      return;
   _navigationInProgress = true;
   NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
   base.OnNavigatedFrom(e);
   _navigationInProgress = false;
}

Both approaches work fine (tested on a WP7 device).

6 comments:

Ilya Troitskiy said...

А почему не проверять флаг в btnPage1_Click, и не вызывать NavigationService.Navigate(), если флаг не выставлен. А сбрасывать флаг там же, где и сейчас. Просто если btnPage1_Click вызывается асинхронно, то в твоём случае возможен вариант, когда NavigationService.Navigate вызовется дважды, а флаг еще не успеет выставится.

Bashir Magomedov said...

Потому что Navigate вызывается синхронно, и пока все события не пройдут, флаг не выставится.

Ilya Troitskiy said...

Чего-то я туплю, но из фразы Ensure that NavigationService.Navigate() can be called only once я понял, что как раз вызов этого Navigate() и надо поточно обезопасить. То есть, проблема возникает из-за одновременно исполняюшихся btnPage1_Click. А в этом случае, как раз мы можем уйти в 2 Navigate-а вместе с событиями, а до _navigationInProgress = true еще не дойти. Если я не прав, поясни, пожалуйста, где происходит асинхронность, которую ты борешь?

Bashir Magomedov said...

Дело в том, что основной (UI) поток выйдет из метода Navigate и установит флаг только после выхода из метода OnNavigating.
Если в OnNavigating (на самом деле в конструкторе страницы на которую мы переходим) делается что-то сложное, то этот выход произойдет нескоро.
Если проверять флаг на клик, то флаг все еще будет ложным и Navigate выполнится без проблем, опять пойдет в OnNavigating и тут случится исключение, т.к. предыдущий Navigation еще не закончился.
Хотя, я кажется понял, что ты имеешь ввиду :). Проверять флаг (если truе то выходить), устанвливать флаг в true и запускать Navigate... а сбрасывать там же где и сейчас... так? :)

Ilya Troitskiy said...

Да, именно так. И флаг сбрасывать после вызова базового обработчика. Теперь еще раз почему. К сожалению, я не знаю эту потоковую модель. Ты говоришь что, основной(как ты его называешь UI) поток не вернет управления из Navigate, пока не выполнит конструктор страницы, и т.д. Но что будет, если юзер в этот момент еще кликает на кнопку? Поставится ли этот клик(btnPage1_Click) в некую очередь сообщений этого UI потока, или создастся еще один поток, в котором он будет выполняться?
Вообще, похоже, что события btnPage1_Click обрабатываются действительно в одном UI потоке, а вот события OnNavigating(ed) вызываются уже из другого. В таком случае, как я предлагаю, мы просто не будем даже пытыться выполнить "залоченный" navigate()

Bashir Magomedov said...

Да все будет так как ты описал. Не знаю, зачем я так усложнил. Насчет сброса флага после базового вызова - ценное замечание. Поправил. Да и твой вариант добавил :).