XAMLFinance – 跨平台 WPF、Silverlight 和 WP7 应用程序






4.94/5 (99投票s)
本文介绍了 XAML Finance 的开发,这是一个跨平台应用程序,它可以在桌面端使用 Windows Presentation Foundation (WPF),在 Web 端使用 Silverlight,以及在 Windows Phone 7 (WP7) 上运行。
目录
- 概述
- 引言 – 为什么要跨平台?
- 跨平台 XAML 开发
- 跨平台,一种实用方法
- WPF / Silverlight 框架差异
- 广泛差异
- API 级别差异
- 解决方案技术
- 条件编译 (#if)
- 分部类
- 设计模式
- 缺失的框架功能
- 缺失的控件
- 常见陷阱
- 意想不到
- 适应各平台
- XAML Finance
- 架构
- 数据源库
- XAML Finance 应用程序
- MVVM 模式
- 适应 Web
- 适应桌面
- 适应 Windows Phone 7
- 结论
概述
本文介绍了 XAML Finance 的开发,这是一个跨平台应用程序,它可以在桌面端使用 Windows Presentation Foundation (WPF),在 Web 端使用 Silverlight,以及在 Windows Phone 7 (WP7) 上运行。我撰写本文的目的是强调用于桌面、Web 和移动设备的基于 XAML 的技术如何让您共享大量代码,并以经济高效的方式在各种设备上分发您的应用程序。
Windows Phone 7 上的 XAML Finance
您可以在下面观看 WP7 版本的视频
在浏览器中运行的 Silverlight 版 XAML Finance,以及 HTML 内容(也可在线查看):
在桌面运行的 WPF XAML Finance
引言 – 为什么要跨平台?
不久前,软件的主要分发模式是通过软盘或 CD,最终用户将您的应用程序直接安装到他们的台式计算机上。在过去十年中,富互联网应用程序 (RIA) 变得越来越流行,它为我们提供了基于 Web 的分发模式。最近,其他平台和外形尺寸(包括智能手机和平板电脑)的增长导致了复杂的技术和分发模式。
每个平台(桌面、Web、移动)都有其自身的优势和独特的功能可以提供给客户端。现在,应用程序需要在各种平台上交付已成为一项普遍要求。这会带来明显的成本影响!开发 WPF 桌面应用程序所需的技能与基于 Flash 的 Web 应用程序或 iOS 移动应用程序所需的技能大相径庭。随着多平台应用程序的开发通常需要多个独立的开发团队……您实际上是在支付三次(或更多次)开发相同软件的费用!
如果您决定使用 Microsoft 技术来提供桌面、Web 和移动产品,这将使您处于优势地位。对于桌面开发,您可以使用 WPF,而对于 Web 和移动开发,您可以使用 Silverlight(当然,您的移动产品将仅限于 WP7 用户——但嘿,它是一款很棒的手机!)。所有三个框架都共享通用的 API,并允许您使用 .NET 框架进行开发。
另外,我不会在本文中探讨您可能用于跨平台开发的其他技术,最值得注意的是 HTML5。如果您想了解我的想法,我建议阅读以下内容
本文更多是关于代码而非策略,并将专注于 .NET 技术。
WPF 和 Silverlight 都是 .NET 框架系列中的应用程序开发框架。它们的 API 共享许多共同的特性,因此很容易从一种技术过渡到另一种技术。由于这两个框架之间有大量相似之处,您可能想知道为什么它们会有两个?为什么 Microsoft 不仅仅为桌面和 Web 创建一个单一的框架呢?要回答这个问题,我们需要深入了解每个框架的历史。
WPF 于 2006 年发布,代号为 Avalon,它是 .NET 框架的一部分,因此只能在 Windows 机器上运行。WPF 是 Microsoft 在 Windows 桌面开发方面的下一个演进步骤 (C++ / MFC => WinForms => WPF),因此其关键要求是提供构建广泛桌面应用程序所需的工具、控件和 API。它还具有 WinForms 互操作性功能,为 WinForms 应用程序提供了迁移路径,并支持较新的 .NET API(例如 Entity Framework、RIA Services)和旧的(例如 ADO.NET 数据集)。WPF 依赖于客户端安装 .NET 客户端配置文件,这是一个 75 MB 的下载。
Silverlight 满足了与 WPF 不同的需求,它为 .NET 应用程序提供了基于 Web 的分发模型。Silverlight 的特点是下载大小有限,插件只有 5MB(而 .NET 框架为 75MB),并且它也可以在 Mac 计算机上运行。因此,Silverlight 不能依赖桌面 .NET 运行时,而是包含了自己精简的公共语言运行时 (CLR)。由于这种大小限制,Silverlight 可用的 API 也很有限。
WPF 和 Silverlight 框架之间一直存在逐渐融合的趋势,Silverlight 4 获得了隐式样式、元素名称绑定等功能……而 Silverlight 5 则添加了隐式 DataTemplates 和标记扩展。所有这些都是 Silverlight 从 WPF 继承的功能。也存在一些功能反向迁移的情况,WPF 3.5 获得了 VisualStateManager,这是一个最初在 Silverlight 中引入的概念。
这两个框架的逐步统一导致一些人推测并向微软提出将两个框架合并为一个的请求。然而,考虑到 Silverlight 面临的部署限制(轻量级、跨操作系统),这不太可能发生。
跨平台,一种实用方法
WPF、Silverlight 和 WP7 程序集不可互换;这意味着您必须为每个目标重新构建相同的代码。在实践中,这很容易通过在 Visual Studio 中创建三个独立的项目(每个框架一个)并“链接”共享文件来实现。在 Visual Studio 中,如果您通过“添加现有项”将文件添加到项目中,并选择“添加为链接”选项,您就可以从多个项目处理同一个文件。基于 Silverlight 是 WPF 的一个子集这一事实,将您的 Silverlight 项目作为主要开发重点是有意义的,尽管为了确保兼容性,最好经常在它们之间切换。
编写跨平台应用程序时,拥有结构良好的代码库非常重要。使用 UI 模式(例如 Model-View-ViewModel (MVVM))并将视图组件分离到用户控件中,将更容易针对每个框架调整代码并解决差异。
我随本文附带的两个演示应用程序之一是一个非常简单的 Twitter 应用程序,它在 Twitter 上搜索 #Silverlight 标签,并在列表中显示结果。您可以在下面看到该应用程序的结构
视图模型由几个简单的类组成,FeedViewModel 查询 Twitter 并公开 FeedItemViewModel 实例的集合,每个推文一个。TwitterView 用户控件使用 ItemsControl 来呈现结果
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Vertical">
<Image Source="/Resources/Twitter_logo.jpg"/>
<!-- renders the list of tweets-->
<ItemsControl ItemsSource="{Binding FeedItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Author}"
FontWeight="Bold"/>
<TextBlock Text="{Binding Title}"
TextTrimming="WordEllipsis"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
Silverlight 应用程序的起点是一个 UserControl,它被设置为我们应用程序的 RootVisual。在 VisualStudioSilverlight 项目模板中,用作根的控件称为 MainPage。在这个简单的 twitter 示例中,MainPage.xaml 文件在代码隐藏中实例化视图模型,并包含视图,如下所示
<UserControl x:Class="SLUGUK.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:SLUGUK.View">
<Grid x:Name="LayoutRoot" Background="White">
<view:TwitterView/>
</Grid>
</UserControl>
生成的应用程序如下所示:

我不知道 Silverlight 对 6ixnin9 做了什么(第四条推文向下)!
WPF 解决方案的结构与 Silverlight 解决方案大致相同;但是,所有 ViewModel、View 和 Resource 文件都作为链接包含在内,如上述步骤所述。

对于 WPF,我们应用程序的起点是一个 Window,这是 Silverlight 中不存在的概念。应用程序启动时显示的窗口由应用程序的 StartupUri 定义。在 WPF Twitter 应用程序中,视图模型在 MainWindow 类中实例化,视图包含如下
<Window x:Class="SLUGUK.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="clr-namespace:SLUGUK.View"
Title="MainWindow" Height="350" Width="525">
<Grid>
<view:TwitterView/>
</Grid>
</Window>
生成的应用程序如下所示
对于这个示例应用程序的代码,我一直很小心地使用 WPF 和 Silverlight 共同的 .NET API 和方法。不幸的是,如果您只将自己限制在 Silverlight API(它是 WPF 的一个真正子集),您会发现自己能实现的功能非常有限。
WPF / Silverlight 框架差异
广泛差异
作为一名跨平台开发人员,您需要尽可能多地利用这三个框架的交集……那些“好”的部分
然而,问题比这要复杂一些。差异不仅存在于 API 级别,即整个 API 和功能集在某个框架中缺失,它们还存在于细节级别,即通用类具有不同的方法签名和行为。因此,有必要针对每个框架定制您的跨平台应用程序,以解决这些差异。
API 级别差异
除了与每个框架上下文相关的明显差异之外,还有许多较小的差异,大多数是由于 Silverlight 的大小限制引起的。
简而言之,Silverlight 不支持以下 WPF 功能
- 触发器(数据、属性、事件)
- 标记扩展(这将在 Silverlight 5 中推出!)
- 依赖属性元数据
- 只读依赖属性
- 多绑定
- RelativeSource FindAncestor 绑定
- 以及更多 ...
幸运的是,这些功能大多是可有可无的,因为可以通过一些稍微更基础的功能来实现相同的行为,从而达到相同的最终结果。
WPF 拥有的控件比 Silverlight 多得多
Silverlight 的核心运行时包含少量控件,然而,更重量级的控件,如 DataGrid,则作为程序集提供,可以添加到您的 XAP 文件中,Silverlight Toolkit 也添加了更多控件。缺失的通常是与更“应用程序化”上下文相关的控件,例如工具栏和多级菜单。
还有较小的 API 级别差异;Silverlight 通常缺少 WPF 开发人员可用的各种方法和构造函数重载

同样,这减少了 Silverlight 的整体占用空间。
解决方案技术
条件编译 (#if)
程序集的构建设置允许您指定一个或多个条件编译符号
默认情况下,Silverlight 项目定义了 SILVERLIGHT
符号。使用 #if 预处理器指令,您可以在为特定框架构建应用程序时将其包含在代码中。
我们可以在上面的 Twitter 示例中使用它,通过更改代码,使其不是查询 Twitter,而是加载一个包含已保存搜索响应的文件,该文件内置在程序集中;我们开始遇到框架差异。以下代码展示了如何使用 SILVERLIGHT
符号来提供 ReadFileToString
的不同实现,从而解决 Silverlight 和 WPF 之间 URI 和文件 API 的差异
public void Update()
{
// previous mechanism for fetching XML from twitter commented out
//_webClient.DownloadStringCompleted += (s, e) => ParseXMLResponse(e.Result);
//_webClient.DownloadStringAsync(new Uri(_twitterUrl));
// load a saved response from a file
ParseXMLResponse(ReadFileToString("data.xml"));
}
private string ReadFileToString(string filename)
{
#if !SILVERLIGHT
using (var stream = this.GetType().Assembly.GetManifestResourceStream(
"SLUGUK.ViewModel." + filename))
{
StreamReader reader = new StreamReader(stream);
string xml = reader.ReadToEnd();
return xml;
}
#else
string path = "/SilverlightTwitterApp;component/ViewModel/" + filename;
Uri uri = new Uri(path, UriKind.Relative);
StreamResourceInfo sri = Application.GetResourceStream(uri);
StreamReader reader = new StreamReader(sri.Stream);
string xml = reader.ReadToEnd();
return xml;
#endif
}
还有另一种无需使用 #if
/ #endif
指令即可进行条件编译的方法。您可以将方法标记为条件属性,这意味着只有当给定符号存在时,该方法才会被调用(并包含在编译的 IL 中)。但是,代码始终由 Visual Studio 编译,因此此技术只能用于解决框架差异的子集。
条件编译是解决跨平台问题的常用技术,但也有其他可用的选项。
部分类
上述示例中,WPF 和 Silverlight 中读取文件的逻辑不同,可以使用分部类重新实现。Silverlight 版本如下所示
通用 FeedViewModel
public partial class FeedViewModel
{
...
public void Update()
{
ParseXMLResponse(ReadFileToString("data.xml"));
}
}
以及通过分部类实现此类的 Silverlight 特定专业化(保存在文件 FeedViewModelSilverlight.cs
中)
public partial class FeedViewModel
{
private string ReadFileToString(string filename)
{
string path = "/SilverlightTwitterApp;component/ViewModel/" + filename;
Uri uri = new Uri(path, UriKind.Relative);
StreamResourceInfo sri = Application.GetResourceStream(uri);
StreamReader reader = new StreamReader(sri.Stream);
string xml = reader.ReadToEnd();
return xml;
}
}
WPF 项目通过链接包含通用 FeedViewModel,但直接将 FeedViewModelWPF.cs
添加到解决方案中
public partial class FeedViewModel
{
private string ReadFileToString(string filename)
{
using (var stream = this.GetType().Assembly.GetManifestResourceStream(
"SLUGUK.ViewModel." + filename))
{
StreamReader reader = new StreamReader(stream);
string xml = reader.ReadToEnd();
return xml;
}
}
}
虽然条件编译为小段特定于框架的代码提供了很好的机制,但分部类对于较大的代码块来说是一种更优雅的解决方案。它们提供了更清晰的特定于框架的代码分离,并消除了当给定条件符号未预设时编译器会忽略的“死”代码段。
设计模式
上述技术适用于框架 API 之间的细节层面差异。然而,在某些情况下,各种框架允许您以非常不同的方式实现相同的最终目标。一个这样的例子是模态对话框,WPF 允许使用任何 Window 作为模态对话框,而 Silverlight 具有 ChildWindow
概念。两者的 API 截然不同。
为了解决这个问题,我们可以调用经典的软件工程模式之一:适配器模式。使用这种方法,您首先定义一个提供所需功能的接口。例如,对于模型对话框,以下接口允许您以模态方式显示 UserControl
:
/// <summary>
/// An interface which provides a host for some content which should be rendered
/// as a modal dialog
/// </summary>
public interface IModalDialogHost
{
/// <summary>
/// Gets or sets the content to host
/// </summary>
UserControl HostedContent { get; set; }
/// <summary>
/// Gets the dialog title
/// </summary>
string Title { set; }
/// <summary>
/// Gets the dialog result (i.e. OK, Cancel)
/// </summary>
bool? DialogResult { get; }
/// <summary>
/// Shows the dialog, invoking the given callback when it is closed
/// </summary>
void Show(DialogClosed closedCallback);
}
/// <summary>
/// A callback which is invoked when a modal dialog is closed.
/// </summary>
public delegate void DialogClosed(IModalDialogHost host)
为这个接口创建 WPF 或 Silverlight 实现是一个相对简单的过程。有关更多详细信息和工作示例,请参阅我之前的博客文章。
缺失的框架功能
如前所述,Silverlight 中缺失的 WPF 框架功能,在很大程度上,我个人认为它们是锦上添花的功能。然而,如果出于某种原因,您确实需要使用它们,有几种方法可以解决这个问题:
- 自己实现该功能,复制 WPF API 以确保兼容性。不幸的是,在大多数情况下这不起作用,因为您会遇到密封类和“封闭”API。
- 实现一个等效功能。与其复制 WPF 框架功能,不如创建一个等效功能并在 Silverlight 和 WPF 中都实现它。我过去曾多次这样做,我发表的最受欢迎的一个是WPF 和 Silverlight 的 MultiBinding 解决方案。 该 API 与 WPF MultiBinding API 略有不同,但它确实提供了两个框架之间的兼容性,因此可在跨平台上下文中使用
缺失的控件
- 在每个框架中使用不同的控件。您通常会发现有多种方式可以向用户呈现相同的界面。
- 查找缺失控件的实现,例如,您可以在互联网上找到许多缺失的控件: 然而,如果原始控件作者没有考虑跨平台开发,您可能会发现 API 与等效的 WPF 控件不兼容。
- 根据 WPF API 实现您自己的 Silverlight 控件。根据所讨论的 WPF 控件,这可能是一项相当漫长的任务。过去我曾编写过与 WPF 兼容的 Silverlight 工具栏和多级菜单。
- 使用源代码移植 WPF 控件。例如,有许多 WPF Toolkit 控件可以相对容易地移植到 Silverlight。同样,有许多 Silverlight Toolkit 控件可以轻松移植到 WP7 版 Silverlight。
常见陷阱
意想不到
第一个是默认的 BindingMode
。在 Silverlight 中,它是 BindingMode.OneWay
,这意味着默认情况下,对模型的更改会更新 UI,但反之则不然。对于 WPF,默认是 BindingMode.Default
……这意味着,这取决于!默认绑定模式由依赖属性元数据定义。这是一个令人困惑的差异,但可以通过始终在绑定上显式设置 BindingMode 来轻松解决。
接下来是控件生命周期
在 WPF 中,OnApplyTemplate
在 Loaded 事件触发之前被调用,而在 Silverlight 中,这不保证,并且 OnApplyTemplate
通常最后发生。如果您使用 OnApplyTemplate 连接控件的 UI,在 XAML 标记中定位元素,这可能会有问题。在 Silverlight 中,此逻辑可能不会在 Loaded 事件中的用户逻辑之前被调用。有方法可以解决此问题;Page Brooks 描述了一种强制生命周期遵循 WPF 事件顺序的方法。
最后,XAML 资源,如果您在 WPF 项目中使用文件链接方法将 XAML 资源链接到 WPF 解决方案中,然后构建项目,再用 ILSpy 检查程序集,您会发现 XAML 资源始终位于程序集的根目录中,无论它们被放入哪个项目文件夹。对于需要 Themes/generic.xaml 文件的自定义控件来说,这是灾难性的!
解决此问题的一个很好的方案,由 Alan Mendelevich 提供,是将所有 XAML 资源放在项目根目录中,然后使用 MergedDictionaries
将所需资源包含在 generic.xaml 文件中:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/WpfControl;component/Themes/MyTemplatedControl.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
适应各平台
上一节描述了可用于在每个平台提供相同功能的技术。然而,如果您只是尝试在桌面、Web 和移动设备上交付完全相同的应用程序,那么您的应用程序可能永远不会真正“在家”在三者中的任何一个。
在所有三个框架中通用的各种 UI 控件都能很好地适应它们所托管的平台。如果我们将使用相同 XAML 生成的简单界面在这三个框架中进行比较,我们会发现一些相当大的差异:

桌面 WPF 和基于 Web 的 Silverlight 界面非常相似,然而,WP7 界面看起来非常不同。按钮、复选框和单选按钮大得多,以适应主要输入设备(您的手指!)的相对不准确性,还有一些动态差异,例如,滚动通过滑动手势实现,滚动条隐藏以节省宝贵的屏幕空间。
然而,不同的平台为您的应用程序提供了更大的适应空间。桌面和基于 Web 的应用程序变得越来越相似;然而,移动环境提供了许多 Web 和桌面通常不具备的功能
方向 – 移动设备可以检测方向,这允许您开发纵向或横向界面,或者在方向改变时进行适应的界面。
相机、GPS、加速度计 – 移动设备包含许多外围输入,您可以在应用程序中使用它们来提供新颖的功能。最常用的是 GPS,提供位置感知搜索结果。
多点触控 – 有多点触控桌面界面可用,但这些并不常见(可能是由于人体工程学问题),相比之下,多点触控是智能手机的标准功能。利用多点触控界面和手势允许用户以新颖有趣的方式探索您的应用程序和数据。
XAML Finance
XAML Finance 应用程序允许您探索构成英国 FTSE100 指数的股票。您可以按股价浏览 FTSE 100,深入查看每只股票的业绩和静态数据,并通过图表查看历史业绩。还有 FTSE100 指数的热力图可视化。
我不会详细描述代码;代码量相当大!完整的源代码可随本文下载,因此请随意深入研究。相反,我将描述整体架构,并重点介绍 XAML Finance 三个版本之间的主要差异。
为了便于共享和专用代码,所有共享的 C# 和 XAML 文件都位于项目文件夹之外的“Common”文件夹中。这样可以使三个平台特定项目在仅在三个平台中的两个平台之间共享代码时更加清晰。
架构
数据源库
数据源库公开了一个公共接口,IDataSource
,它具有几个简单的异步返回数据的方法,以结构化模型(POCO 实例)的形式。在内部,这由 DataSource
类实现,该类依赖于由 IXmlDataSource
接口定义的 XML 数据源。此接口有几个实现,XmlDataSource
从互联网获取 XML 数据,而 XmlFileDataSource
从嵌入式文件资源返回 XML 数据——这对于应用程序的离线测试很有用。DataSource
利用 Linq-to-XML 解析字符串响应并创建所需的 POCO 对象图。
数据源类库在所有三个 XAML Finance 应用程序中是相同的。
XAML Finance 应用程序
XAML Finance 应用程序依赖于数据源项目,使用它来提供所需数据。在 XAML Finance 应用程序的三个版本中,我们看到了框架差异得到解决,并且应用程序正在适应其目标平台。
MVVM 模式
XAML Finance 应用程序利用了模型-视图-视图模型 (MVVM) UI 设计模式(Josh Smith 在这篇 MSDN 文章中详细描述了该模式),对我来说,MVVM 模式中要记住的一个关键点是 ViewModel 是视图的模型,换句话说,ViewModel 的结构应该与数据在屏幕上的呈现方式密切相关。
Silverlight 版的 XAML Finance 应用程序向用户提供了一个选项卡式界面。单击左侧的按钮之一会添加一个新的 FTSE100 或热力图视图,而单击其中一个股票则会添加一个视图,为您提供有关特定公司的更多信息,包括图表。顶层视图模型 XamlFinanceViewModel
暴露了一个“子”视图模型的 ObservableCollection
,每个视图模型都绑定到一个选项卡页。当添加新的“子”视图模型时,它们通过 IDataSource 接口获取数据来初始化自己。视图模型实现了 INotifyPropertyChanged
接口,以便在数据到达时更新视图。这都是非常标准的 MVVM 操作!
所有子视图模型都继承自 NamedViewModelBase
。这个基类提供了一个 Name 属性,它显示为选项卡标题,以及一个 CloseCommand
,它绑定到移除选项卡的小“x”按钮。此命令的实现只是从 XamlFinanceViewModel
中的 TabItems
集合中移除视图模型,集合绑定确保 UI 相应地更新。
适应 Web
基于 Web 的 Silverlight 应用程序通常与基于 Web 的 HTML 内容并存。Silverlight 应用程序的默认样式(基于 Aero 主题)与大多数 Web 内容不太协调。Silverlight 中灵活的样式/模板使其可以很容易地重新设计 UI 以适应现有的 Web 内容。在下面的屏幕截图中可以看到,字体、配色方案、超链接和其他图像已从托管应用程序的网站复制而来,这使得很难确定 Silverlight 应用程序的边界

适应桌面
桌面应用程序通常与基于 Web 的应用程序具有相似的外观和布局。我使用了 WPF 可用的“库存”主题之一,为应用程序提供了一个 Aero 主题。
对于基于 Web 的应用程序,通常期望浏览器最大化,从而为每个应用程序提供相似的屏幕空间。在 XAML Finance 中,我想探索使用多个窗口。在 Web 版本中,您可以为多个股票打开页面,每个页面都显示在单独的选项卡中,但您不能并排比较两个。
XAML Finance 的 WPF 版本通过分部类专门化了顶层模型 XAMLFinanceViewModel
,以添加第二个“子”视图模型集合。
TabItems
集合直接绑定到视图中 TabControl
的 ItemsSource
,绑定框架管理 TabItems
的创建/销毁,因为此集合会发生变化。但是,浮动窗口的管理必须手动完成。视图模型的 WPF 特定附加项需要 WPF 特定视图。但是,视图由 UserControl
组成,允许重用各种组件,例如,公司摘要和图表页面在 WPF 和 Silverlight 之间共享,即使托管它们的视图不同。
NamedViewModelBase
通过分部类在 WPF 项目中进行了扩展,以添加 PopOutCommand
和 PopInCommand
。这些命令绑定到选项卡标题中的按钮
PopOutCommand
将视图模型从 TabItems
集合移动到 FloatingItems
集合。创建 Window
以托管弹出视图模型的任务由应用程序执行,该应用程序处理由 FloatingItems
集合引发的集合更改事件:
private XAMLFinanceViewModel _model = new XAMLFinanceViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = _model;
model.FloatingViewModels.CollectionChanged += FloatingViewModels_CollectionChanged);
}
private void FloatingViewModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
// create a window to host the view model that has been moved into this collection
var host = new ViewModelHost();
host.DataContext = e.NewItems[0];
host.Show();
}
}
添加视图模型时,会创建一个 ViewModelHost
窗口,并将视图模型作为 DataContext
提供。ViewModelHost
仅包含一个 ContentControl
:
<Window x:Class="XAMLFinance.ViewModelHost"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
Closed="Window_Closed"
Title="{Binding Path=Title}" Height="600" Width="500">
<Grid>
<ContentControl Content="{Binding}"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"/>
</Grid>
</Window>
当 Closed
事件触发时,执行 PopInCommand
(通过代码隐藏简单地调用此命令),这将导致视图模型移回 TabItems
集合中。
通过隐式 DataTemplates
执行绑定视图模型的视图选择,将以下 XAML 合并到应用程序资源中:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:view="http://XAMLFinance/View"
xmlns:vm="http://XAMLFinance/ViewModel">
<DataTemplate DataType="{x:Type vm:InstrumentDetailsViewModel}">
<view:InstrumentDetailsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:PriceListViewModel}">
<view:PriceListGridView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:HeatMapViewModel}">
<view:HeatMapView/>
</DataTemplate>
</ResourceDictionary>
除了添加浮动窗口之外,WPF 和 Silverlight 应用程序非常相似,共享视图模型(如上所述的扩展)以及大部分视图代码 (XAML)。
适应 Windows Phone 7
XAML Finance 的 WPF 和 Silverlight 版本的“核心”是选项卡式界面,允许用户打开多个文档并在它们之间切换。Windows Phone 7 没有选项卡控件,因此没有简单的方法可以将 WPF/Silverlight 界面复制到 WP7。没有什么可以阻止您创建 WP7 选项卡控件,但是,由于移动设备的屏幕尺寸有限,多文档界面效果不佳。因此,大多数 WP7 应用程序都采用基于导航的方法。实际上,WP7 架构,包括页面状态、返回堆栈和墓碑化,确实促使您采用逐页的 UI 模型。
我已经在之前的文章中介绍了如何在 MVVM 模式下开发时集成 WP7 墓碑化和导航框架,所以这里不再赘述。简而言之,视图模型呈现了一个对象图,可以通过 URI 导航以定位视图模型,这些 URI 用于导航。视图模型与应用程序实例关联,当导航到页面时,它使用其 URI 在视图模型中找到正确的“节点”作为其 DataContext
。
这种不同的架构反映在重组的视图模型中,例如,在 PriceListViewModel
中单击公司不会导致新的视图模型被添加到顶层视图模型中的集合中(与 WPF 和 Silverlight 应用程序的情况一样)。相反,视图模型会就地“展开”,并通过 IDataSource
请求额外的现场数据。
WP7 登录页使用了流行的 Panorama
控件,“磁贴”UI 范式用于创建 Metro 风格的 UI。
不是立即显示全景图,而是在数据加载时隐藏 UI。XAMLFinanceViewModel
(WP7 特定扩展)立即初始化其各种子视图模型。随着每个状态的变化,会检查各种条件。当所有子视图都完全填充后,布尔值 IsLoaded
属性设置为 true:
/// <summary>
/// Initialises the various view-models required for the panorama
/// </summary>
public override void Init()
{
Pricelist.PropertyChanged += (s,e) => CheckChildViewModelsLoaded();
HeatMap.PropertyChanged += (s, e) => CheckChildViewModelsLoaded();
Pricelist.Init();
HeatMap.Init();
// get the FTSE100 prices
App.Current.DataSource.GetFTSE100PriceHistory(
priceHistory =>
{
FTSE100Price = new InstrumentPriceHistoryViewModel(priceHistory);
CheckChildViewModelsLoaded();
});
}
/// <summary>
/// Updates the IsLoaded state based on the state of the related view models.
/// </summary>
private void CheckChildViewModelsLoaded()
{
IsLoaded = Pricelist.Prices.Count > 0 &&
HeatMap.Sectors.Count > 0 &&
FTSE100Price.Count > 0;
}
托管全景控件的页面的代码隐藏会检查 IsLoaded
属性的状态,并在所有视图模型完全填充时隐藏加载指示器:
public PanoramaIndex()
{
InitializeComponent();
this.DataContext = App.Current.ViewModel;
LoadingIndicator.Opacity = 1;
// monitor the IsLoaded view model property to determine when the
// data we require has arrived
if (App.Current.ViewModel.IsLoaded)
{
DataLoadComplete();
}
else
{
App.Current.ViewModel.PropertyChanged += (s, e) =>
{
if (App.Current.ViewModel.IsLoaded)
{
DataLoadComplete();
}
};
}
}
/// <summary>
/// If data has arrived, hide the loading indicator.
/// </summary>
private void DataLoadComplete()
{
BackgroundImage.Opacity = 0.4;
LoadingIndicator.Visibility = Visibility.Collapsed;
}
WP7 应用程序的另一个不同之处是 FTSE100 热力图,它通过颜色表示股票表现,通过大小表示市值。热力图还按行业部门进一步结构化。WPF 和 Silverlight 工具包提供了 TreeMap 控件,非常适合可视化此类数据,通过使用嵌套的 TreeMap,可以在纯 XAML 中创建此热力图。单击任何股票都会执行一个绑定命令,该命令会将所需的视图模型添加到选项卡项集合中。
WP7 Toolkit 不包含 Silverlight 和 WPF 工具包中存在的 TreeMap
控件。然而,通过使用 Silverlight 版本的源代码,我发现将其移植到 WP7 非常容易。手机上更大的问题是可用性,对于市值非常小的公司,它们在热力图中所占据的区域非常小。虽然它们仍然可以用鼠标点击,并且可以高精度操作,但在手机界面上(即使是最纤细的手指!)也无法点击。
对于 WP7 版的 XAML Finance,用户通过热力图进行向下钻取,第一次点击选择行业,第二次点击选择特定公司
公司信息在 Silverlight 和 WPF 应用程序中显示为单个页面,而在 WP7 中则使用 WP7 枢轴控件显示在多个屏幕上
XAML Finance 使用的唯一第三方控件是 Visiblox 图表控件。该图表有 WPF、Silverlight 和 WP7 版本,每个版本都公开相同的 API。此外,WP7 版本利用了手机的多点触控界面,允许用户用手指滚动和“捏合”图表。
与图表交互时,将其最大化以填充整个手机屏幕是有意义的。此外,横向方向对于时间序列图表也最有意义。XAML Finance 没有让用户明确导航到全屏图表页面,而是利用了手机可以检测方向的事实。当用户将手机旋转到横向模式时,图表将以全屏显示。
WP7 版的 XAML Finance 还利用了 codeplex 上的 WP7Contrib 项目,该项目为 WP7 开发者提供了许多有用的工具和实用程序。
结论
通过本文,我展示了如何通过分别使用 WPF、Silverlight 和用于 WP7 的 Silverlight 框架,在桌面、Web 和移动应用程序之间重用大量代码。不幸的是,编写跨平台应用程序并非易事;除了链接文件之外,没有真正的工具支持,而且无数的框架差异是必须克服的障碍。幸运的是,通过对这些差异使用标准解决方案,以及合适的 UI 模式来专门化应用程序的每个版本,可以编写共享大量代码的跨平台应用程序,同时它们看起来在每个环境中都真正“适合”。
可以共享多少代码确实取决于您正在创建的应用程序。本文附带的 XAML Finance 应用程序约有 75% 的代码位于通用代码库中

基于 Web 的 Silverlight 和 WPF 版本之间也共享了相当多的代码。XAML Finance 的 WP7 版本比其他版本有更多的平台特定代码,因为平台本身存在更显著的差异。
我希望这篇文章能提供信息并鼓励其他人思考使用 XAML 技术进行跨平台开发。一旦克服了障碍,就可以共享大量代码。
一个价格购买三个应用程序(或一个半),真是划算!
变更历史
- 2011 年 9 月 22 日 - 修复了损坏的链接
- 2011 年 9 月 21 日 - 添加了 WP7 版本的视频
- 2011 年 9 月 19 日 - 首次上传文章