65.9K
CodeProject 正在变化。 阅读更多。
Home

Prism for Silverlight/MEF 简单示例。第二部分 - Prism 导航

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (66投票s)

2011 年 2 月 13 日

CPOL

12分钟阅读

viewsIcon

221963

downloadIcon

3822

Prism for Silverlight/MEF 简单示例教程。第二部分 - Prism 导航

下载 PrismTutorialSamples_PART2.zip - 285.46 KB

引言

这是 Prism for Silverlight/MEF 简单示例教程的第二部分。第一部分请访问 Prism for Silverlight/MEF 简单示例。第一部分 - Prism 模块,第三部分请访问 Prism for Silverlight/MEF 简单示例。第三部分 - 模块间的通信

在本部分中,我将介绍 Prism 区域导航(Region Navigation),它允许在 Prism 区域中更改显示的或激活的视图。(关于区域定义和示例,请参阅本教程的第一部分)。

区域导航对于通过根据应用程序的当前状态在应用程序屏幕的不同位置更改显示的视图来正确利用应用程序的“实际空间”至关重要。

Prism 中的区域导航功能允许根据视图的类名加载视图到区域中,传递导航参数,决定是否显示视图,在需要时取消导航,并记录导航操作。

除了“第一部分”中列出的先决条件(C#、MEF 和 Silverlight)之外,还需要理解 MVVM 模式才能理解“第二部分”中的一些示例。

区域导航概述和示例

简单区域导航示例

此示例的代码位于 SimpleRegionNavigation.sln 解决方案下。它演示了基本的导航功能。示例应用程序的 Module1 有两个视图:Module1View1 和 Module1View2。这两个视图都在 Module1Impl.Initialize 方法中与“MyRegion1”区域注册(视图发现用于将这些视图与区域关联)。

        public void Initialize()
        {
            TheRegionManager.RegisterViewWithRegion
            (
                "MyRegion1", 
                typeof(Module1View1)
            );
            
            TheRegionManager.RegisterViewWithRegion
            (
                 "MyRegion1", 
                 typeof(Module1View2)
            );
        }

两个视图都显示一条消息(一个 TextBlock 指定视图的名称)和一个切换到另一个视图的按钮。Module1View1 的前景是红色,而 Module1View2 的前景是绿色。

有两种方法可以导航到区域中的视图:

  1. 使用 RegionManager 的 RequestNavigate 方法。
  2. 使用 Region 的 RequestNavigate 方法。
这两种方法非常相似,只是 RegionManager.RequestNavigate 函数将区域名称作为其第一个参数,而 Region 的方法则不需要。

为了演示这两种方法,Module1View1 使用 RegionManager.RequestNavigate 函数导航到 Module1View2。

            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View2", UriKind.Relative),
                PostNavigationCallback
            );

而 Module1View2 使用 Region.RequestNavigate 导航到 Module1View1。

            _region1.RequestNavigate
            (
                new Uri("Module1View1", UriKind.Relative),
                PostNavigationCallback
            );

可以看到,这两种方法都需要一个相对 URI 参数,**其字符串匹配我们要导航到的视图的名称**。

这两种方法都需要一个导航后的回调作为它们的最后一个参数。这是因为某些导航实现可能是异步的,所以如果你想确保某些代码在导航结束后执行,就把它放在导航后的回调中。在我们的例子中,我们想显示一个模块化的 MessageBox,告知导航是否成功。

   
        void PostNavigationCallback(NavigationResult navigationResult)
        {
            if (navigationResult.Result == true)
                MessageBox.Show("Navigation Successful");
            else
                MessageBox.Show("Navigation Failed");
        }

然而,很多时候并不需要导航后的回调,这时可以使用如下简单的 lambda 表达式代替:

a => { }

到目前为止,我们已经介绍了位于 Module1View1.xaml.cs 文件中的 Module1View1 视图的代码。然而,Module1View2 的功能更为复杂。我们可以看到 Module1View2 类实现了 IConfirmNavigationRequest 接口。IConfirmNavigationRequest 扩展了 INavigationAware 接口。

