相当多的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的区别就不再赘述了。