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

使用 Silverlight 3 导航、 Prism 和按需加载

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (16投票s)

2009 年 9 月 4 日

CPOL

9分钟阅读

viewsIcon

116354

downloadIcon

1056

一篇演示如何将 Silverlight 3 导航与 Prism 2 集成(包括模块的按需加载)的文章。

The finished sample

引言

本文演示了一种可能的集成 Silverlight 3 的新导航功能(包括深度链接支持)与 Prism 的一些优势的方法——特别是,松散耦合的模块能够将视图注入到导航页面,并且模块可以按需加载,从而最大限度地减少初始下载量。

背景

复合应用程序指导

2009 年 2 月,Microsoft 发布了 WPF 和 Silverlight 的复合应用程序指导 2.0 版,通常称为 Prism。Prism 提供了关于多个主题的指导和参考,包括多目标定位(WPF 和 Silverlight 之间的代码共享)、UI 组合和模块化。

UI 组合设计概念引入了在区域中显示视图的概念。RegionManager 负责维护 Shell 应用程序中区域的集合。视图包含在松散耦合的模块中,并在初始化时将自身注入到区域中。视图本身不知道区域或它所在的位置。

Silverlight 3 导航框架

Silverlight 3 中引入的导航框架允许开发人员在同一个 Silverlight 应用程序中创建单独的页面,并通过 URL 导航到它们。例如,您的 Silverlight 应用程序可能托管在 http://myapp.com/default.aspx。您可以使用 http://myapp.com/default.aspx#/About 直接导航到“关于”页面。

这种“深度链接”是富互联网应用程序以及 Silverlight 在企业级场景中的采用方面的一大进步。

在本文中,我们将介绍一个 Shell 应用程序的创建过程,其中包含按需下载的松散耦合模块,以及完整的导航页面和深度链接。

先决条件

要遵循本文的演练,您需要一些先决条件

  • Visual Studio 2008 SP1
  • Visual Studio 的 Silverlight 工具
  • Silverlight 3 开发者运行时
  • Microsoft 复合应用程序指导 2009 年 2 月

本文还假定您具备 Silverlight 应用程序、XAML 的基本知识,以及对 Prism 的基本了解。

创建 Shell

我们将从创建一个新的 Silverlight 导航应用程序开始。将应用程序命名为 Prism.Shell,并允许 Visual Studio 创建一个新网站来托管它。

导航应用程序模板为我们提供了一个简单的应用程序 Shell,其中包含一些导航框架和按钮,它们已经为我们连接好了。运行应用程序(当被询问是否修改 web.config 以进行调试时,选择“是”),然后查看结果。

The basic navigation template application

快速查看 MainPage.xaml 会发现 navigation:Frame 部分,其中包含 uriMapper ——这是 Silverlight 导航的基础。框架的内容是在 Views 文件夹下的单独 XAML 文件中定义的。这些位置由 uriMapper 根据输入的 URL 进行映射,并在 navigation:Frame 中渲染内容。在此过程中会触发几个事件,例如 Navigated(或 NavigationFailed),在未更改的 MainPage 中,Navigated 事件用于确保菜单顶部的相关按钮被高亮显示。

添加新的导航页面

在我们的示例中,我们将添加两个带有静态内容的新页面,以及两个允许我们导航到它们的新按钮。稍后在演练中,我们将用 Prism 区域替换新页面的静态内容,这些区域的内容将被动态加载和注入。

打开 MainPage.xaml 并修改 LinksStackPanel,以便我们有多几个按钮