INavigationAware 接口有三个方法:

        bool IsNavigationTarget(NavigationContext navigationContext);
        void OnNavigatedFrom(NavigationContext navigationContext);
        void OnNavigatedTo(NavigationContext navigationContext);

IsNavigationTarget 方法允许导航目标通过返回“false”来声明它不想被导航。当使用注入视图时,此方法非常有用,如下面所示。

OnNavigatedFrom 方法在离开一个视图之前被调用。它允许对已离开的视图进行任何导航前处理。在 Module1View2 的情况下,我们显示一个消息框。

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            MessageBox.Show("We are within Module1View2.OnNavigatedFrom");
        }

OnNavigatedTo 方法在导航到视图之后被调用。它允许对已导航到的视图进行任何导航后处理。在 Module1View2 的情况下,会显示一个消息框。

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            MessageBox.Show("We are within Module1View2.OnNavigatedTo");
        }

以上方法 IsNavigationTargetOnNavigatedFromOnNavigatedToINavigationAware 接口(IConfirmNavigationRequest 扩展了该接口)的一部分,只有当视图类实现了 INavigationAware 时才能使用。然而,ConfirmNavigationRequest 方法只属于 IConfirmNavigationRequest,要使用它,需要实现 IConfirmNavigationRequest 接口。

ConfirmNavigationRequest 方法在从当前视图导航之前被调用。它给你最后一次取消导航的机会。它的第二个参数是一个名为“continuationCallback”的委托,该委托接受一个布尔值作为参数。调用 continuationCallback(true) 将允许导航继续,而调用 continuationCallback(false) 将取消导航。

Module1View2 中,我们显示一个消息框,询问用户是否继续导航或取消它,而“continuationCallback”的参数取决于用户是点击了 OK 还是 Cancel 按钮。

        
        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            MessageBoxResult messageBoxResult =
                MessageBox.Show("Should Navigate from Current View?", "Navigate", MessageBoxButton.OKCancel);

            bool shouldNavigateFromCurrentView = messageBoxResult.HasFlag(MessageBoxResult.OK);

            continuationCallback(shouldNavigateFromCurrentView);
        }

尽管我们使用了一个阻塞了控件流程直到用户点击按钮的模块化窗口,但也可以使用非模块化控件来继续或取消导航——我们只需将“continuationCallback”参数传递给它,并根据用户的操作调用 continuationCallback(true)continuationCallback(false)

应用程序启动时的样子如下:

点击“Navigate to Next View”按钮后,我们会收到一个弹出窗口,指示我们正在 Module1View2.OnNavigatedTo 函数中。点击 OK 后,我们会收到导航成功的导航后回调消息。点击 OK 后,新视图显示在屏幕上。

如果我们现在点击“Navigate to Next View”按钮,我们会首先触发 Module1View2.ConfirmNavigationRequest 方法,该方法会显示一个弹出窗口,询问是否继续导航或取消。如果点击 OK,导航到 Module1View1 将成功继续;否则,导航失败,我们仍停留在 Module1View2。(如果导航成功,我们会收到两个额外的模块化弹出窗口——一个指示我们正在 Module1View2.OnNavigatedFrom 函数中,另一个通知导航成功;如果导航失败,我们会收到一个消息弹出窗口告知此情况。)

练习:创建一个类似的演示(请参阅 “Prism for Silverlight/MEF 简单示例教程第一部分”,了解如何为 Silverlight Prism 应用程序和模块创建项目,以及如何使用启动器将模块加载到应用程序中)。

练习:创建一个类似的演示,将两个视图放在不同的模块中,而不是放在同一个模块中。(由于一些人在此练习中遇到困难,我添加了另一个示例,演示了两个位于不同模块中的视图之间的导航:NavigationBetweenTwoDifferentModules.zip)。

通过 ViewModel 进行简单导航

