So, you're already started working on your Metro Style App on Windows 8 Consumer Preview? That's great!
As you know, there are still many issues with Metro Style Apps development on this version of Windows 8 - is it even a beta? Anyway, during development of an app for a customer in C# and XAML, I ran into several bumps.
Today I want to talk about a bug in the built in Frame control. This control is responsible for navigating between pages in Metro Style apps. When you want to navigate to a different page, you need to call the Frame class's Navigate function:
Navigation in Windows 8 Metro Style Apps
var frame = (Frame)Window.Current.Content;
frame.Navigate(typeof(SecondPage));
Well, that's simple enough, right? It is very similar to the way navigation works in Silverlight, and of course Windows Phone:
Navigation in Silverlight and Windows Phone
var pageUri = new Uri("/Pages/SecondPage.xaml", UriKind.Relative);this.NavigationService.Navigate(pageUri);
You can notice the major difference from Silverlight is the use of the page type, rather than its Uri. This shouldn't have been an issue, but unfortunately there seems to be a bug in the built in Frame control in Windows 8 Consumer Preview. Currently, when you try to Navigate to the same page type twice in a row - perhaps passing a different parameter each time - the navigation is not cached correctly by the internal navigation stack of the Frame class.
So when you try to navigate back to the previous page, you'll notice that the frame counts all instances of a specific page type as one entry in the navigation history stack. Here is a quick repro:
Start from page A
Navigate to page B (#1)
Navigate to page B again (#2)
Navigate back
Notice that you're back in page A, and the entry for page B (#1) is completely ignored.
This can be quite frustrating, especially for developers like myself, with experience in developing XAML based apps in other frameworks. After reading in the Metro Style Apps forums, all I found was a suggestion to implement a fixed Frame control:
http://social.msdn.microsoft.com/Forums/en-US/winappswithcsharp/thread/61b66a9a-3557-4b6b-ab47-50bb9d44f38b
Here I am, stepping up to the challenge. Trying to inherit from class Frame won’t do the trick – none of the Frame class members are marked as virtual. Creating our own Frame class could cause some other problems, since some built in classes like Page have members of type Frame. I decided to go this path since I needed the workaround, hopefully Microsoft will fix this bug in the next version.
So what do we need? We need some sort of a FrameEx class which inherits from class ContentControl, allows navigation and remembers navigation history. We also need it to implement the INavigate interface, like the original Frame class does.
Lets handle nav history first. We’ll create a Stack instance which will remember each call to create a page, together with the specific parameter:
private Stack<PageTypeWithParam> _navigationStack;
public FrameEx()
{ _navigationStack = new Stack<PageTypeWithParam>();
}
Notice that the PageTypeWithParam class will be defined to hold the value for both page type and parameter for each navigation record.
During the navigation request, the current record will be saved to temp fields while the previous record will be inserted to the stack:
private Type _currentPageType;
private object _currentParam;
public bool Navigate(Type sourcePageType, object parameter)
{ if (_currentPageType != null)
{ _navigationStack.Push(new PageTypeWithParam { PageType = _currentPageType, Param = _currentParam }); }
this.Content = page;
if (parameter != null && page.DataContext == null)
{ page.DataContext = parameter;
}
_currentPageType = sourcePageType;
_currentParam = parameter;
}
Notice that I chose to enter the parameter right into the page’s DataContext. This is far from ideal, but again, just like other limitations we ran into, I had no way of calling the OnNavigatedTo function of the page.
Finally, the CanGoBack property and the GoBack function are pretty straight forward:
public bool CanGoBack
{ get
{ return _navigationStack.Count > 0;
}
}
public void GoBack()
{ _currentPageType = null;
_currentParam = null;
var pageTypeWithParam = _navigationStack.Pop();
Navigate(pageTypeWithParam.PageType, pageTypeWithParam.Param);
}
Here is the full source code for the FrameEx class:
public class FrameEx: ContentControl, INavigate
{ private Stack<PageTypeWithParam> _navigationStack;
private Type _currentPageType;
private object _currentParam;
public FrameEx()
{ _navigationStack = new Stack<PageTypeWithParam>();
}
public bool Navigate(Type sourcePageType, object parameter)
{ if (sourcePageType == null)
{ throw new ArgumentNullException("sourcePageType"); }
var page = CreatePage(sourcePageType);
if (page == null)
{ throw new ArgumentException("Page cannot be constucted from the given type", "sourcePageType"); }
if (_currentPageType != null)
{ _navigationStack.Push(new PageTypeWithParam { PageType = _currentPageType, Param = _currentParam }); }
this.Content = page;
if (parameter != null && page.DataContext == null)
{ page.DataContext = parameter;
}
_currentPageType = sourcePageType;
_currentParam = parameter;
return true;
}
public bool Navigate(Type sourcePageType)
{ return Navigate(sourcePageType, null);
}
private Page CreatePage(Type sourcePageType)
{ var page = Activator.CreateInstance(sourcePageType) as Page;
return page;
}
public bool CanGoBack
{ get
{ return _navigationStack.Count > 0;
}
}
public void GoBack()
{ _currentPageType = null;
_currentParam = null;
var pageTypeWithParam = _navigationStack.Pop();
Navigate(pageTypeWithParam.PageType, pageTypeWithParam.Param);
}
public void GoHome()
{ if (_navigationStack.Count > 0)
{ while (_navigationStack.Count > 1)
{ _navigationStack.Pop();
}
GoBack();
}
}
private class PageTypeWithParam
{ public Type PageType { get; set; } public object Param { get; set; } }
}
Of course we will need to use our new FrameEx class in App OnLaunched method:
protected override void OnLaunched(LaunchActivatedEventArgs args)
{ // ...
// Create a Frame to act navigation context and navigate to the first page
var rootFrame = new FrameEx();
rootFrame.Navigate(typeof(HomePage));
// Place the frame in the current Window and ensure that it is active
Window.Current.Content = rootFrame;
Window.Current.Activate();
}
Have fun…