<Border x:Name="LinksBorder" Style="{StaticResource LinksBorderStyle}">
    <StackPanel x:Name="LinksStackPanel" Style="{StaticResource LinksStackPanelStyle}">

        <HyperlinkButton x:Name="Link1" 
                Style="{StaticResource LinkStyle}" 
                NavigateUri="/Home" 
                TargetName="ContentFrame" Content="home"/>

        <Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}"/>
        
        <HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}" 
                NavigateUri="/About" 
                TargetName="ContentFrame" Content="about"/>

        <Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}"/>

        <HyperlinkButton x:Name="Link3" 
                Style="{StaticResource LinkStyle}" 
                NavigateUri="/Module1" TargetName="ContentFrame" 
                Content="module 1"/>

        <Rectangle x:Name="Divider3" Style="{StaticResource DividerStyle}"/>

        <HyperlinkButton x:Name="Link4" Style="{StaticResource LinkStyle}" 
                NavigateUri="/Module2" 
                TargetName="ContentFrame" Content="module 2"/>

    </StackPanel>
</Border>

我们还需要两个新页面供导航按钮导航,所以首先从 Views 文件夹复制一个现有页面并粘贴两个新副本。在示例中,它们被命名为 Module1 和 Module2。请记住重命名代码隐藏文件,并修改 XAML 顶部的类名、代码隐藏中的类名以及构造函数。在新的页面中添加一些不同的内容,这样当我们运行应用程序时,就能看到导航在起作用了。

此时,我们应该借此机会将我们的“MainPage”重命名为“Shell”。这更多是约定俗成,而非必要,但它确实可以减少我们稍后提及“shell”时的混淆。同样,请记住更新类名和构造函数。

集成 Prism

此时,我们的示例应用程序将无法编译,因为 App.xaml.cs 中的 Application_Startup 方法将无法找到 MainPage 来引导应用程序。当添加 Prism 组件时,我们将提供引导程序,引导程序将指导应用程序找到 ModulesCatalog.xaml,在那里它将找到可能需要的模块的详细信息。这样,Shell 应用程序就不需要对任何模块进行硬引用,并且它们可以按需加载。

要连接 Prism,我们需要一些 Prism DLL 的引用

Prism references

添加这些引用后,我们可以添加一个 Bootstrapper.cs 和一个 ModulesCatalog.xaml

using System;
using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;

namespace Prism.Shell
{
    internal class Bootstrapper : UnityBootstrapper
    {
        protected override DependencyObject CreateShell()
        {
            Shell shell = this.Container.Resolve<Shell>();

            Application.Current.RootVisual = shell;
            return shell;
        }

        protected override IModuleCatalog GetModuleCatalog()
        {
            ModuleCatalog catalog = new ModuleCatalog();

            return ModuleCatalog.CreateFromXaml(
                new Uri("Prism.Shell;component/ModulesCatalog.xaml", 
                        UriKind.Relative));
        }
    }
}

XAML

<m:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System; assembly=mscorlib"
    xmlns:m="clr-namespace:Microsoft.Practices.Composite.
             Modularity;assembly=Microsoft.Practices.Composite"
    >

    <m:ModuleInfoGroup Ref="Prism.Module1.xap" InitializationMode="OnDemand">
        <m:ModuleInfo ModuleName="Prism.Module1.InitModule"
                        ModuleType="Prism.Module1.InitModule, 
                        Prism.Module1, Version=1.0.0.0"></m:ModuleInfo>
    </m:ModuleInfoGroup>

    <m:ModuleInfoGroup Ref="Prism.Module2.xap" InitializationMode="OnDemand">
        <m:ModuleInfo ModuleName="Prism.Module2.InitModule"
                        ModuleType="Prism.Module2.InitModule, 
                        Prism.Module2, Version=1.0.0.0"></m:ModuleInfo>
    </m:ModuleInfoGroup>

</m:ModuleCatalog>

既然我们已经实现了自己的引导程序,我们就可以修改 App.xaml.cs 来调用它,而不是直接加载 XAML 页面。

private void Application_Startup(object sender, StartupEventArgs e)
{
    var bootstrapper = new Bootstrapper();
    bootstrapper.Run();
}

此时,应用程序可以再次运行,等待我们包含一些 Prism 区域。在我们的示例中,我们将在每个新页面中添加一个区域。页面将不知道注入的内容是什么,同样,模块将初始化它们的视图并将它们注入到一个区域,而不知道该区域在哪里。

创建模块

模块包含应用程序本身的功能,Shell 只是显示它们的框架,在我们的例子中,也是导航框架。