在上面的示例中,Module1View2 视图实现了 IConfirmNavigationRequest 接口,因此它的 IsNavigationTargetOnNavigatedFromOnNavigatedToConfirmNavigationRequest 方法参与了导航过程。然而,更好的方法是(如果遵循 MVVM 模式)在 ViewModel 中做出所有导航决策。因此,Prism 提供了一种方式让 ViewModel 实现 INavigationAwareIConfirmNavigationRequest 接口(而不是视图)。

规则如下:如果视图实现了 INavigationAwareIConfirmNavigationRequest 接口之一,那么在导航期间将调用视图相应的的方法。如果视图没有实现,但视图的 DataContext 实现了,那么将使用 DataContext 的相应函数,这正如我们在 NavigationViaViewModel.sln 解决方案下的示例所演示的那样。

Module1View1 与之前的示例几乎相同(除了它不弹出消息窗口)。

Module1View2 不实现 IConfirmNavigationRequest。相反,在构造函数中,它将其 DataContext 属性设置为 View2Model 类型的新对象。

            // we are setting the DataContext property of
            // Module1View2 view
            View2Model view2Model = new View2Model();
            DataContext = view2Model;

View2Model 是一个非视觉(正如 ViewModel 应有的)类,它实现了 IConfirmNavigationRequest

    
    public class View2Model : IConfirmNavigationRequest
    {
        // we use this event to communicate to the view
        // that we are starting to navigate from it
        public event Func<bool> ShouldNavigateFromCurrentViewEvent;

        #region INavigationAware Members

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            return true;
        }

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
           
        }

        public void OnNavigatedFrom(NavigationContext navigationContext)
        {
            
        }

        #endregion

        #region IConfirmNavigationRequest Members

        public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool> continuationCallback)
        {
            bool shouldNavigateFromCurrentViewFlag = false;

            if (ShouldNavigateFromCurrentViewEvent != null)
                shouldNavigateFromCurrentViewFlag = ShouldNavigateFromCurrentViewEvent();

            continuationCallback(shouldNavigateFromCurrentViewFlag);
        }

        #endregion
    }

我们使用 ViewModel 的 ShouldNavigateFromCurrentViewEvent 事件来通知视图已开始离开当前视图的导航。视图在视图的构造函数中连接了一个事件处理器到此事件。

     view2Model.ShouldNavigateFromCurrentViewEvent += 
                new Func<bool>(view2Model_ShouldNavigateFromCurrentViewEvent);

与上一个示例一样,View2Model.view2Model_ShouldNavigateFromCurrentViewEvent 显示一个模块化弹出窗口,询问用户是否要继续或取消导航。然后,根据用户的选择,它向 ViewModel 返回一个布尔值,并且 View2Model.ConfirmNavigationRequest 的“continuationCallback”参数会使用相应的值来指定是继续还是取消导航。

练习:创建一个类似的演示。

导航参数示例

如果一个视图或 ViewModel 实现了 INavigationAwareIConfirmNavigationRequest 接口,导航就可以将参数作为导航 URL 的一部分传递给这些函数。如上所述,URL 应包含要导航到的视图类的名称。这个类名后面可以跟一个“?”字符和键值对,其中键用“=”字符与值分隔,对之间用“&”字符分隔。

NavigationParameters.sln 解决方案包含一个示例,其中视图 Module1View1 导航到自身,但带有不同的参数。

示例窗口的外观如下:

您可以在上面的文本框中输入任何文本,然后按“Copy via Navigation”按钮,文本将被复制到下面的文本块中。

“Copy via Navigation”按钮具有以下回调:

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            if (TheTextToCopyTextBox.Text == null)
                TheTextToCopyTextBox.Text = "";

            // navigate passing the paramter "TheText" 
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View1?TheText=" + TheTextToCopyTextBox.Text, UriKind.Relative),
                a => { }
            );
        }

请注意,完整的 URL 将是:“Module1View1?TheText=<text-to-copy>”。

