使用 Cinch V2 将 Silverlight 导航应用程序转换为 MVVM






4.92/5 (5投票s)
如何使用 Cinch V2 将 Silverlight 导航应用程序转换为 MVVM。
引言
尽管 Sacha Barber 在其各种文章中(在此 CodeProject 上搜索 Cinch 文章)非常详细地解释了 Cinch 的工作原理,但对于使用 Silverlight 的企业级 (LOB) 应用程序,关于 Cinch 的报道非常少。大多数,如果不是全部,企业级应用程序都需要某种形式的页面间导航,而且由于 Silverlight 团队已经为导航创建了一些项目模板,我认为展示一种将导航项目转换为使用 MVVM 模式的方法将会很有用。特别是,我们正在使用 Cinch V2,尽管可以非常轻松地对其进行调整以与其他框架(如 MVVM Light)配合使用。
背景
正如我所说,大多数,如果不是全部,Silverlight 中的企业级应用程序都需要某种页面(视图)之间的导航。所以,让我们看看使用 Cinch V2 将导航项目转换为 MVVM 以用于其主页面和导航框架是多么容易。这正是我最初非常挣扎,不得不放弃,直到我对 Cinch 和 MVVM 更熟悉后才重新开始研究的。
有关 Cinch 的文档,请从 Sacha Barber 关于如何使用该框架的文章开始。
我不会深入探讨为什么您应该使用 MVVM 模式,因为这里 CodeProject 和网络上的其他地方有无数的文章。所以,如果您对 MVVM 不感兴趣,请在此停止阅读。
我研究了各种 MVVM 框架,但选择了 Cinch,因为 Sacha 对其框架的所有方面都进行了非常好的文档记录,它使用 MEF,能够显示设计时数据和运行时数据,并且采用视图优先的方法。此外,它还具有与 WPF 配合使用的附加优势。
那么,MEF 的重要性是什么?好吧,首先,请阅读以下关于使用托管可扩展性框架的 10 个理由,然后访问 Marlon Grech 的网站并了解MEFedMVVM(它驱动 Cinch 中的 MEF 部分)。我认为您将看到这为构建大型 Silverlight 应用程序带来了巨大的好处。
入门
首先,前往 CodePlex 网站下载Cinch V2 的源代码并构建解决方案,以获取您将需要的两个 DLL:Cinch.SL.dll 和 MEFedMVVM.SL.dll。
现在,在 Visual Studio 2010 中,请按照以下步骤操作
- 使用导航项目模板创建一个新的 Silverlight 导航应用程序
- 向应用程序添加这四个文件夹
- 控件
- Libs
- 模型
- ViewModels
- Cinch.SL
- MEFedMVVM.SL
- System.ComponentModel.Composition
- System.ComponentModel.Composition.Initialization
如下图所示
将上面提到的两个 DLL 添加到 Windows Explorer 中的libs文件夹,然后使用“添加引用”对话框添加对它们以及以下 DLL 的引用
现在,解决方案资源管理器中 Silverlight 应用程序的“引用”部分应如下图所示。
现在,打开App.xaml.cs并添加这些 using
子句,然后像下面一样更改 Application_Startup
方法,而将 Application_UnhandledException
方法保持其原始状态。
using Cinch;
using System.ComponentModel.Composition;
using System.Reflection;
namespace CinchNavigation
{
public partial class App : Application
{
public App()
{
this.Startup += this.Application_Startup;
this.UnhandledException += this.Application_UnhandledException;
InitializeComponent();
}
private void Application_Startup(object sender, StartupEventArgs e)
{
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
this.RootVisual = new MainPage();
}
现在App.xaml.cs的开头应为
那么,上面代码中发生了什么?好吧,CinchBootStrapper 启动了 Cinch 系统,我们传入了一个程序集列表,供 MEF 查找要匹配的 Exports 和 Imports。稍后将详细介绍这一点。在这种情况下,我们传入了当前的程序集 (XAP) 文件。但是,我们可以扩展此列表以包含其他 XAP 文件,例如,如果我们动态下载了额外的 XAP 文件。有关如何执行此操作,请参阅 Cinch 文档。
编译并运行应用程序以检查是否没有错误。(您还看不到任何不同。)
那么,我们需要做什么才能将应用程序转换为使用 MVVM?
- 为 Main、Home 和 About 页面创建 ViewModel,并将它们链接到相应的页面(在 MVVM 术语中称为 View)。
- 在 MainPageViewModel 中,创建一个方法,我们可以将其连接到 MainPage 上 Navigation Frame 的
Navigated
事件。 - 在 MainPageViewModel 中,创建一个方法,我们可以将其连接到 MainPage 上 Navigation Frame 的
NavigationFailed
事件。 - 在
Navigated
方法中,更新超链接按钮,使其状态更改为“ActiveLink”或“InactiveLink”状态。
前三项不是问题,因为我们可以使用 Cinch 的 EventToCommandTrigger
行为在 XAML 中绑定到我们的事件。但最后一项是主要的绊脚石,因为按照良好的 MVVM 风格(无紧密耦合),我们不应该有任何指向 View(Page)的链接,并且現状下,我们需要在导航发生后访问 View 来设置 Hyperlinks
状态属性。那么答案是什么?
解决方案
好吧,我们需要一种动态创建超链接列表的方法,但要添加一个额外的属性 CurrentState
。然后,我们需要将值绑定到 ViewModel 中我们新的 MvvmHyperlink
控件的 Uri
、Content
和 CurrentState
属性。我们还需要在 Hyperlink
之间创建一个分隔符,并显示它,除了列表中的第一个链接。
我实现此目标的方式如下
- 通过创建一个名为
MvvmHyperlink
的新控件来扩展Hyperlink
类,该类通过添加一个名为CurrentState
的新依赖属性来扩展Hyperlink
控件。 - 创建一个类(
NavItemInfo
)来存储我们将绑定到MvvmHyperlink
控件的属性。 - 用
ItemsControl
和数据模板替换主页面 XAML 中的超链接列表,以容纳我们的新控件。 - 在
MainPageViewModel
的构造函数中,创建一个新的NavItemInfo
项目的可观察集合,并将它们绑定到ItemsControl
。 - 将
Navigated
和NavigationFailed
事件连接到 ViewModel 中的 Commands。 - 最后,删除
MainPage
代码隐藏文件中的事件处理程序。
让我们从新的 Hyperlink 控件开始。向Controls文件夹添加一个名为 MvvmHyperlink
的新类文件。然后添加以下代码
public class MvvmHyperlink : HyperlinkButton
{
public static readonly DependencyProperty CurrentStateProperty =
DependencyProperty.Register("CurrentState", typeof(object),
typeof(MvvmHyperlink),
new PropertyMetadata(CurrentStatePropertyChanged));
public object CurrentState
{
get { return (object)GetValue(CurrentStateProperty); }
set { SetValue(CurrentStateProperty, value); }
}
private static void CurrentStatePropertyChanged(DependencyObject o,
DependencyPropertyChangedEventArgs e)
{
MvvmHyperlink hyp = o as MvvmHyperlink;
if (hyp != null)
{
hyp.OnCurrentStatePropertyChanged((object)e.NewValue);
VisualStateManager.GoToState(hyp,(string)e.NewValue, true);
}
}
private void OnCurrentStatePropertyChanged(object newValue)
{
CurrentState = newValue;
}
}
这里不是详细介绍依赖属性的地方,我将留给读者自己研究。但它将创建我们可以绑定的 CurrentState
属性。
接下来,我们需要新的 NavItemInfo
类。所以在Models文件夹下,创建一个名为 NavItemInfo
的新类。然后添加以下代码
using Cinch;
namespace CinchNavigation.Models
{
public class NavItemInfo : ViewModelBase
{
public bool SeperatorVisible { get; set; }
public string PageUri { get; set; }
public string ButtonContent { get; set; }
public string CurrentState { get; set; }
}
}
现在我们需要开始处理 MainPage
XAML。首先,在 XAML 的顶部添加一些引用
xmlns:controls="clr-namespace:CinchNavigation.Controls"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.SL"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
meffed:ViewModelLocator.ViewModel="MainPageViewModel"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
那么,这里发生了什么?好吧,首先我们有一个对Controls文件夹的引用。然后后面的两个引用是 Cinch 和 MEF。然后,MEFed 行将我们的 Page(View)与我们的 ViewModel 联系起来。最后,我们引用 Interactivity DLL 来用于将事件连接到命令的行为。
现在我们需要修改导航框架 XAML,使用 Cinch 内置的用于将 UI 事件连接到 ViewModel 中命令的行为,来添加 Navigated
和 NavigationFailed
事件处理程序的行为。
<navigation:Frame x:Name="ContentFrame"
Style="{StaticResource ContentFrameStyle}"
Source="/Home">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Navigated">
<CinchV2:EventToCommandTrigger Command="{Binding Navigated}" />
</i:EventTrigger>
<i:EventTrigger EventName="NavigationFailed">
<CinchV2:EventToCommandTrigger Command="{Binding NavigationFailed}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<navigation:Frame.UriMapper>
<uriMapper:UriMapper>
<uriMapper:UriMapping Uri=""
MappedUri="/Views/Home.xaml" />
<uriMapper:UriMapping Uri="/{pageName}"
MappedUri="/Views/{pageName}.xaml" />
</uriMapper:UriMapper>
</navigation:Frame.UriMapper>
</navigation:Frame>
现在我们需要修改链接区域,使其包含一个 ItemsControl
,并带有一个用于显示我们的 Hyperlink 控件的数据模板,并将其绑定到将在 ViewModel 中构建的链接列表。
<Border x:Name="LinksBorder"
Style="{StaticResource LinksBorderStyle}">
<StackPanel x:Name="LinksStackPanel"
Style="{StaticResource LinksStackPanelStyle}">
<ItemsControl x:Name="NavItems"
ItemsSource="{Binding Path=NavItemsInfo}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Name="LayoutRoot"
Orientation="Horizontal">
<Rectangle Name="separator"
Style="{StaticResource DividerStyle}"
Visibility="{Binding SeperatorVisible}" />
<controls:MvvmHyperlink x:Name="hlink"
Style="{StaticResource LinkStyle}"
NavigateUri="{Binding PageUri}"
TargetName="ContentFrame"
Content="{Binding ButtonContent}"
CurrentState="{Binding CurrentState}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
现在我们准备对 MainPageViewModel
进行编码。首先,我们需要一些 using
子句。
using System.ComponentModel.Composition;
using System.Windows.Navigation;
using Cinch;
using MEFedMVVM.ViewModelLocator;
using System.Collections.ObjectModel;
using CinchNavigation.Models;
接下来,我们需要用一些属性标记我们的类,Cinch 使用 MEFedMVVM 需要这些属性来将我们的视图(MainPage
)与我们的 ViewModel(MainPageViewModel
)匹配。
[ExportViewModel("MainPageViewModel")]
[PartCreationPolicy(CreationPolicy.Shared)]
public class MainPageViewModel : ViewModelBase
ExportViewModel
将允许 MEF 将其加载到内存中,并且 Shared
的 PartCreation 将创建一个单例。NonShared
将在每次调用时创建一个新实例。但是,由于我们只需要为我们的主页面创建一个实例,所以 Shared
是理想的。最后,我们的类继承自 ViewModelBase
,这是 Cinch 的 ViewModel 基类。然后,我们需要一些公共属性。
public SimpleCommand<Object, EventToCommandArgs> Navigated { get; private set; }
public SimpleCommand<Object, EventToCommandArgs> NavigationFailed { get; private set; }
public ObservableCollection<NavItemInfo> NavItemsInfo { get; private set; }
首先,是我们由行为触发的命令,最后是我们 NavItemInfo
项目的集合。现在,在我们的构造函数中,我们将设置我们的链接,并创建我们的命令。
public MainPageViewModel()
{
NavItemsInfo = new ObservableCollection<NavItemInfo>();
NavItemsInfo.Add(new NavItemInfo { ButtonContent = "home",
PageUri = "/Home", SeperatorVisible = false,
CurrentState = "ActiveLink" });
NavItemsInfo.Add(new NavItemInfo { ButtonContent = "about",
PageUri = "/About", SeperatorVisible = true,
CurrentState = "InActiveLink" });
//add any more pages here, note: seperator is only hidden on the first link
// Setup the commands
Navigated = new SimpleCommand<Object, EventToCommandArgs>(ExecuteNavigatedCommand);
NavigationFailed = new SimpleCommand<Object,
EventToCommandArgs>(ExecuteNavigationFailedCommand);
}
正如您所见,我们在这里设置了两个链接:Home 和 About,并设置了它们的初始状态。然后我们继续创建新的命令并将它们连接到两个私有方法。这两个方法的代码是
private void ExecuteNavigatedCommand(EventToCommandArgs args)
{
NavigationEventArgs navArgs = (NavigationEventArgs)args.EventArgs;
foreach (var link in NavItemsInfo)
{
if (link.PageUri.ToString().Equals(navArgs.Uri.ToString()))
{
link.CurrentState = "ActiveLink";
}
else
{
link.CurrentState = "InActiveLink";
}
}
}
private void ExecuteNavigationFailedCommand(EventToCommandArgs args)
{
NavigationFailedEventArgs navArgs = (NavigationFailedEventArgs)args.EventArgs;
navArgs.Handled = true;
ChildWindow errorWin = new ErrorWindow(navArgs.Uri);
errorWin.Show();
// note: we could use the Cinch IChildWindowService for the error window
}
我们逐个分析。ExecuteNavigatedCommand
接受一个 EventToCommandArgs
参数,其中包含 EventArgs
,在这种情况下是 NavigationEventArgs
,它的一个属性是导航到的页面的 URI。通过它,我们可以遍历 NavitemsInfo
列表并更新状态。现在,因为我们使用的是实现 NotifyPropertyChanged
的 ObservableCollection
,所以我们的 UI 将会更新!ExecuteNavigationFailedCommand
返回一个 NavigationFailedEventArgs
对象,其中同样有一个 URI,这次是失败页面的 URI。基于此,我们复制了现有代码。
现在您可以最终删除或注释掉 MainPage
代码隐藏文件中的事件处理程序。如果您没有输入任何错误,您的程序现在应该可以运行了。
最后
源项目还为 Home 和 About 页面设置了 ViewModel。它们遵循与 MainPage 相同的模式,因此我将把它留给读者作为练习来实现,或者获取源文件!此外,它还包含一个非常基础的测试项目,只是向您展示如何开始使用 MVVM 进行单元测试。非常欢迎改进建议;或者,如果您认为我的方法完全错误,并且有更好的方法,请告诉我。