前两篇文章中,我们的Demo代码都是基于页面切换的,而我们在Silverlight框架下开发的就是以XAML文件为基础的事件驱动程序。也就是说我们的程序会由一个或多个页面作成,这一点和Web程序很相似,所以页面间的切换就很重要。
这一篇文章就来将介绍Windows Phone平台上导航功能。
一 导航控件
从Silverlight3开始,提供了内置的导航框架,可以比较轻松的在
Silverlight Page之间进行切换,并且可以和浏览器的前进、后退按钮集成。在Silverlight
3之前的版本,Silverlight没有特定的导航框架,项目中页面之间的切换是通过修改RootVisual布局容器的内容而实现的
。利用SDK中提供了Frame和Page可以完成导航操纵。其中Frame是导航的框架,Page通过Frame加载显示并实现页面间的导航。
Windows Phone程序也是基于Sliverlight的Page
Model进行导航,同时你也可以使用后退按钮进行后退操纵。WP上核心的导航容器控件为PhoneApplicationFrame,他可以容纳一个PhoneApplicationPage。我们可以创建多个页面,通过Frame来进行导航。
对于Windows Phone来说,只允许有一个Frame,一个Frame有一下特性:
操作寄宿的Page页面的属性,比如orientation
指定页面呈现的客户端区域
为状态栏和程序栏保留空间
监听obscured和unobscured事件
而对于Page来说,他会填充满Frame所在的空间。除此之外,程序中还有Status
bar和Application Bar,他们都能设置visible 属性;Window Phone也支持屏幕旋转,但是只有在转动设备时才能使之旋转,而不能通过编程的方式实现,因为orientation是只读属性;我们只能通过设置SupportedOrientations来完成;机器的后退按钮可以完成导航中的后退功能,也能关闭键盘,菜单,弹出窗体,同时还能关闭程序。
关于使用这两个空间导航,参见前两篇文章的例子。定义一个Frame,设置到VisualRoot。在配置文件中设置开始导航的页面。然后通过Frame或者NavigationService来进行导航。参考
:Frame and Page Navigation for Windows Phone
二 导航框架分析
我们还是接着上一篇文章程序启动来展开,看看Frame是如何去导航的。
1 导航框架的初始化
程序启动的的第一步是在App类中,我们创建了一个PhoneApplicationFrame实例。因为PhoneApplicationFrame是继承于Frame的,所以我们先看看Frame构造函数做了什么。
internal Frame() { base.DefaultStyleKey = typeof(Frame); base.Loaded += new RoutedEventHandler(this.Frame_Loaded); this._hostInfo = new HostInfo(); } |
构造函数中绑定一个Loaded事件和设置Host信息(如果看过SilverLight的源码,会发现和这的构造函数是有区别的)。接着就看看PhoneApplicationFrame的构造函数
//有删减 public PhoneApplicationFrame() { Action a = null; ShellFrame.Initialize(); base.DefaultStyleKey = typeof(PhoneApplicationFrame); this.Orientation = PageOrientation.PortraitUp; this._visibleRegion = rect; base._navigationService = new NavigationService(this); if (!Frame.IsInDesignMode() && !base._hostInfo.Rehydrated) { if (a == null) { a = delegate { base.Load(); }; } Deployment.Current.Dispatcher.BeginInvoke(a); } if (Current == null) { Current = this; } } |
这里面前面一部分是设置界面方向相关的一些内容,然后设置了可见区域,这里被省略了,这些都是上面说的Frame有的特性;接下来的工作很重要定义了一个NavigationService对象,保存在Frame的字段中。我们接着看看这个NavigationService对象构造函数
internal NavigationService(PhoneApplicationFrame nav) { this._navigationPendingLock = new object(); this._cacheRequiredPages = new Dictionary<string, PhoneApplicationPage>(); PerfUtil.BeginLogMarker(MarkerEvents.TH_INIT_NAVIGATIONSERVICE, "NavigationService started"); Guard.ArgumentNotNull(nav, "nav"); this._host = nav; HostFrame = nav; HostInfo info = new HostInfo(); this._shellPageManagerCallback = new ShellPageManagerCallback(); this._shellPageManagerCallback.OnCancelRequestEventHandler = (EventHandler) Delegate.Combine
(this._shellPageManagerCallback.OnCancelRequestEventHandler, new EventHandler(this.ShellPageManager_OnCancelRequest)); this._shellPageManagerCallback.OnPageStackReactivatedEventHandler = (EventHandler<PageStackReactivatedEventArgs>)
Delegate.Combine(this._shellPageManagerCallback.OnPageStackReactivatedEventHandler,
new EventHandler<PageStackReactivatedEventArgs>(this.ShellPageManager_OnPageStackReactivated)); this._shellPageManagerCallback.OnResumePageRequestEventHandler = (EventHandler<ResumePageRequestEventArgs>)
Delegate.Combine(this._shellPageManagerCallback.OnResumePageRequestEventHandler,
new EventHandler<ResumePageRequestEventArgs>(this.ShellPageManager_OnResumePageRequest)); this._shellPageManagerCallback.OnInvokeReturningEventHandler = (EventHandler<InvokeReturningEventArgs>)
Delegate.Combine(this._shellPageManagerCallback.OnInvokeReturningEventHandler,
new EventHandler<InvokeReturningEventArgs>(this.OnInvokeReturning)); this._shellPageManager = new ShellPageManager(info.LastInstanceId, info.HostWnd, this._shellPageManagerCallback); this._shellPageManager.OnObscurityChangeEventHandler += new EventHandler<ObscurityChangeEventArgs>
(this._host.ShellPageManager_OnObscurityChange); this._shellPageManager.OnLockStateChangeEventHandler += new EventHandler<LockStateChangeEventArgs>
(this._host.ShellPageManager_OnLockStateChange); this._shellPageManager.PauseSupported = true; this._quirkShouldNotAllowBackgroundNavigation = QuirksMode.ShouldNotAllowBackgroundNavigation(); this._quirkShouldCallOnNavigatingFromPageForExternalNav =
QuirksMode.ShouldCallOnNavigatingFromPageForExternalNavigations(); this._quirkShouldForceTextBindings = QuirksMode.ShouldForceTextBindings(); ChooserListener.Initialize(); } |
这里主要是定义了一个_cacheRequiredPages和设置了host和Frame的值为传入的PhoneApplicationFrame对象。这时,PhoneApplicationFrame和NavigationService相互引用对方,和面绑定了一些事件。而PhoneApplicationFrame构造函数继续执行,Dispatcher.BeginInvoke(a)来执行Frame.Load方法。
internal void Load() { if (!this._loaded) { this._navigationService.InitializeJournal(); this._navigationService.InitializeContentLoader(); this._navigationService.InitializeNavigationCache(); this._loaded = true; if (!IsInDesignMode()) { UriMapperBase uriMapper = this.UriMapper; if (!this._navigationService.Resume()) { string uriString = this.ApplyDeepLinks(); if (uriString != null) { this.Navigate(new Uri(uriString, UriKind.Relative)); } else if (this._deferredNavigation != null) { this.Navigate(this._deferredNavigation); this._deferredNavigation = null; } else if (this.Source != null) { this.Navigate(this.Source); } else if (uriMapper != null) { Uri uri = new Uri(string.Empty, UriKind.Relative); Uri uri2 = uriMapper.MapUri(uri); if ((uri2 != null) && !string.IsNullOrEmpty(uri2.OriginalString)) { this.Navigate(uri); } } } } else if (this.Source != null) { base.Content = string.Format(CultureInfo.InvariantCulture,
Resource.Frame_DefaultContent, new object[] { this.Source.ToString() }); } else { base.Content = typeof(Frame).Name; } } } |
我们仔细看看这个方法。
A. 执行了3个初始化的方法,InitializeJournal初始化历史记录相关的对象,InitializeNavigationCache初始化缓存相关的对象;
InitializeContentLoader初始化内容加载相关的对象
B. InitializeContentLoader中生成了一个PageResourceContentLoader对象,它是
Silverlight 框架中当前唯一的 INavigationContentLoader实现。他的作用就是用来加载要显示的xaml页面,这里是为Frame加载Page做准备,后面会继续介绍。
C. 然后我们看到一些列的Navigate方法。记得上一篇中我们谈到过为什么没有调用Navigate或者设置Source属性,还能显示MainPage.xaml的问题。是的,就在这里,在这里调用了Navigate方法。到底是那一个方法呢?我们在这里做个小实验。
//NavigationPage="MainPage.xaml" 删除这个属性 <Tasks> <DefaultTask Name ="_default"> </Tasks> |
修改WMAppMainfest.xml文件。这样就没有默认的页面了,我们在App中加入对Source的设置和Navigate调用。
RootFrame = new PhoneApplicationFrame(); RootFrame.Source = new Uri("/Page1.xaml", UriKind.Relative); RootFrame.Navigate(new Uri("/Page2.xaml", UriKind.Relative)); RootFrame.Navigated += CompleteInitializePhoneApplication; |
运行程序发现程序正常显示了Page2.xaml页面,那么说明第一个条件判断的ApplyDeepLinks方法是从配置文件中获取NavigationPage的方法。具体如何获得的这里就不深究了,有兴趣自己看源码吧,通过调试发现是在Host对象的TaskPage属性中。这时Frame就导航到了指定的页面。此时也完成了导航框架的初始化。那么系统如何获得Page2的Uri的呢?这个后面解释。
private void InitializePhoneApplication() { if (phoneApplicationInitialized) return; RootFrame = new PhoneApplicationFrame(); RootFrame.Navigated += CompleteInitializePhoneApplication; RootFrame.NavigationFailed += RootFrame_NavigationFailed; phoneApplicationInitialized = true; } private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e) { // Set the root visual to allow the application to render if (RootVisual != RootFrame) RootVisual = RootFrame; // Remove this handler since it is no longer needed RootFrame.Navigated -= CompleteInitializePhoneApplication; } |
我们在回头看看上一篇留下的疑问,为什么这里要绑定Navigated事件。现在应该明白了,在PhoneApplicationPhone的构造函数中,调用了Navigate方法导航到配置文件中的MainPage.xaml,当导航完成时,就吧Frame设置到VisualRoot,这时就能把Frame中的Page显示出来了。这里可能会有个疑问,在构造函数中调用Navigation,但在对象构造完才绑定Navigated事件,这能行吗?
在构造函数中调用的是下面的方法:
Deployment.Current.Dispatcher.BeginInvoke(a); |
这方法实际是把操作发送UI线程执行,但是目前我们还在执行App的构造函数,所有只有构造好App对象UI才能处理这个Load操作,这是我个人看法不知道对不对,有待在研究。
2 Frame加载XAML文件
框架初始化完成后,执行Load方法中的Navigate,让Frame导航到指定页面,那么导航到这个页面是怎么加载这个页面的呢?
我们先看看Frame的Navigate方法到底是怎么执行的。
public bool Navigate(Uri source) { if (this._loaded) { return this._navigationService.Navigate(source); } this._deferredNavigation = source; return true; } |
我们发现塔实际是调用NavigationService的方法,实际上Frame和NavigationService中有很多同名的方法,实际都是在NavigationService类中实现的,他才是实际负责导航和加载页面的。这也和我们开始标注的Frame的4点特性吻合。
这里还有个_deferredNavigation字段,表示延迟导航。实际这个我们在上面的例子中留下的疑问。上面的例子我们删除掉配置文件中的MainPage.xaml,而增加了调用RootFrame.Navigate方法显示了Page2,这时并没有真的导航,应为Frame还没有初始化好,只是包Uri保存到了这个字段中。所以返回True不代表完成导航。等App构造完成,执行Load方法时没有获取配置文件,而_deferredNavigation字段又有值,就导航到了Page2。
public bool Navigate(Uri source) { Action a = null; Uri uri = source; NavigationMode mode = NavigationMode.New; PerfUtil.BeginLogMarker(MarkerEvents.TH_PAGE_NAVIGATION, "Page navigation: " + ((uri == null) ? "" : uri.ToString())); try { JournalEntry journalEntry = new JournalEntry(uri.OriginalString, uri); this.Journal.AddHistoryPoint(journalEntry); return true; } catch (Exception exception) { if (this.RaiseNavigationFailed(uri, exception)) { throw; } return true; } } |
以上代码是NavigationService.Navigate方法的实现,只保留了关键代码。其中将传入的Uri保存到了一个JournalEntry的实体类中,此类是表示后退或前进导航历史记录中的一个条目。我们把加载的Page都用AddHistoryPoint保存起来。
internal void AddHistoryPoint(JournalEntry journalEntry) { Guard.ArgumentNotNull(journalEntry, "journalEntry"); this._shellPagePending = this._shellPageManager.CreatePage(journalEntry.Source.ToString()); if (this._shellPagePending == null) { throw new InvalidOperationException("Unable to create ShellPage"); } this._shellPagePending.ShellPageCallback.OnNavigateAwayEventHandler
+= new EventHandler<NavigateAwayEventArgs>(this.ShellPage_NavigatedAway); this._shellPagePending.ShellPageCallback.OnNavigateToEventHandler
+= new EventHandler<NavigateToEventArgs>(this.ShellPage_NavigatedTo); this._shellPagePending.ShellPageCallback.OnRemoveEventHandler
+= new EventHandler<RemoveEventArgs>(this.ShellPage_RemovedPage); this._shellPagePending.ShellPageCallback.OnBackKeyPressInternalEventHandler
+= new EventHandler<BackKeyPressEventArgs>(this.ShellPage_BackKeyPressed); this._shellPageManager.NavigateTo(this._shellPagePending); this.IsBusy = true; } |
通过CreatePage创建了一个ShellPage对象,这是一个未公开的类型,所以没有资料,但是应该是和界面有关,这里不深入。然后注册了相关的事件,最后调用了NavigateTo方法。但是此方法看不到具体实现。根据这里情况推测应该是跳转到指定的ShellPage页面。这个时候会触发Journal对象的OnNavigateAwayEvent事件,执行绑定的方法:
private void ShellPage_NavigatedAway(object sender, NavigateAwayEventArgs args) { if (!args.IsExternal) { if ((args.Direction == Direction.Back) && (this._backStack.Count > 0)) { this.IsBusy = false; this._lastRemovedEntry = this._currentEntry; this._currentEntry = this._backStack.Pop(); NavigationMode back = NavigationMode.Back; this.UpdateObservables(this._currentEntry, back); } } else { this.OnNavigatedExternally("", new Uri("app://external/", UriKind.Absolute),
(args.Direction == Direction.Back) ? NavigationMode.Back : NavigationMode.New); } } |
此方法中包含一个名为UpdateObservables方法,此方法是
private void UpdateObservables(JournalEntry currentEntry, NavigationMode mode) { bool isPaused = null == currentEntry.PageInstance; this.OnNavigated(currentEntry.Name, currentEntry.Source, mode, isPaused); } |
OnNavigate触发了在NavigateService中InitializeJournal()注册的Journal的Navigated事件
this._journal.Navigated += new EventHandler<JournalEventArgs>(this.Journal_Navigated) |
最后执行绑定的NativagetService中的Journal_Navigated方法:
private void Journal_Navigated(object sender, JournalEventArgs args) { this.NavigateCore_ContinueNavigation(args.Uri, args.NavigationMode, args.IsPaused); } private bool NavigateCore_ContinueNavigation(Uri uri, NavigationMode mode, bool isPagePaused) { try { Uri mappedUri = this.GetMappedUri(uri); Uri mergedUriAfterMapping = this.GetMergedUriAfterMapping(mappedUri); Uri mergedUri = this.GetMergedUri(uri); this._currentNavigation = new NavigationOperation(mergedUriAfterMapping, mergedUri, uri, mode); this.IsNavigationInProgress = true; if (!isPagePaused && (mode == NavigationMode.Back)) { PhoneApplicationPage reusedPage = this._journal.CurrentEntry.PageInstance; this.Host.Dispatcher.BeginInvoke(delegate { this.CompleteNavigation(reusedPage, mode); }); } else { if (!isPagePaused && (mode != NavigationMode.New)) { throw new InvalidOperationException("Invalid NavigationMode, only New and Back are supported"); } this._contentLoader.BeginLoad(this._currentNavigation.Uri, new AsyncCallback
(this.ContentLoader_BeginLoad_Callback), this._currentNavigation); } } catch (Exception exception) { if (this.RaiseNavigationFailed(uri, exception)) { throw; } } return true; } |
实际上执行的就是NavigateCore_ContinueNavigation
方法,我们注意到 ._contentLoader.BeginLoad()方法,这里用到了我们前面Frame初始化说到的PageResourceContentLoader对象来加载指定XAML文件。
以上就是Frame加载XAML文件的全部过程。从Navigate到实际的NavigateCore_ContinueNavigation之间很负责,涉及了一些没有公开的类型。但实际意图很简单,就是对要导航的页面进行历史记录操作。
三 页面的显示
上面介绍了XAML被PageResourceContentLoader对象加载到Frame。那么XAML是如何显示到界面上的呢?
Silverlight 导航系统使用此类作为其默认的内容加载程序。此类是
INavigationContentLoader 的默认实现,且此类的实例是 Frame.ContentLoader
属性的默认值。虽然您通常会加载 Page 实例,Silverlight 导航系统要求导航目标应为 UserControl
实例。Page 类派生自 UserControl 类,并提供附加导航支持
以上是MSDN对PageResourceContentLoader的解释,实际总用就是加载应用程序包(.xap
文件)中对应于给定 URI 的页。我们根据源码也看到Navigate方法最终是通过此对象来加载URI页面。
this._contentLoader.BeginLoad(this._currentNavigation.Uri, new AsyncCallback
(this.ContentLoader_BeginLoad_Callback), this._c |
我们看看BeginLoad方法的实现
public override IAsyncResult BeginLoad(Uri uri, AsyncCallback userCallback, object asyncState) { SendOrPostCallback d = null; Action a = null; PageResourceContentLoaderAsyncResult result = new PageResourceContentLoaderAsyncResult(uri, asyncState); if (uri == null) { result.Exception = new ArgumentNullException("uri"); } if (SynchronizationContext.Current != null) { if (d == null) { d = delegate (object args) { BeginLoad_OnUIThread(userCallback, result); }; } SynchronizationContext.Current.Post(d, null); } else { if (a == null) { a = delegate { BeginLoad_OnUIThread(userCallback, result); }; } Deployment.Current.Dispatcher.BeginInvoke(a); } return result; } |
实际是调用了BeginLoad_OnUIThread方法,而里面又调用了GetLocalXaml方法来加载本地的XAML文件,而且获得了xclass属性指定的类型。最后使用Activator.CreateInstance创建了此XAML文件类对象的实例,保存到result中。
private static void BeginLoad_OnUIThread(AsyncCallback userCallback, PageResourceContentLoaderAsyncResult result) { if (result.Exception != null) { result.IsCompleted = true; userCallback(result); } else { try { string pagePathAndName = UriParsingHelper.InternalUriGetBaseValue(result.Uri); string localXaml = GetLocalXaml(pagePathAndName); if (string.IsNullOrEmpty(localXaml)) { result.Exception = new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
Resource.PageResourceContentLoader_NoXAMLWasFound, new object[] { pagePathAndName })); } else { string xClass = GetXClass(localXaml); if (string.IsNullOrEmpty(xClass)) { try { result.Content = XamlReader.Load(localXaml); } catch (Exception exception) { result.Exception = new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
Resource.PageResourceContentLoader_XAMLWasUnloadable,
new object[] { pagePathAndName }), exception); } } else { Type typeFromAnyLoadedAssembly = GetTypeFromAnyLoadedAssembly(xClass); if (typeFromAnyLoadedAssembly == null) { result.Exception = new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
Resource.PageResourceContentLoader_TheTypeSpecifiedInTheXClassCouldNotBeFound,
new object[] { xClass, pagePathAndName })); } else { result.Content = Activator.CreateInstance(typeFromAnyLoadedAssembly); } } } } catch (Exception exception2) { result.Exception = exception2; } finally { result.IsCompleted = true; if (userCallback != null) { userCallback(result); } } } } |
这里BeginLoad是用异步的方式来加载,避免UI线程等待。而加载完成后执行回调方法 ContentLoader_BeginLoad_Callback
private void ContentLoader_BeginLoad_Callback(IAsyncResult result) { DependencyObject obj2 = null; Uri uriBeforeMapping = null; try { NavigationOperation asyncState = result.AsyncState as NavigationOperation; NavigationOperation operation2 = this._currentNavigation; if ((operation2 != null) && (operation2.Uri == asyncState.Uri)) { uriBeforeMapping = operation2.UriBeforeMapping; obj2 = this._contentLoader.EndLoad(result) as DependencyObject; if (!(obj2 is UserControl)) { throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
Resource.NavigationService_ContentIsNotAUserControl,
new object[] { (obj2 == null) ? "null" : obj2.GetType().ToString(), "System.Windows.Controls.UserControl" })); } PhoneApplicationPage p = obj2 as PhoneApplicationPage; if (p != null) { this._journal.CompletePendingShellPage(p); Frame host = this.Host; p.InternalVisibleRegionChanged +=
new EventHandler<VisibleRegionChangeEventArgs>(host.OnVisibleRegionChanged); Frame frame2 = this.Host; p.BeginOrientationChanged +=
new EventHandler<OrientationChangedEventArgs>(frame2.OnBeginOrientationChanged); Frame frame3 = this.Host; p.BeginLayoutChanged +=
new EventHandler<OrientationChangedEventArgs>(frame3.OnBeginLayoutChanged); Frame frame4 = this.Host; p.InternalBackKeyPress +=
new EventHandler<CancelEventArgs>(frame4.OnBackKeyPress); } JournalEntry.SetNavigationContext(obj2, new NavigationContext
(UriParsingHelper.InternalUriParseQueryStringToDictionary(asyncState.Uri, true))); obj2.SetValue(NavigationServiceProperty, this); this.CompleteNavigation(obj2, asyncState.Mode); } } catch (Exception exception) { if (this.RaiseNavigationFailed(uriBeforeMapping, exception)) { throw; } } } |
加载完成后,从result中获得PhoneApplicationPage对象,也就是之前加载的XAML文件。然后设置了导航的Context内容,这个用来页面间传值的。接下来
obj2.SetValue(NavigationServiceProperty, this)方法设置了NavigationServiceProperty依赖属性。这个属性和导航时的空引用异常有关。而
CompleteNavigation方法用来完成导航。
this.RaiseNavigated(content, uriBeforeMapping, mode, null != existingContentPage, existingContentPage, newContentPage); |
其中一段代码RaiseNavigated出发了Frame的Navigated事件,也就是会执行我们在App中的CompleteInitializePhoneApplication方法。此时我们已经获得了要显示页面的Page对象。
我们知道在WPF中有对象树和可视数的概念,而在Silverlight上只有对象树。
RootVisual属性是设置显示的元素,把Frame设置为RootVisual,此时就会把Frame加载到对象树上,而前面提到过,Frame初始化时绑定了Loaded事件,这个事件此时会被执行。
private void Frame_Loaded(object sender, RoutedEventArgs e) { this.Load(); } |
实际上她执行的也是Load方法,这个方法已经在PhoneApplicationFrame的构造函数中执行过了,有_loaded字段标记,也就不会在执行了。
public UIElement RootVisual { get { return (XcpImports.Application_GetVisualRoot() as UIElement); } [SecuritySafeCritical] set { XcpImports.CheckThread(); if ((value == null) || !XcpImports.DependencyObject_IsPointerValid(value)) { throw new InvalidOperationException(Resx.GetString("Application_InvalidRootVisual")); } XcpImports.Application_SetVisualRoot(value); this._rootVisual = value; } } |
RootVisual的实现看不到,这里贴的是Silverlight的实现。这里用到了XcpImports,这个对象上一篇文章介绍过了,最终是使用Core
presentation framework的方法设置了RootVisual,通过Frame得到当前的Page,这时Presentation
Core会根据当前的对象树生成显卡可以识别的三角形,最终显示到屏幕上。这方面内容可以参考WPF Presentation
四 PhoneApplicationPage和NavigateService
前面介绍了Frame框架的初始化和第一个页面导航加载到显示的过程。我们已经知道,实际负责页面导航的是NavigateService这个类。但只有使用了Frame框架才能完成导航。而在PhoneApplicationPage中有一个NavigateService属性:
internal static NavigationService GetNavigationService(DependencyObject dependencyObject) { Guard.ArgumentNotNull(dependencyObject, "dependencyObject"); return (dependencyObject.GetValue(NavigationServiceProperty) as NavigationService); } |
他实际是返回了NavigationServiceProperty依赖属性。而这个属性上面提到过,是在初始化时加,BeginLoad加载了XAML后的回调函数中设置了此属性。所以我们可以在Page类中使用下面代码进行导航,也可以获得App类的RootFrame对象来导航。
this.NavigationService.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); (Application.Current as App).RootFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative)); |
另外我们在Page中还有OnNavigateTo和OnNavigateFrom等方法,来控制导航。这些内容将在下一篇文章中介绍。
五 Silverlight的不同之处
同Windows Phone导航不同,我们默认建立一个Silverlight的普通项目是没有添加Frame框架的。我们可以仿照Windows
Phone修改App文件。
private void Application_Startup(object sender, StartupEventArgs e) { RootFrame = new Frame(); RootFrame.Navigate(new Uri("/MainPage.xaml", UriKind.Relative)); this.RootVisual = RootFrame; } |
在程序启动的时候执行,整个过程和前面基本一样。相比Windows Phone,他们的构造函数简单了很多。
public Frame() { base.DefaultStyleKey = typeof(Frame); base.Loaded += new RoutedEventHandler(this.Frame_Loaded); this._navigationService = new NavigationService(this); } internal NavigationService(Frame nav) { this._cacheRequiredPages = new Dictionary<string, Page>(); Guard.ArgumentNotNull(nav, "nav"); this._host = nav; } |
Frame中注册了Loaded事件,和WP不同的是没有在Frame的构造函数中就执行。那么他只能在要显示的时候运行,也就是我们设置了RootVisual属性后才能执行Navigate方法。另外没有配置文件设置导航初始画面,所以我们也必须在这里调用Navigate方法或设置Source属性。
当页面开始呈现,加载了Frame到对象树后触发了Loaded时间, 最终调用了NavigateService.Navigate方法。
public bool Navigate(Uri source) { return this.NavigateCore(source, NavigationMode.New, false, false); } |
这里也和Windows Phone不同,NavigateService中直接调用了NavigateCore方法。此方法实现和Windows
Phone一样,此方法中有用的是NavigateCore_StartNavigation,最终是通过的this._contentLoader.BeginLoad来完成的。然后显示核心把加载的页面显示到屏幕上。
以上是Silverlight的过程,相比Windows Phone简单一些。因为Loaded事件触发的时间不同,所以WP会自动根据配置文件设置在App对象建立后就导航到指定页面;而SL是在加载对象准备呈现时触发,而且没有配置文件,所以必须手动设置一下。这也就解释了我们前一篇的疑问。也是为什么SL模仿WP来创建框架,却不能在Navigated时间中才设置RootVisual。因为此事件需要设置运行了Loaded方法才能触发。
六 总结
本文主要对导航框架源代码进行了简单的解析,了解了程序启动后导航到第一个界面的过程。首先创建了PhoneApplication对象,在此对象构建完成后,调用了Frame的Loaded方法来导航到配置文件指定的URI;然后加载此XAML文件,然后触发Frame的Navigated方法设置RootVisual,此属性会调用显示框架的Native方法,将对象树呈现到屏幕上。
|