互聯網的興起,造就和培養了一種新的用戶交互界面 —— Page & Navigation。無論是前進、後退還是頁面,都完全是一個全新的門類,不同於以往的 SDI/MDI。WPF 或者是它的簡化版 Silverlight 都不可避免地遵從了這種改良的 B/S 模式,使用 URI 來串接 UI 流程。
NavigationService、Page、Hyperlink、Journal(日志/歷史記錄) 是 WPF 整個導航體系的核心。NavigationService 提供了類似 IE Host 的控制環境,Journal 可以記錄和恢復相關 Page 的狀態,我們通常會選用的宿主方式包括:Browser(XBAP) 和 NavigationWindow。
1. NavigationWindow
NavigationWindow 繼承自 Window,不知什麼原因,我並沒有在 VS2008 "New Item..." 中找到相關的條目,只好自己動手將一個 Window 改成 NavigationWindow。
Window1.xaml
<NavigationWindow x:Class="Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300" WindowStartupLocation="CenterScreen"
Source="Page1.xaml">
</NavigationWindow>
Source 屬性指定了該窗口的默認頁面,當然,我們還要修改一下 Window1.xaml.cs 裡的基類。
public partial class Window1 : NavigationWindow
{
public Window1()
{
InitializeComponent();
}
}
創建一個 Page1.xaml,我們就可以像普通 Window 那樣添加相關的控件和操作。
2. Hyperlink
超鏈接應該是我們最熟悉的一種導航方式。
Page1.xaml
<Page x:Class="Learn.WPF.Page1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page1">
<Grid>
<TextBlock>
<Hyperlink NavigateUri="Page2.xaml">Page2</Hyperlink>
</TextBlock>
</Grid>
</Page>
NavigateUri 相當於 "Html a.href",當然我們也可以使用 Hyperlink.Click 事件,然後使用 NavigationService 來完成導航操作。
Page1.xaml
<Hyperlink Click="Hyperlink_Click">Page2</Hyperlink>
Page1.xaml.csprivate void Hyperlink_Click(object sender, RoutedEventArgs e)
{
this.NavigationService.Navigate(new Uri("Page2.xaml", UriKind.Relative));
}
Hyperlink 還支持 "test.htm#name" 這樣的導航定位方式,滾動頁面直到某個特定名稱的控件被顯示。Hyperlink 的另外一個實用屬性是 Command,我們可以使用 NavigationCommands 中創建的一系列靜態成員來執行一些常用操作。
<Hyperlink Command="NavigationCommands.Refresh">Refresh</Hyperlink>
<Hyperlink Command="NavigationCommands.BrowseBack">BrowseBack</Hyperlink>
<Hyperlink Command="NavigationCommands.BrowseForward">BrowseForward</Hyperlink>
3. NavigationService
很多時候我們都需要使用 NavigationService 代替 Hyperlink.NavigateUri,比如非默認構造的 Page,動態確定目標頁面等等。我們可以使用 Page.NavigationService 或者 NavigationService.GetNavigationService() 獲得 NavigationService 的實例引用 (別忘了添加 using System.Windows.Navigation)。
public partial class Page1 : Page
{
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
var page2 = new Page2();
page2.label1.Content = "Beijing 2008!";
this.NavigationService.Navigate(page2);
}
}
除了 Navigate(),還可以使用 NavigationService 的兩個屬性完成導航切換操作。
//this.NavigationService.Content = page2;
this.NavigationService.Source = new Uri("Page2.xaml", UriKind.Relative);
NavigationService 提供了大量的方法和時間來管理相關導航操作。
日志: AddBackEntry、RemoveBackEntry。
載入: Navigate、Refresh、StopLoading。
切換: GoBack、GoForward。
事件: Navigating(新導航請求時觸發,可取消導航)……
我們也可以使用 Application 的相關事件來處理導航過程。
4. Journal
Journal 相當於 WebBrowser.History,它包含兩個數據棧用來記錄前進和後退頁面的顯示狀態,每個相關 Page 都會對應一個 JournalEntry。日志狀態自動恢復僅對單擊導航條上前進後退按鈕有效。
5. Page
有關 Page 本身的使用並不是本文的內容,我們此處關心的是它在導航過程中的生命周期。在 WPF 中,Page 注定是個短命鬼,無論我們使用導航還是後退按鈕都會重新創建 Page 對象實例,然後可能是日志對其恢復顯示狀態。也就是說日志只是記錄了 Page 相關控件的狀態數據,而不是 Page 對象引用(默認情況下)。
有兩種方式來維持一個 Page 引用。第一種就是我們自己維持一個 Page 引用,比如使用某個類似 Application.Properties 這樣的容器。
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
var page2 = Application.Current.Properties["page2"] as Page2;
if (page2 == null)
{
page2 = new Page2();
page2.label1.Content = DateTime.Now.ToString();
Application.Current.Properties["page2"] = page2;
}
this.NavigationService.Navigate(page2);
//this.NavigationService.Content = page2;
}
另外一種就是設置 Page.KeepAlive 屬性,這樣一來日志會記錄該 Page 的引用,當我們使用前進後退按鈕時,將不會再次創建該 Page 的對象實例。
<Page x:Class="Learn.WPF.Page2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Page2" Loaded="Page_Loaded"
KeepAlive="True">
</page>
有一點需要注意:該方法僅對前進後退等日志操作有效。如果我們使用 HyperLink.NavigateUri 或 NavigationService.Navigate() 導航時依舊會生成新的頁面實例,並可能代替日志中最後一個同類型的對象引用記錄。另外,當多個頁面存在循環鏈接時,會導致多個頁面實例被日志記錄,造成一定的內存浪費。
6. Frame
Frame 的作用和 HTML 中的 IFrame 類似,我們可以用它在一個普通的 Window 或 Page 中嵌套顯示其他的 Page。
<Window x:Class="Learn.WPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
<Frame Source="Page1.xaml"></Frame>
</Grid>
</Window>
默認情況下,Frame 會嘗試使用上層頁面(Page)或窗體(NavigationWindow)的日志,當然我們也可以使用 JournalOwnership 屬性強行讓 Frame 使用自己的日志導航。
<Frame Source="Page1.xaml" JournalOwnership="OwnsJournal"></Frame>
Frame 的另外一個作用就是可以導航到 HTML 頁面,我們可以把它當作一個嵌入式 IE WebBrowser 來使用。
<Frame Source="http://www.rainsts.net" />
7. PageFunction<T>
WPF 提供了一個稱之為 PageFunction 的 Page 繼承類來實現類似 HTML showModal 的功能。我們可以用它來收集某些數據並返回給調用頁,當然這個封裝其實非常簡單,我們完全可以自己實現,無非是提供一個類似 OnReturn 的方法實現而已。泛型參數 T 表示返回數據類型。
Page1.xaml.cs
public partial class Page1 : Page
{
private void Hyperlink_Click(object sender, RoutedEventArgs e)
{
var modal = new PageFunction1();
modal.Return += (s, ex) => this.label1.Content = ex.Result.ToString();
this.NavigationService.Navigate(modal);
}
}
PageFunction1.xaml.cspublic partial class PageFunction1 : PageFunction<int>
{
private void button1_Click(object sender, RoutedEventArgs e)
{
OnReturn(new ReturnEventArgs<int>(DateTime.Now.Millisecond));
}
}
使用步驟:
(1) 創建 PageFunction<T> 對象實例,當然我們可以使用含參構造傳遞額外的數據;
(2) 調用 PageFunction<T>.OnReturn() 方法用來返回一個特定的結果包裝對象 —— ReturnEventArgs<T>;
(3) 調用者通過訂閱 PageFunction<T>.Return 事件獲取這個返回結果。
MSDN 中還提到用 OnReturn(null) 來表示 Cancel, ~~~~ 說實話,個人覺得這個 PageFunction 從命名到執行邏輯都有點別扭,難道僅僅是因為 Page 特殊的實例構造邏輯?我們也可以使用 Application.Properties + Page 來實現一個非關聯耦合的 showModel 邏輯,只不過不那麼 "標准" 罷了。
有一點需要提醒一下:我們應該及時解除對 FunctionPage<T>.Return 的訂閱,我上面的例子和 MSDN 一樣偷懶了。