Module1View1 视图实现了 INavigationAware 接口,因此,在导航过程结束时将调用其自身的 OnNavigatedTo 函数。在此函数中,我们从 navigationContext 参数获取 UriQuery 字典,并从 UriQuery 字典中获取与“TheText”键对应的如下值:

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            TheCopiedTextTextBlock.Text = navigationContext.UriQuery["TheText"];
        }

当然,在实际应用中,RequestNavigateOnNavigatedTo 函数可能位于不同的视图甚至不同的模块中,并且可以执行比复制文本更复杂的、需要导航参数的任务。

练习:创建一个类似的演示。

使用 IsNavigationTarget 方法导航到注入的视图

在视图注入的情况下,一个应用程序中可能存在多个相同类型的视图。在这种情况下,仅根据包含视图类名称的 URL 进行导航将不起作用,因为所有这些视图都具有相同的视图类。Prism 使用 bool INavigationAware.IsNavigationTarget(NavigationContext) 方法来指定在这种情况下导航到哪个视图。

在导航到注入的视图时,将调用指定类型的区域中的每个视图的 INavigationAware.IsNavigationTarget 方法,直到其中一个返回“true”(或直到所有视图都返回“false”)。INavigationAware.IsNavigationTarget 方法返回“true”的视图将被导航到。

相应的示例位于 InjectedViewNavigation.sln 解决方案下。与之前的示例不同,应用程序的 Shell.xaml 文件中的区域控件表示为一个 ListBox

        <ListBox HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 prism:RegionManager.RegionName="MyRegion1">
        </ListBox>

因此,它显示连接到区域的每个视图,并且导航到视图会使该视图在列表框中被选中。

视图由 Module1View1 类定义。它有一个 ViewID 属性,可以唯一地标识视图对象。该类还有一个静态方法 CreateViews(),该方法创建 5 个具有不同 ViewID(从 1 到 5)的视图,并将创建的视图与“MyRegion1”区域关联。

        public static void CreateViews(IRegionManager regionManager)
        {
            IRegion region = regionManager.Regions["MyRegion1"];

            for (int i = 1; i <= MAX_VIEW_ID; i++)
            {
                Module1View1 view = new Module1View1 { ViewID = i };
                view.TheRegionManager = regionManager;
                region.Add(view);
            }
        }

每个视图都有一个按钮来导航到下一个视图。按钮点击具有以下事件处理程序:

        void NextViewButton_Click(object sender, RoutedEventArgs e)
        {
            NextViewButton.IsEnabled = false;
            TheRegionManager.RequestNavigate
            (
                "MyRegion1",
                new Uri("Module1View1?ViewID=" + NextViewID, UriKind.Relative),
                a => { }
            );
        }

正如你所见,我们导航到“Module1View1”视图,并传递“ViewID”参数,该参数设置为指向 NextViewID 属性返回的值。

NextViewID 返回当前视图的 ViewID 加一,除非是集合中的最后一个视图,此时它会返回到 ViewID=1。

        int NextViewID
        {
            get
            {
                if (ViewID < Module1View1.MAX_VIEW_ID)
                    return ViewID + 1;

                return 1;
            }
        }

IsNavigationTarget 方法将作为导航 URL 一部分接收的“ViewID”参数与视图的 ViewID 属性进行比较。如果它们相同,则返回 true(这意味着我们导航到此视图);如果它们不同,则返回 false(不导航到该视图)。

        public bool IsNavigationTarget(NavigationContext navigationContext)
        {
            string viewIDString = navigationContext.UriQuery["ViewID"];

            int viewID = Int32.Parse(viewIDString);

            bool canNavigateToThisView = (viewID == ViewID);

            return canNavigateToThisView;
        }

可以看到,通过按视图的“Navigate to Next View”按钮,我们确实切换到了下一个视图。将 NextViewID 属性更改为返回每第二个视图后,我们可以看到列表中的每第二个视图被选中。如果我们将应用程序项目中的 Shell.xaml 文件中的区域控件更改为 ContentControl 而不是 ListBox,导航将显示导航到的视图而不是选择它。