我们将为这个示例创建一些简单的模块,其中包含一些模块特定的内容,以便在我们导航到不同页面时,清楚地显示从不同位置注入的视图。

向您的解决方案添加一个新的 Silverlight 应用程序项目——我们将称之为 Prism.Module1。在被问到在哪里托管应用程序时,选择您现有的网站,但请确保取消选中“添加一个引用该应用程序的测试页面”选项,因为这并不必要。

添加对 Prism DLL 的一些引用,并删除为您添加的 MainPage.xaml;而是添加一个名为 InitModule.cs 的新类。它的名字并不重要,只需确保 ModulesCatalog.xaml 中对它的引用是正确的。

我们现在可以添加模块的初始化代码了

using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
using Prism.Module1.Views;

namespace Prism.Module1
{
    public class InitModule : IModule
    {
        IUnityContainer _container;
        IRegionManager _regionManager;

        public InitModule(IUnityContainer Container, 
                          IRegionManager RegionManager)
        {
            _container = Container;
            _regionManager = RegionManager;
        }

        public void Initialize()
        {
            RegisterViewsAndServices();
        }

        protected void RegisterViewsAndServices()
        {
            _regionManager.RegisterViewWithRegion("Module1Region", 
                                                  typeof(Module1View));
        }
    }
}

为了简单起见,示例使用了一个纯字符串来指示视图(Module1View)将被注入的区域。我建议将其抽象到一个 enum 或类似的结构中,可能在它自己的项目中——比如 Prism.Infrastructure。请注意,我们还没有创建 Module1View,所以同样,如果您是按照演练进行的,它叫什么名字并不重要,只要记住以后更改它时更新即可。

您还需要删除 App.xamlApp.xaml.csInitModule 将完成我们模块的所有初始化工作。

现在是时候在模块项目中添加一个文件夹(“Views”)并添加一个新的 Silverlight 用户控件(“Module1View”)了。在视图中添加一些内容,以清楚地表明该视图是从哪个模块注入的。

为了提供一个合理的示例,我们将按照相同的思路创建另一个模块。这次命名为 Module2。遵循与 Module1 相同的结构,只需记住一致地重命名类和命名空间。

Solution structure

连接导航

回到 Prism.Shell 项目,打开 Module1.xaml 视图并向其中添加一个 Prism 区域。这将是 Module1 的输出被注入的地方。添加 Prism Regions 代码的命名空间(在此例中为“cal”),并用 Prism 区域替换我们之前包含的静态文本——它采用 ItemsControl 的形式

<navigation:Page x:Class="Prism.Shell.Module1" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;
               assembly=Microsoft.Practices.Composite.Presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:navigation="clr-namespace:System.Windows.Controls;
                      assembly=System.Windows.Controls.Navigation"
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
    Title="About" 
    Style="{StaticResource PageStyle}">

    <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
        <ItemsControl x:Name="Module1Region" 
                      cal:RegionManager.RegionName="Module1Region"/>
    </StackPanel>

</navigation:Page>

对 Module2 重复此操作,并记住相应地更改类名和区域名。

按需加载

此时应用程序可以生成并运行,但模块中的视图尚未被注入。这是因为 Shell 项目没有对它们的硬引用,并且它们在 ModuleCatalog 中被标记为“OnDemand”。Shell 不知道它们,也没有请求它们。

按需加载的关键概念是减少应用程序的初始占用空间。我们可以预先请求模块,以便当页面被导航到时,它们已经将其内容注入到区域中,但这会适得其反。相反,我们需要在导航到相应页面时请求相关的模块。

一种可能的解决方案是实现一个简单的“Module Mapper”——一个静态类,它将导航页面映射到一个模块(或多个模块,页面上可以有多个区域,视图可以从多个模块注入)。当页面被导航到时,我们可以调用“Module Mapper”,它会请求相关的模块。然后模块将初始化并注入其内容。

