相當多的WPF程序都有着豐富的頁面和功能,如何使程序在不同頁面間轉換並降低資源占用,選擇適合自己的導航框架就很重要了。最近花了一點時間做了一個簡單的導航框架,並在這個過程中對Window、Page、UserControl有了更多的認識。
1.“簡單粗暴”的TabControl
如果你的應用程序很簡單,各個頁面間沒有直接的聯系,那么TabControl就完全可以滿足要求。剛開始學WPF的時候,頁面導航我只會用TabControl(其他不懂),自帶Tab切換效果。
1 <Window> 2 <TabControl> 3 <TabItem Header="頁面A"> 4 <Frame Source="PageA.xaml"></Frame> 5 </TabItem> 6 <TabItem Header="頁面B"> 7 <Frame Source="PageB.xaml"></Frame> 8 </TabItem> 9 </TabControl> 10 </Window>
效果如上圖(設置Frame的屬性NavigationUIVisibility=”Hidden”可以隱藏導航圖標)。如果是再多一級子頁面呢?那就再加一層TabControl。但使用TabControl做頁面導航的問題是,繪制窗口時,所有子頁面都將被實例化一遍,尤其是頁面較多時加載速度會變慢,占用資源也相對較高。另外在樣式上將TabItem的Header和Content分離也需要費很大一番功夫。
2.“專注導航”的Frame
WPF中提到頁面導航切換就絕對繞不開Frame,它的導航特性使得其連接Window和Page更加自由。簡單的Frame導航是幾個按鈕加上一個Frame,通過按鈕事件控制Frame的Source屬性。
1 <Window> 2 <Grid> 3 <Grid.RowDefinitions> 4 <RowDefinition Height="40"></RowDefinition> 5 <RowDefinition></RowDefinition> 6 </Grid.RowDefinitions> 7 <WrapPanel VerticalAlignment="Center"> 8 <Button Name="btnA" Height="30" Width="60" Margin="5" Click="btnA_Click">頁面A</Button> 9 <Button Name="btnB" Height="30" Width="60" Click="btnB_Click">頁面B</Button> 10 </WrapPanel> 11 <Frame Name="frmMain" NavigationUIVisibility="Hidden"></Frame> 12 </Grid> 13 </Window>
1 private void btnA_Click(object sender, RoutedEventArgs e) 2 { 3 //注意:這里使用Navigate,不用Source,具體區別自己可以試試 4 this.frmMain.Navigate(new Uri("PageA.xaml", UriKind.Relative)); 5 } 6 7 private void btnB_Click(object sender, RoutedEventArgs e) 8 { 9 this.frmMain.Navigate(new Uri("PageA.xaml", UriKind.Relative)); 10 }
這樣一個簡單的Frame導航框架就完成了。但是仔細想一想,如果后期增加更多頁面,后台代碼豈不是要加很多Click事件,能不能把這些Click事件合在一起呢?答案是可以的。關鍵就在於執行Click事件時要知道是由哪個導航按鈕觸發的,可以利用控件的Tag屬性實現這一點。代碼修改如下:
1 <Window> 2 <Grid> 3 <Grid.RowDefinitions> 4 <RowDefinition Height="40"></RowDefinition> 5 <RowDefinition></RowDefinition> 6 </Grid.RowDefinitions> 7 <WrapPanel VerticalAlignment="Center"> 8 <Button Tag="PageA" Name="btnA" Height="30" Width="60" Margin="5" Click="btnNav_Click">頁面A</Button> 9 <Button Tag="PageB" Name="btnB" Height="30" Width="60" Click="btnNav_Click">頁面B</Button> 10 </WrapPanel> 11 <Frame Name="frmMain" NavigationUIVisibility="Hidden"></Frame> 12 </Grid> 13 </Window>
cs代碼改為
private void btnNav_Click(object sender, RoutedEventArgs e) { Button btn = sender as Button; this.frmMain.Navigate(new Uri(btn.Tag.ToString()+".xaml",UriKind.Relative)); }
這樣無論添加多少頁面,不需要修改后台方法,只需為導航按鈕添加相應的Tag就可以了。(使用Name屬性或其他屬性也是可以的,有興趣的可以自己試試)
3.互相調用的Window和Page
在復雜一點的WPF程序里,我們往往不僅需要頁面間切換瀏覽,有時也需要相互調用方法,比如說在PageA中調用MainWindow的方法,代碼如下:
在MainWindow.xaml.cs中有一個公共方法:
1 public void CallFromChild(string name) 2 { 3 MessageBox.Show("Hello," + name + "!"); 4 }
在PageA.xam.cs中為其添加一個屬性,使其在實例化后能訪問MainWindow。
1 private MainWindow _parentWin; 2 public MainWindow ParentWindow 3 { 4 get { return _parentWin; } 5 set { _parentWin = value; } 6 }
當頁面切換到PageAxaml,即PageA實例化后,使得ParentWindow=MainWindow;
1 private void btnA_Click(object sender, RoutedEventArgs e) 2 { 3 PageA a = new PageA(); 4 this.frmMain.Content = a; 5 a.ParentWindow = this; 6 }
注意這里頁面導航的方法由this.frmMain.Navigate換成了this.frmMain.Content。然后在PageA就可以添加方法來調用MainWindow中的CallFromChild()方法了。
1 private void btnCall_Click(object sender, RoutedEventArgs e) 2 { 3 ParentWindow.CallFromChild("PageA"); 4 }
4.進階的導航框架
上面我們已經實現了簡單的導航框架,也實現了在Page中調用MainWindow中的方法,但問題也是顯而易見的:每新增一個頁面都要為其添加ParentWindow屬性,而且只有在頁面實例化后為其ParenWindow屬性賦值,才能調用MainWindow中的CallFromChild方法;通用的導航事件btnNav_Click中拿到的只是頁面的Uri字符串,必須將其實例化后作為frmMain的Content。
上述兩個問題從兩個方面解決:創建繼承於Page類的BasePage類,使所有頁面都繼承於BasePage,同時在BasePage中添加屬性ParentWindow;使用反射將頁面的Uri字符串轉為Page實例,同時查找其ParentWindow屬性並賦值為MainWindow。
進階后的全部代碼如下:
MainWindow.xaml
1 <Window x:Class="WPFClient.App.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 5 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 6 xmlns:local="clr-namespace:WPFClient.App" 7 mc:Ignorable="d" 8 Title="MainWindow" Height="480" Width="800"> 9 <Grid> 10 <Grid.RowDefinitions> 11 <RowDefinition Height="50"></RowDefinition> 12 <RowDefinition></RowDefinition> 13 </Grid.RowDefinitions> 14 <Grid.ColumnDefinitions> 15 <ColumnDefinition Width="1*"></ColumnDefinition> 16 <ColumnDefinition Width="3*"></ColumnDefinition> 17 <ColumnDefinition Width="1*"></ColumnDefinition> 18 </Grid.ColumnDefinitions> 19 <WrapPanel Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center"> 20 <Button Tag="Home" Width="40" Height="40" Margin="5" Click="btnNav_Click">首頁</Button> 21 <Button Tag="SimpleChat" Width="40" Height="40" Margin="0,0,5,0" Click="btnNav_Click">內容</Button> 22 </WrapPanel> 23 <Grid Grid.Row="1" Grid.ColumnSpan="3"> 24 <Frame Name="frmMain" NavigationUIVisibility="Hidden"></Frame> 25 </Grid> 26 </Grid> 27 </Window>
MainWindow.xaml.cs
1 namespace WPFClient.App 2 { 3 /// <summary> 4 /// MainWindow.xaml 的交互邏輯 5 /// </summary> 6 public partial class MainWindow : Window 7 { 8 public MainWindow() 9 { 10 InitializeComponent(); 11 Navigate("Home"); 12 } 13 14 15 #region 頁面導航 16 private void btnNav_Click(object sender, RoutedEventArgs e) 17 { 18 Button btn = sender as Button; 19 Navigate(btn.Tag.ToString()); 20 } 21 private void Navigate(string path) 22 { 23 string uri = "WPFClient.App.Views." + path; 24 Type type = Type.GetType(uri); 25 if (type != null) 26 { 27 //實例化Page頁 28 object obj = type.Assembly.CreateInstance(uri); 29 UserControl control = obj as UserControl; 30 this.frmMain.Content = control; 31 PropertyInfo[] infos = type.GetProperties(); 32 foreach (PropertyInfo info in infos) 33 { 34 //將MainWindow設為page頁的ParentWin 35 if (info.Name == "ParentWindow") 36 { 37 info.SetValue(control, this, null); 38 break; 39 } 40 } 41 } 42 } 43 44 #endregion 45 46 //公共方法 47 public void CallFromChild(string name) 48 { 49 MessageBox.Show("Hello," + name + "!"); 50 } 51 52 } 53 }
BasePage.cs
1 namespace WPFClient.App 2 { 3 public class BasePage : Page 4 { 5 #region 父窗體 6 private MainWindow _parentWin; 7 public MainWindow ParentWindow 8 { 9 get { return _parentWin; } 10 set { _parentWin = value; } 11 } 12 #endregion 13 14 } 15 }
Home.xaml
1 <base:BasePage x:Class="WPFClient.App.Views.Home" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 xmlns:local="clr-namespace:WPFClient.App.Views" 7 xmlns:base="clr-namespace:WPFClient.App" 8 mc:Ignorable="d" > 9 <Grid> 10 <Grid.RowDefinitions> 11 <RowDefinition Height="50"></RowDefinition> 12 <RowDefinition></RowDefinition> 13 </Grid.RowDefinitions> 14 <Grid.ColumnDefinitions> 15 <ColumnDefinition Width="3*"></ColumnDefinition> 16 <ColumnDefinition Width="1*"></ColumnDefinition> 17 </Grid.ColumnDefinitions> 18 <WrapPanel Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Center"> 19 <TextBox Name="txtParam" Width="120" Height="30"></TextBox> 20 <Button Name="btnCall" Width="90" Height="30" Margin="5" Click="btnCall_Click">CallApiByGet</Button> 21 </WrapPanel> 22 <Grid Grid.Row="1"> 23 24 </Grid> 25 </Grid> 26 </base:BasePage>
Home.xaml.cs
1 namespace WPFClient.App.Views 2 { 3 /// <summary> 4 /// Home.xaml 的交互邏輯 5 /// </summary> 6 public partial class Home : BasePage 7 { 8 public Home() 9 { 10 InitializeComponent(); 11 } 12 13 private void btnCall_Click(object sender, RoutedEventArgs e) 14 { 15 string param = txtParam.Text; 16 ParentWindow.CallFromChild(param); 17 } 18 19 } 20 }
通過實驗發現,使用這種方案使得Page頁訪問MainWindow中的公共屬性、控件元素或公共變量也是可行的。此外將BasePage的基類從Page改成UserControl也是可以的,畢竟Page就是繼承於UserControl,關於Page和UserControl的區別就不再贅述了。