练习:创建一个类似的演示。

使用导航日志实现撤销/重做功能

导航功能还包括撤销/重做功能,如本示例所示。该示例位于 NavigationJournal.sln 解决方案下。

此示例与上一个示例非常相似,只是每个注入的视图都多了两个按钮:“Back”按钮和“Forward”按钮。“Back”按钮允许撤销上次操作,“Forward”按钮则重做上次撤销的操作。

只有当相应的操作可行时,“Back”和“Forward”按钮才会被启用。

撤销-重做功能来自 RegionNavigationJournal,而 RegionNavigationJournal 又可以通过 RegionNavigationService 访问。我们可以在 OnNavigatedTo 方法中的 navigationContext 参数中获取到导航服务的引用。

        public void OnNavigatedTo(NavigationContext navigationContext)
        {
            // getting a reference to navigation service:
            if (_navigationService == null)
                _navigationService = navigationContext.NavigationService;

            ...
        }

这是“Back”按钮如何连接到相应的撤销功能:

        void BackButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoBack();
         
            ...
        }

“Forward”按钮的处理器调用的是 _navigationService.Journal.GoForward() 方法。

按钮启用/禁用功能基于“Back”和“Forward”按钮的 _navigationService.Journal.CanGoBack_navigationService.Journal.CanGoForward 属性。似乎将按钮启用/禁用功能添加到 OnNavigatedTo 方法体中(在进行撤销/重做时调用)是很合乎逻辑的,但可惜它在导航日志状态更新之前就被调用,因此 CanGoBackCanGoForward 属性没有正确的值。为了解决这个问题,我不得不找出当前视图是哪个,方法是根据日志中当前条目的 Uri 属性。我们从 Uri 获取 ViewID,并设置相应视图的“Back”和“Forward”按钮上的 IsEnabled 属性。

       void UpdateIfCurrentView()
        {
            if (_navigationService == null)
                return;

            string uriStr = _navigationService.Journal.CurrentEntry.Uri.OriginalString;

            int lastEqualIdx = uriStr.LastIndexOf('=');

            string viewIDStr = uriStr.Substring(lastEqualIdx + 1);

            int viewID = Int32.Parse(viewIDStr);

            if (viewID != this.ViewID)
                return;

            BackButton.IsEnabled = _navigationService.Journal.CanGoBack;
            ForwardButton.IsEnabled = _navigationService.Journal.CanGoForward;
        }

UpdateIfCurrentView 方法被设置为每个创建视图的静态 JournalUpdatedEvent 事件的事件处理程序。

        public Module1View1()
        {
            InitializeComponent();

            ...

            JournalUpdatedEvent += UpdateIfCurrentView;

            ...

        }

在调用 _navigationService.Journal.GoBack()_navigationService.Journal.GoForward() 方法后,“Back”和“Forward”按钮的点击事件处理程序会调用 JournalUpdatedEvent

        void BackButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoBack();
            DisableButtons();

            JournalUpdatedEvent();
        }

        void ForwardButton_Click(object sender, RoutedEventArgs e)
        {
            _navigationService.Journal.GoForward();

            DisableButtons();

            JournalUpdatedEvent();
        }

重要提示:日志的撤销/重做功能仅在一个区域内工作。如果有多个区域,每个区域都有自己的撤销/重做序列。

练习:创建一个类似的演示。

结论

在本次教程的第二部分中,我用简单的小示例详细描述了 Prism 的导航功能。如果您能为本文投票并留下评论,我将不胜感激。我很想听听您对改进和扩展本教程内容的建议。

历史

2011 年 2 月 15 日 - 代码已更改为使用最新的 Prism dll(这导致了 API 的重大更改:INavigationAwareWithVeto 接口被 IConfirmNavigationRequest 取代,CanNavigateTo()IsNavigationTarget() 取代,并且 OnNavigatedFrom 方法已添加到 INavigationAware 接口。感谢 jh1111 指出我的 API 已过时。)
© . All rights reserved.