示例项目使用了这种技术,并且有意限制为每个导航页面一个模块,所以它可以很容易地改进。它们如何映射并不重要,我们只需要一种方法来做到这一点。Navigation 文件夹包含一个类,ModuleMapper.cs

using System.Collections.Generic;

namespace Prism.Shell.Navigation
{
    public static class ModuleMapper
    {
        public static Dictionary<string, string> ModuleMaps { get; set; } 
        
        static ModuleMapper()
        {
            // if any navigation pages have prism regions
            // then put the map to the relevant
            // module here. The module will then be
            // dynamically loaded when necessary.
            ModuleMaps = new Dictionary<string, string>();
            ModuleMaps.Add("/Module1", "Prism.Module1.InitModule");
            ModuleMaps.Add("/Module2", "Prism.Module2.InitModule");
        }
    }
}

Shell.xaml.cs 中,我们修改构造函数以接受 Prism ModuleManager。在 ContentFrame_Navigated 方法中,在突出显示导航按钮之前,我们调用一个 LoadModule 方法,该方法查找模块映射并使用 ModuleManager 来加载所需的模块。显然,这对于我们的示例来说是稍微简化的,例如没有异常处理,但它展示了概念的实际应用。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Practices.Composite.Modularity;

namespace Prism.Shell
{
    public partial class Shell : UserControl
    {
        private IModuleManager _moduleManager;

        public Shell(IModuleManager ModuleManager)
        {
            _moduleManager = ModuleManager;

            InitializeComponent();
        }

        // After the Frame navigates, ensure the HyperlinkButton
        // representing the current page is selected
        private void ContentFrame_Navigated(object sender, 
                                  NavigationEventArgs e)
        {
            LoadModule(e.Uri.ToString());

            foreach (UIElement child in LinksStackPanel.Children)
            {
                HyperlinkButton hb = child as HyperlinkButton;
                if (hb != null && hb.NavigateUri != null)
                {
                    if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))
                    {
                        VisualStateManager.GoToState(hb, "ActiveLink", true);
                    }
                    else
                    {
                        VisualStateManager.GoToState(hb, "InactiveLink", true);
                    }
                }
            }
        }

        private void LoadModule(string uri)
        {
            // if link requires a module then load it
            if (Navigation.ModuleMapper.ModuleMaps.ContainsKey(uri))
            {
                _moduleManager.LoadModule(Navigation.ModuleMapper.ModuleMaps[uri]);
            }
        }

        // If an error occurs during navigation, show an error window
        private void ContentFrame_NavigationFailed(object sender, 
                                  NavigationFailedEventArgs e)
        {
            e.Handled = true;
            ChildWindow errorWin = new ErrorWindow(e.Uri);
            errorWin.Show();
        }
    }
}

运行应用程序,并使用导航按钮导航到新页面。模块将被按需加载,并将它们的视图注入到相关的区域。现在,尝试使用 Silverlight 3 深度链接导航到新页面。应该观察到完全相同的行为,但现在,您可以直接从应用程序外部导航到由动态加载的模块注入的页面。

改进示例

示例有意做得简单,并且有几个明显的改进之处。

  • 改进 ModuleMapper,允许每个导航页面有多个模块,并从配置文件而不是硬编码在类中读取映射。现有的 ModulesCatalog 可用于此。
  • 在单独的 DLL 中定义区域名称,可能是一个处理其他解决方案级任务(如服务定位)的“基础设施”项目。
  • 在模块检索期间添加一个“加载中”指示器。在慢速连接上等待时间可能很长,目前没有任何关于正在发生什么的反馈。

使用示例代码

使用示例代码

  • 解压源代码
  • 在 Visual Studio 中打开解决方案
  • 重新引用 Prism DLL
  • 将启动项目设置为 Prism.Shell.Web
  • 将启动页面设置为 Prism.ShellTestPage.aspx
  • 运行应用程序

为了快速入门,请在您的 C: 盘创建一个名为“Prism Library”的文件夹,并将 Prism DLL 放在其中。示例代码期望在那里找到它们。

历史

  • 原始文章:2009 年 9 月 4 日。
© . All rights reserved.