导航.简单的理解可以是从一个页面跳转到另外一个页面。在传统的ASP.NET网站中这种效果很容易实现。而在Silverlight中我们也同样可以,我们有两种方法来实现这个效果。
第一个选择是使用代码更改页面视图(修改容器Content属性),移除/添加User Control来实现导航,这个方法比较简单、直接代码量也很少。并且在这个过程中还可以加入动画、变形等效果。
第二个选择就是使用Silverlight的导航系统,导航系统包含两个主要的控件:Frame、Page。基本的效果是可以在一个Frame里面切换多个页面(UserControl、Page)。
此次先简单介绍下第一种方法。 简单的页面切换效果:
这个例子,将页面分成上下两部分,上面表示菜单,下面放置一个容器空间用于承载内容。容器控件你可以旋转Border、ScrollViewer、StackPanel、Grid。下面是主页面的代码:
<Grid x:Name="LayoutRoot">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="6*"></RowDefinition>
</Grid.RowDefinitions>
<ListBox Grid.Row="0" SelectionChanged="lstPages_SelectionChanged">
<!--页面列表-->
</ListBox>
<basics:GridSplitter Grid.Row="1" Margin="0 3" HorizontalAlignment="Stretch"
Height="2"></basics:GridSplitter>
<Border Grid.Row="2" BorderBrush="SlateGray" BorderThickness="1"
x:Name="mainFrame" Background="AliceBlue"></Border>
</Grid>
这个例子中使用Border作为容器命名为mainFrame,在页面加载的时候先为容器加入一个页面(为项目添加2个以上UserControl):
Page1 page1 = new Page1();
mainFrame.Child = page1;
这里的容器控件是多选的,你也可使用单行单列的Grid。下面为ListBox加入事件代码:
private void lstPages_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//获取当前的Item
string newPageName = ((ListBoxItem)e.AddedItems[0]).Content.ToString();
//根据名称使用反射创建一个UserControl实例
Type type = this.GetType();
Assembly assembly = type.Assembly;
UserControl newPage = (UserControl)assembly.CreateInstance(
type.Namespace + "." + newPageName);
//加入到容器中.
mainFrame.Child = newPage;
}
至此一个简单的页面切换效果就OK了。这个DEMO的效果图如下:
●使用根视图(Root Visual)
上面是一个简单的例子,这个方法很常见,但不是通用方式。它的小缺陷是整个页面布局已经固定死了,工具栏或页面始终都固定在那里,那如果你想要的是一个全新的页面就不行了。下面对这个小例子进行一下扩展就可以达到我们的目的了。
首先在App.xaml.cs中声明一个Grid:
private Grid rootGrid = new Grid();
然后修改Appliaction_Startup事件代码:
this.RootVisual = rootGrid;
rootGrid.Children.Add(new Page1());
这样只能保证在初始化的时候会有一个页面而不能导航,为此在App.xaml.cs代码中加入一个静态方法,代码如下:
public static void Navigation(UserControl newPage)
{
//获取当前的Appliaction实例
App currentApp = (App)Application.Current;
//修改当前显示页面内容.
currentApp.rootGrid.Children.Clear();
currentApp.rootGrid.Children.Add(newPage);
}
这里要注意 方法的参数是UserControl。这样在你的UserControl中即可添加如下的代码(可以加到button事件中)进行页面切换了:
App.Navigation(new Page2());
new的对象是你的目标页面,不要写错了。就这几句代码又完成了咱们的目的!
●保存页面状态(Cache)
如果你想让用户在返回到历史页面的时候可以页面的修改状态比如用户输入的数据。首先对项目进行一下小小的修改,添加一个名字叫Pages的enmu,用于保存页面名称以便使用字符串产生不必要的问题:
public enum Pages
{
Page1,
Page2
}
下一步是在App.cs代码中加入一个泛型集合用于保存页面:
private static Dictionary<Pages, UserControl> pageCache = new Dictionary<Pages, UserControl>();
其中Key是Pages枚举,Value是UserControl。然后从新定义Naviagte方法:
public static void Navigation(Pages newPage)
{
App currentApp = (App)Application.Current;
if (!pageCache.ContainsKey(newPage))
{
//根据名称使用反射创建目标页面实例,并加入缓存
Type type = currentApp.GetType();
Assembly assembly = type.Assembly;
pageCache[newPage] = (UserControl)assembly.CreateInstance(
type.Namespace + "." + newPage.ToString());
}
currentApp.rootGrid.Children.Clear();
currentApp.rootGrid.Children.Add(pageCache[newPage]);
}
这样在其他的UserControl中调用如下代码就可以进行切换了:
App.Navigation(Pages.Page2);
如果在页面中放置一个文本框然后输入值,跳转到其他页面再切换回来的话就会看到文本框的值依然存在,这是因为UserControl被保存在内存中了。效果图如下:
如果你进行切换就会发现TextBox的值依然存在.这样就实现了简单的缓存.
●页面传值:
关于页面传值我仅仅说一下我的方式,当让网上也有其他的关于页面之间传值的方法。主要是使用独立存储的IsolatedStorageSettings对象,首先在UserControl中创建对象:
private IsolatedStorageSettings appSettings = IsolatedStorageSettings.ApplicationSettings;然后在Button事件中加入如下代码,用于传值:
if (!appSettings.Contains("Page2"))
appSettings.Add("Page2", "UserName");
OK了,在目标页面获取值的方式就简单了.
if (appSettings.Contains("Page2"))
txbShowvalue.Text = "User Name: " + appSettings["Page2"].ToString();
需要注意的一点是这个获取值的代码不要写在页面的构造函数里面,有可能不会触发,原因是在上面对象已经保存在内存中了,但是会触发Loaded事件,因此可以把代码放到这个事件里面。最总效果如下:
你也可以使用同样的方式来给Page1传值。OK..关于第一种页面导航、传值、简单缓存,就介绍完毕..
此次主要说一下如何使用Navigation System进行导航。它包含了两个重要的控件:Frame、Page。其中Frame控件是主要控件因为它负责导航以及显示内容。而Page空间是一个可选的控件,它可以使用普通的UserControl来代替,但两者之间稍有差别,后面会简单说一下。
1.Frame控件
Frame控件也是一个容器控件,它通过Content属性进行修改内容,当然最好是使用Navigate()方法来代替Content属性,因为它既会修改Content属性也会触发事件和保存Frame的日志(历史记录,更改当前浏览器的地址)。下面看一个简单的例子,首先定义一个两行一列的Grid,上面是一个包含了Frame的Border,下面是用来触发导航的按钮:
<UserControl x:Class="FramNavigation.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<Grid>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Border Margin="10" Padding="10" BorderBrush="DarkOrange"
BorderThickness="2" CornerRadius="4">
<navigation:Frame x:Name="mainFrame"></navigation:Frame>
</Border>
<Button Grid.Row="1" Margin="5" Padding="5" HorizontalAlignment="Center"
Content="Go to Page1" Click="btnNavigate_Click"></Button>
</Grid>
</Grid>
</UserControl>
使Frame记得添加System.Windows.Controls.Navigation.dll引用。
然后在项目中创建一个UserControl命名为Page1,并在Main.xaml.cs的按钮事件中加入如下代码:
mainFrame.Navigate(new Uri("/Page1.xaml", UriKind.Relative));
Uri前的“/”表示应用程序的根目录..不可以使用Navigate()方法访问应用程序以外的页面,比如其他网站的页面。同样也可以使用如下代码代替Navigate()方法:
mainFrame.Content = new Page1();
如果你观察就会发现前后的浏览器地址栏是不一样的,你也可以使用Frame.Source属性查看当前Uri..如图:
这是因为只修改Content属性不会触发Navigation事件..这个DEMO最终的效果图如下:
当你使用Navigate()方法更新Frame控件内容时那么目标的xaml页面就会以#隔开附加到当前的地址中,所以前后的地址就如上图所示的,多出了#Page1.xaml部分。
这个特点即有好处也有潜在的危害。本来当你使用Frame来创建导航系统,每个页面都会有一个独立的名称标识,唯一的历史记录以及使用每次重新访问。比如当你重新打开浏览器在初始地址后面加入#Page1.xaml那么你就可以进入到这个页面,同样你也可以把这个完整的地址加入到标签中以便下次访问。这个特性叫做Deep linking.因此使得Silverlight对搜索引擎是友好的。比如你可以创建多个HTML/ASP.NET页面,然后让他们指向同一个XAP,不过URI连接到不同的页面,那么搜索引擎就会为你的程序创建多个索引。
这只是它的一个好处那么下面看问题是什么:
- 如果一个页面中存在多个Frame控件会怎么样?
使用URI片段可以标识一个页面,但是真个URI中并没有标识Frmae,这说明在程序中同事只有一个Frame控件在工作。(程序中包含多个Frame的做法很少见)
但是如果不止一个Frame控件,它们将会同时相应浏览器地址,如果你重写URI进行访问或调用Navigate()方法那么请求的页面就会添加到每一个Frame中。为了避免这种情况,你需要选择一个Frame控件来做为主控件,这个控件会受到浏览器历史地址的影响,而其他的Frame则会负责跟踪自己的导航而不受浏览器影响。为了达到这个目的你需要为每一个副Frame的JournalOwnership 属性设置为OwnJournal,这样的话这些Frame就只能通过代码调用Navigate方法进行导航了。下面是JournalOwnership 属性值的说明(或看MSDN中更详细的介绍):
- 如果页面中并不存在Frame控件会怎么样?
使用URI访问多Frame的程序并不是唯一的问题,还有一个问题是是否能正确的处理用户请求,因为有可能Root visual中并不包含Frame。如果你用代码动态生成用户界面。比如使用代码创建Frame对象或者在进入另外一个页面的时候页面中包含了一个Frame。这种情况下程序会正常运行,不过因为没有frame可用,URI片段会被忽略。
为了避免此类问题你可以简化应用程序确保在启动的时候frame是可用的,并且在Application.Startup中验证请求的地址中是否含有URI片段,验证代码如下:
string fragment = System.Windows.Browser.HtmlPage.Document.DocumentUri.Fragment;
3.安全问题如何?
从另一个角度考虑,也给你的程序留了一个很多的后门。比如用户输入URI访问了一个你不想使用Navigate()方法访问的页面。Silverlight本身并没有提供措施来避免此类问题,因此在你使用导航系统的时候潜在的也给你带来了URI访问的问题。
不过幸好你可以使用以下方法来人为避免。第一个像前面一样设置Frame的JournalOwnership为OwnJournal,那样就可以避免使用URL访问你应用程序的任何页面,同时地址也不会集成到浏览器的历史记录列表中。另外一个更好的方法是使用Navigating事件,这个方法可以验证请求的URI从而有选择性的进行导航,验证代码如下:
void mainFrame_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e)
{
if (e.Uri.ToString().ToLower().Contains("Page1.xaml")) {
e.Cancel = true; }
}
这样就可以在程序执行Navigate()方法后来验证URI是否合法。从而避免用户访问到禁止请求的页面。
4.对历史记录的支持
Frame控件的导航也可以与浏览器进行集成。你每次调用Navigate()方法,Silverlight就会添加一个地址到浏览器的历史记录中(如下图)。使用浏览器的前进/后台按钮或者在历史记录列表中选择一个页面都可以正常的访问历史页面。你可以复制地址,然后重新打开浏览器进行访问,依然是可以的。这样Silverlgiht的Appliaction.Startup事件会重新触发,并加载相应的页面到Frame中。
5.URI Mapping
正如我们看到的,页面是以URI片段方式显示在URI中的,那么你可能不想让用户看到具体的页面名称,并且也不希望.XAML结尾让人感觉到混淆,而使用更简单、易记的名称。那么为了解决这个问题可以使用URI Mapping来定义简单的URI片段。首先需要在资源中加入UriMapper对象,一般是定义在App.xaml中的
<!--xmlns:navigation="clr-namespace:System.Windows.Navigation;assembly=System.Windows.Controls.Navigation"-->
<Application.Resources>
<navigation:UriMapper x:Key="PageMapper">
</navigation:UriMapper>
</Application.Resources>
同时在页面Frame控件设置属性UriMapper:
<navigation:Frame x:Name="mainFrame" UriMapper="{StaticResourcePageMapper}"></navigation:Frame>
现在你就可以在UriMapper中添加映射了:
<navigation:UriMapping Uri="Home" MappedUri="/Page1.xaml" />
再次访问页面然后看看地址栏结尾就会显示成#Home,对了,记得修改导航按钮的事件:
mainFrame.Navigate(new Uri("Home", UriKind.Relative));
否则地址栏是不会变化的。并且在映射地址前面不再需要加入一个“/”,它同样能正常工作并且访问到目标页面。另外一个还可以设置一个页面作为初始页面。
<navigation:UriMapping Uri="" MappedUri="/InitialPage.xaml" />
那么这样的话,当你使用后台按钮回到首页就不会再出现刚才说的那个“No XAML found at the location”的错了。现在是强制的设置初始页面,从某种意义上讲也算是Silverlight的一个BUG。
URI地址映射是支持地址栏参数,在说Page控件的时候会再专门介绍一下..
<navigation:UriMapping Uri="Home/{name}" MappedUri="/Page1.xaml?name={name}" />
6.支持自定义 前进/后退导航按钮.
我们可以设置了Frame控件的JournalOwnership属性来确定它如何与浏览器集成,管理日志。如果设置成OwnJournal那么Frame将自己来管理历史记录,不会与浏览器集成。这种情况就需要你自己来提供前进/后退功能了,一般是使用两个按钮来代替前进/后退。
而如果你的程序需要支持out-ofbrowser application,那么设置自定义的前进后台按钮就很有比较了,因为它是运行在普通的windws窗口中的,不会有浏览器的导航按钮,那怕你并没有设置JournalOwnership属性,并且你可以判断是否是OOB来显示你的导航按钮,你可以将以下代码添加到程序中:
if (App.Current.IsRunningOutOfBrowser)
btnNavigate.Visibility = Visibility.Visible;
这样就可以根据是否为OOB来控制按钮了。并且自定义按钮你也可以加你喜欢的样式或动画效果,并且还可以根据Frame的两个属性来判断当前页面是否是第一页/最后一页来禁用按钮或修改按钮样式:
void mainFrame_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
if (mainFrame.CanGoBack)
btnBack.Visibility = Visibility.Collapsed;
else
btnBack.Visibility = Visibility.Visible;
if (mainFrame.CanGoForward)
btnForward.Visibility = Visibility.Collapsed;
else
btnForward.Visibility = Visibility.Visible;
}
这只是判断按钮显示与否。你也可以修改按钮视觉效果或禁用它们(比如更改颜色、透明度、图片、添加动画),具体怎么做就看自己喜欢了。
7.HyperlinkButton
在前面我们是使用普通按钮来触发导航事件的。一般情况下Silverlight是使用HyperlinkButton来进行导航的,这个按钮使用起来相当的简单,只要设置按钮的NavigateUri属性指向页面的URI或者是在UriMapper中配置的地址都行,代码参考如下:
<StackPanel Margin="5" HorizontalAlignment="Center" Orientation="Horizontal">
<HyperlinkButton NavigateUri="/Page1.xaml" Content="Page 1" Margin="3" />
<HyperlinkButton NavigateUri="/Page2.xaml" Content="Page 2" Margin="3" />
<HyperlinkButton NavigateUri="Home" Content="Home" Margin="3" />
</StackPanel>
在之前的例子中使用的是UserControl来做为页面,但通常的话我们是使用Page控件的,或者自己继承Page类来代替UserControl。因为Page提供了更方便的导航功能以及状态管理。其实,Page类是继承自UserControl的,然后添加了一些成员,一些可重写的方法和四个属性:Title、NavigationService、NavigationContext、NavigationCacheMode。其中Titel属性比较简单,不做多说,其他属性在后面都会有说到。
使用Page控件很简单,和向项目中添加UserControl一样。
1.Page控件属性介绍
每一个Page控件内都会有一个NavigationService属性,这个属性就相当于访问Silverlight导航系统的入口,因为它提供了与Frame对象一样的方法和属性(Navigate()、GoBack()、GoForward(),属性有CanGoBack、CanGoForward、CurrentSource等)。意思就是说在Page里面就可以进行导航了..
- this.NavigationService.Navigate(new Uri("/Page2.xaml", UriKind.Relative));
Page类还含有一个NavigationContext属性用来访问NavigationContext对象。使用这个属性可以获取当前的URL,使用QueryString可以获取URL中的参数。也就是说你可以在跳转页面的时候使用地址栏参数传值。如下:
- string uriText = String.Format("/Product.xaml?id={0}&type={1}",productID, productType);
- mainFrame.Navigate(new Uri(uriText), UriKind.Relative);
这样你就可以传两个值到目标页面了 ..
然后在Priduct.xaml页面你就可以获取到值了:
- int productID, type;
- if (this.NavigationContext.QueryString.ContainsKey("productID"))
- productID = Int32.Parse(this.NavigationContext.QueryString["productID"]);
- if (this.NavigationContext.QueryString.ContainsKey("type"))
- type = Int32.Parse(this.NavigationContext.QueryString["type"]);
当让你还可以使用其他方式传值,比如在存储在Appliaction对象中,或者是使用独立存储都可以实现,因为使用URL参数很容易就会被篡改..
保存页面状态
通常,用户第一次进入页面或者是使用前进后退按钮切换页面,都会重新创建一个对象,当用户离开,对象就会被释放。这种情况下,如果用户输入的有信息,再回到页面就会编程默认值,页面的其他成员也会初始化成默认值。而如果可以存储页面状态的话就不会出现这种情况了。
Silverlight允许使用Page.NavigationCacheMode属性来设置存储策略,这个属性的默认值是Disabled所以不会默认不会存储页面。把属性设置为Required那么页面就会保存到内存中。当用户离开页面再返回的时候就可以看到自己修改的内容依然存在,不过再次回到页面不会触发页面的构造方法,所以如果你在构造函数里写的有逻辑就需要注意了。不过会触发页面的Loaded事件。
NavigationCacheMode的另外一个值是Enabled,如果设置成这个值,那么页面就好与Frame.CacheSize(保存页面的数量)属性关联,加入CacheSize属性设置为10,当第11个页面存储进来的话第一个页面就会被释放。而NavigationCacheMode属性设置为Required属性页面就不会被计算在CacheSize中。这个可以根据自己的需要进行选择。
Page控件的方法
Page类包含了几个方法使你能更加灵活的管理导航。
- OnNavigatedTo():当页面不再是框架中的活动页面时调用。
- OnNavigatingFrom():当页面成为框架中的活动页面时调用。
- OnNavigatedFrom():在页面即将不再是框架中的活动页面时调用。
你可以使用这些方法在离开页面或是访问页面时添加一些自己的逻辑,比如初始化一些参数或是管理其他状态。
关于Page类就说到这里,详细的用法与说明可以查看MSDN..
Navigation Templates
现在已经学会如何使用Frome/Page控件来创建具有导航功能的应用程序,然后美中不足的是视觉效果还具有很多差距。不过你可以模拟别人的实例来不到完善,从而达到自己的效果。另外一个办法是使用现有的模版。
如果你使用vs自带的Silverlight Navigation Appliaction模版创建一个程序的话,会自动给你创建一套默认样式。运行的效果如下图:
样式模板下载 |