KnockoutJS 对比 Silverlight






4.99/5 (67投票s)
本文通过实现相同应用程序的这两种框架,比较了 Silverlight 和 KnockoutJS,并试图回答那个至关重要的问题“哪个框架最好?”
目录
- 概述
- 引言
- 演示应用程序
- 应用程序视图模型结构概述
- 视图模型 – INotifyPropertyChanged 对比 Observable
- 绑定 – 标记扩展 对比 data-bind
- 创建基本用户界面 – 模板 对比 用户控件
- 渲染类别图片 – 值转换器 对比 HTML
- 渲染答案列表 – ItemsControls 对比 foreach 绑定
- 一些 Silverlight 技巧 – 选择绑定
- 鼠标悬停效果 – 视觉状态 对比 CSS3 过渡
- 下一题 – 命令 对比 函数
- 一些 Knockout 技巧 - $parent 绑定和计算型可观察对象
- 显示结果 – Linq 对比 $.grep
- 提供测验数据 – 输入参数 & XML 对比 JavaScript & JSON
- 结论
概述
本文通过比较使用 Silverlight 和 KnockoutJS (http://knockoutjs.com/) 实现的同一应用程序,来对比这两个框架。在比较过程中,我们将看到这两个框架之间有许多相似之处,相同的概念在两者中都有出现。然而,这些概念的执行/实现方式却大相径庭。最后,我们将探讨一个非常重要的问题:“Silverlight 和 Knockout 哪个更好?”
战斗开始...
引言
不可否认,HTML5 和 JavaScript 的受欢迎程度正在上升。以前属于插件技术(Silverlight、Flex、Flash、Java Applet)领域的复杂 Web 应用程序现在越来越多地使用 JavaScript 编写。选择哪种技术进行特定的 Web 开发是另一篇文章的主题(Flex、Silverlight 还是 HTML5:是时候决定了……),本文则侧重于 Silverlight 开发人员,以及他们如何通过使用 KnockoutJS 框架充分利用现有技能。
那么为什么要选择 KnockoutJS 呢?好问题!为了回答这个问题,我们将简要介绍 Silverlight 和 HTML5/JavaScript 开发之间的区别。
Silverlight 有三个主要组成部分:首先是语言本身(C# 或 VB.NET),其次是所有 .NET 语言通用的核心库,第三是 Silverlight 框架本身,其中包括控件、动画和 XAML 的概念。对于 JavaScript 开发,情况则大不相同,语言本身扮演着与 C# 相同的角色,但是没有执行与 .NET 核心和 Silverlight API 相同功能的标准库。相反,有许多框架填补了空白,典型的 JavaScript 应用程序将使用其中的一系列框架。
有许多 JavaScript 框架支持基于模式的 UI 设计(MVP、MVC)。您可以在 ToDoMVC 项目中看到 16 个(没错,16 个!)不同 UI 框架的实际演示,该项目使用每个框架重新实现了相同的简单应用程序。对于希望学习 JavaScript 应用程序开发的 Silverlight 开发人员来说,Knockout 是一个显而易见的D选择,因为它支持 Model-View-ViewModel (MVVM) 模式,这是 Silverlight 框架“内置”的 UI 模式。Knockout 相对较新,于 2010 年首次发布。它也引起了微软的关注,微软最近聘请了该框架的创建者 Steve Sanderson。然而,微软并未收购 Knockout,它仍然是一个开源项目。
注意:在本文中,我将 CSS、HTML、JavaScript 和 Knockout 统称为“Knockout”。这四者本身都是截然不同的技术,但为简洁起见,我们将它们都视为我们 Knockout 解决方案的组成部分。
演示应用程序
为了向 Silverlight 开发人员介绍 JavaScript 和 Knockout 开发,本文将描述使用这两种技术开发一个简单应用程序的过程。该应用程序是一个测验,下面显示了 Silverlight 和 Knockout 版本的屏幕截图。
Silverlight 版本
Knockout 版本
从上面的截图可以看出,这两个版本的应用程序看起来几乎完全相同。
应用程序视图模型结构概述
这两个应用程序都具有完全相同的视图模型结构,如下所示为一个简单模型
下面简要描述了视图模型及其职责
QuestionViewModel
– 这详细说明了一个问题,包括问题文本本身以及可能的答案。此视图模型还存储用户选择的答案。AnswerViewModel
– 这详细说明了一个问题的单个潜在答案。此视图模型不包含任何动态状态。ResultsViewModel
– 这详细说明了用户答对了多少个问题。当用户完成测验时,将动态生成此视图模型的一个实例。QuizWizardViewModel
– 此视图模型管理整体应用程序流程,根据CurrentQuestion
属性的指示,从一个问题导航到下一个问题。当用户正确回答所有问题后,此视图模型将生成一个ResultsViewModel
实例。
视图模型 – INotifyPropertyChanged 对比 Observable
在本文中,我们将大部分注意力集中在 QuestionViewModel
上,它展示了我们比较所需的大部分功能。
Silverlight 视图模型如下所示
public class QuestionViewModel : ViewModelBase
{
private AnswerViewModel _selectedAnswer;
public QuestionViewModel()
{
// detailed later ...
}
public QuizWizardViewModel Parent { get; private set;}
public string Text { get; private set; }
public int Index { get; private set; }
public List<AnswerViewModel> Answers { get; private set; }
public string InterestingFacts { get; private set; }
public string Category { get; private set; }
public AnswerViewModel SelectedAnswer
{
get
{
return _selectedAnswer;
}
set
{
_selectedAnswer = value;
OnPropertyChanged("SelectedAnswer");
}
}
}
上述视图模型非常标准,它包含许多将绑定到 UI 的属性。这些属性分为两类:第一类我们认为是不可变的(即它们的值不会改变),它们只是常规属性;第二类,它们会因 UI 交互或应用程序逻辑而改变,这些属性通过 INotifyPropertyChanged
触发更改通知。
由于涉及 INotifyPropertyChanged
的样板代码,视图模型扩展了一个通用基类
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
#endregion
}
这也很常见。
那么,我们来看看等效的 Knockout 视图模型:
QuestionViewModel = function (index, config) {
// ------ properties
this.index = index;
this.text = config.text;
this.category = config.category;
this.interestingFact = config.interestingFact;
this.answers = [];
this.selectedAnswer = ko.observable();
// ------ construction logic
// detailed later!
};
这里最显著的区别之一是,视图模型本身是一个函数,而不是一个类。这是因为 JavaScript 不是一种强类型语言,即您不能定义自己的类型(类),然后构造它们的实例。相反,可以通过向对象添加属性和函数来动态构建对象。上面的代码使用构造函数定义视图模型,换句话说,QuestionViewModel
是一个构造对象的函数,它动态地向对象添加属性并设置它们的值。欲了解更多详情,我推荐阅读 Mike Koss 撰写的文章 “JavaScript 中的面向对象编程”。
与 Silverlight 视图模型类似,有些属性我们不期望它们会改变,例如 text
和 index
,而有些属性则会改变,例如 selectedAnswer
。在上面的视图模型中,selectedAnswer
是通过调用 ko.observable()
构造的。此函数创建了一个可观察属性,它具有内置的机制来通知订阅者值更改。这相当于 Knockout 中的 INotifyPropertyChanged
。
值得注意的是,在 Silverlight 中,属性和属性访问器是语言的一等特性,其中带有私有设置器的自动属性为创建不可变属性提供了方便的简写。另一方面,JavaScript 并不广泛支持 getter 和 setter 的概念(这是 一些浏览器支持的最新语言特性),但为了最大限度地提高“覆盖范围”,Knockout 不使用它)。因此,JavaScript 不支持不可变属性。此外,缺乏 getter 和 setter 意味着使用 ko.observable
意味着 selectedAnswer
属性不能像常规属性一样设置,而是必须被调用。
var viewModel = new QuestionViewModel(…)
viewModel.selectedAnswer = foo; // this will fail – badly!
viewModel.selectedAnswer(foo); // that’s better ;-)
因为这有点不自然的语法,我不得不承认我经常犯这个错误!
简而言之,Knockout 视图模型更加紧凑,但缺乏不可变属性,并且设置属性的语法有点笨拙。
绑定 – 标记扩展 对比 data-bind
在深入了解完整的视图之前,我们先来看看每个框架的绑定语法。两者都共享数据上下文的概念,即应用程序 UI 的区域由视图模型“支持”,因此它是该区域内定义的任何绑定的源。在 Silverlight 中,您只需将视图的 DataContext 设置为视图模型即可
this.DataContext = new QuestionViewModel(...);
而在 Knockout 中,您调用 applyBindings 函数
ko.applyBindings(new QuestionViewModel(...));
区别不大!
请注意,Knockout 有数据上下文的概念,在文档中称为绑定上下文。但是,与 Silverlight 不同,它不是您可以直接设置或检查的属性。
使用 Silverlight,您可以绑定到可视化树中元素的任何依赖项属性。绑定通常使用 {Binding}
标记扩展定义
<StackPanel Orientation="Horizontal">
<TextBlock Text="Q"/>
<TextBlock Text="{Binding Path=Index}"/>
<TextBlock Text=")"/>
</StackPanel>
而 Knockout 使用 HTML5 自定义数据属性 data-bind
,如以下示例所示
<div class="index">Q<span data-bind="text: index" />)</div>
上面的 text
绑定将把 index 属性的值渲染到 span 元素的主体中。Knockout 拥有少量内置绑定,用于 text
、css
和 visibility
,以及一个通用的 attr
绑定,可用于将元素的属性绑定到视图模型属性。
虽然 Silverlight 和 Knockout 绑定表面看起来相似,但有一个相当显著的区别。这两个框架都要求您绑定与绑定目标兼容的属性值。对于 Silverlight,这意味着您经常需要为可见性等属性提供类型转换器。而对于 Knockout,JavaScript 类型强制转换以及大多数 DOM 属性可以设置为字符串的事实,意味着很少需要类型转换。
使用 Silverlight,您可以将不触发 PropertyChanged
事件的属性绑定到 UI,这会产生一次性绑定,即初始属性值在 UI 元素上设置,但随后的任何更新都将被忽略。同样,使用 Knockout,您可以绑定非 Observable 属性并达到相同的效果。
创建基本用户界面 – 模板 对比 用户控件
现在我们对视图模型和绑定有了基本的了解,我们将看看它们是如何组合成一个视图的。对于 Silverlight,UI 由用户控件(或自定义控件)组成,这些控件使用 XAML 定义。MVVM 模式的常见方法是为每个视图模型定义一个单独的视图。
MainPage.xaml
是我们应用程序的起点,它包含一个 QuizWizardView
的单个实例
<UserControl x:Class="EcoQuiz.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EcoQuiz.View"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
<Grid x:Name="LayoutRoot">
<local:QuizWizardView />
</Grid>
</UserControl>
它是一个用户控件,用于渲染绿色背景、标题和当前问题
<UserControl x:Class="EcoQuiz.View.QuizWizardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"
xmlns:local="clr-namespace:EcoQuiz.View"
Width="450" Height="400" Foreground="White"
FontFamily="Verdana"
FontSize="14">
<Border CornerRadius="20" BorderThickness="5">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1" >
<GradientStop Color="#739A39" Offset="0" />
<GradientStop Color="#A5CF63" Offset="1.0" />
</LinearGradientBrush>
</Border.Background>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Path=Title}" FontSize="25" Margin="10"
FontFamily="Georgia"/>
<local:QuestionView Grid.Row="1"
Margin="0,10,0,0"
DataContext="{Binding Path=CurrentQuestion}"/>
</Grid>
</Border>
</UserControl>
我们稍后再详细介绍 QuestionView
。在我们的应用程序中,应用程序启动时在 App.xaml.cs
文件中构建“根”视图模型,如下所示
private void Application_Startup(object sender, StartupEventArgs e)
{
QuizWizardViewModel quiz = new QuizWizardViewModel();
quiz.Title = “Eco-Quiz”;
((FrameworkElement)this.RootVisual).DataContext = quiz;
}
可视化树 DataContext
继承将确保我们 QuizWizardView
的 DataContext
是上面创建的 QuizWizardViewModel
实例。运行当前代码的应用程序,我们看到一个带有圆角边框的美丽阴影正方形,以及我们的测验标题
使用 Knockout,方法非常相似。我们的应用程序在 HTML 页面上下文中运行,其中包括各种脚本。视图定义为 jQuery 模板,允许重用相同的 UI 标记
<!DOCTYPE html>
<html>
<head>
<script src="lib/jquery-1.6.1.min.js" type="text/javascript"></script>
<script src="lib/jquery.tmpl.min.js" type="text/javascript"></script>
<script src="lib/knockout-2.0.0.js" type="text/javascript"></script>
<script src="viewModel/QuizWizardViewModel.js" type="text/javascript"></script>
<script src="viewModel/QuestionViewModel.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link href="style.css" rel="stylesheet" type="text/css"></link>
<title>Untitled Page</title>
</head>
<body>
<!-- ... -->
<script id="quizWizardView" type="text/x-jquery-tmpl">
<h2 data-bind="text: title"
class="title"></h2>
<div data-bind="template: { name: 'questionView', data: currentQuestion }"
class="question"></div>
</script>
<div id="app">
<div data-bind="template: { name: 'quizWizardView' }"></div>
</div>
</body>
</html>
template
绑定在我们的 HTML 文档中给定位置创建指定模板的实例。“根”视图模型在 app.js
文件中构建并绑定到 UI
$(document).ready(function () {
viewModel = new QuizWizardViewModel();
ko.applyBindings(viewModel);
});
虽然 Silverlight 和 Knockout 构建 UI 的方式非常相似,都分别通过用户控件和模板创建视图的机制,但 UI 本身的标记却大相径庭。任何使用 XAML 和 HTML 创建过 UI 的人都会明白,两者之间存在很大差异。XAML 旨在创建应用程序,而 HTML 旨在呈现内容。因此,在 Silverlight 中非常简单的事情,例如垂直居中内容,在 HTML 中实际上相当棘手,反之,在 Silverlight 中创建优雅流畅的响应式杂志风格布局几乎是不可能的。
Silverlight 和 Knockout 应用程序之间的另一个显著区别是样式。简而言之,Silverlight 样式只是属性值的集合,通过显式引用样式应用于元素。HTML 通过 CSS 进行样式设置,CSS 提供了标记和样式之间的完全分离,其中元素使用 CSS 选择器进行匹配。下面给出了测验应用程序的 CSS
#app
{
position: relative;
background: #739A39;
width: 450px;
height: 400px;
margin: auto;
padding: 10px;
-webkit-border-radius: 20px;
-moz-border-radius: 20px;
background: -webkit-linear-gradient(top, #739A39, #A5CF63);
background: -moz-linear-gradient(top, #739A39, #A5CF63);
}
body
{
font-size: 14px;
font-family: Verdana, Sans-Serif;
color: White;
}
.title
{
font-size: 25px;
font-family: Georgia, Serif;
}
(请注意背景渐变和边框半径属性使用了供应商特定的前缀)。
有了上述样式,Knockout UI 看起来几乎与 Silverlight UI 完全相同
渲染类别图片 – 值转换器 对比 HTML
现在我们已经了解了 Silverlight 和 Knockout 中视图的定义方式,并研究了它们各自的基本绑定语法,我们将探讨一个更复杂的问题。每个问题都有一个类别,用于选择一张图片来 acompañarla。
Silverlight 的 QuestionViewModel
有一个字符串属性 Category
。为了在视图中渲染它,它被绑定到 Image 元素的 Source 属性
<Image Source="{Binding Path=Category, Converter={StaticResource CategoryToImageConverter}}"
Grid.Row="0" Margin="0,0,0,10"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Stretch="UniformToFill"/>
然而,Source
属性的类型是 ImageSource
,所以我们需要应用一个值转换器,从字符串视图模型属性构造一个 BitmapImage
public class CategoryToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string category = (string)value;
var img = new BitmapImage(new Uri("/EcoQuiz;Component/View/" + category + ".jpg", UriKind.Relative));
return img;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
使用 Knockout,这种事情要容易得多。首先,图像元素的 src 属性可以直接设置为字符串值,因此我们能够直接绑定到它。其次,Knockout 允许您直接在绑定中使用表达式,从而无需值转换器
<img class="picture"
data-bind="attr: { src: 'view/' + category + '.jpg' }"/>
请注意,Knockout 没有针对 src
属性的特定绑定,因此我们使用通用的 attr
绑定。
通过简单的样式设置,测验应用程序的两个版本现在都为每个问题渲染了一张图片
Silverlight 测验应用程序至少有五个值转换器。在每种情况下,由于缺乏强类型或仅由于 HTML 元素接受字符串值,Knockout 的实现都更简单。Knockout 还有一个可见性绑定,它提供了一种方便的方式,根据布尔视图模型属性显示/隐藏元素。
渲染答案列表 – ItemsControls 对比 foreach 绑定
QuestionViewModel
公开了一组潜在答案。在 Silverlight 视图模型实现中,它被公开为通用 List
,而在 Knockout 视图模型中,它被公开为一个数组。
对于 Silverlight 版本的测验应用程序,使用 ListBox 渲染与问题相关的答案
<!-- the question -->
<TextBlock Text="{Binding Path=Text}" Grid.Column="1" TextWrapping="Wrap"/>
<!-- the answers -->
<ListBox ItemsSource="{Binding Path=Answers}"
SelectedItem="{Binding Path=SelectedAnswer, Mode=TwoWay}"
BorderThickness="0"
HorizontalAlignment="Left"
Grid.Column="1" Grid.Row="1"
Background="Transparent" Foreground="White">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Index, Converter={StaticResource IndexToLetterConverter}}"/>
<TextBlock Text=". " />
<TextBlock Text="{Binding Path=Text}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
这将产生如下所示的用户界面
再次使用了值转换器,这次是将每个答案的索引转换为一个字母。
Knockout 通过具有 foreach
属性的 template
支持集合绑定。下面的标记迭代答案数组中的每个项目,为每个项目渲染 answerView
模板的一个实例。answerView
模板中的任何数据绑定都将答案实例作为其数据上下文
<script id="questionView" type="text/x-jquery-tmpl">
...
<div data-bind="text: text" />
<ul data-bind="template: { name: 'answerView', foreach: answers }"
class="answers"/>
</script>
<script id="answerView" type="text/x-jquery-tmpl">
<li>
<span data-bind="text: String.fromCharCode(index + 64)"
class="index"/>.
<span data-bind="text: "text"
class="answerText"/>
</li>
</script>
我们添加一点 CSS
ul.answers
{
margin: 0;
padding: 0;
float: left;
}
ul.answers li
{
list-style-type: none;
margin-top: 5px;
cursor: pointer;
}
结果是 UI 看起来与 Silverlight 的完全相同:
同样,Knockout 集合绑定与 Silverlight 非常相似,您可以绑定到不引发更改通知的集合,例如,在 Silverlight 中,您可以使用 ItemsControl
渲染常规 List
,而在 Knockout 中,foreach
绑定可以渲染常规 JavaScript 数组。对于更改通知,Silverlight 使用 INotifyCollectionChanged
接口(最常见的是通过 ObservableCollection
),而 Knockout 使用可观察数组。可观察数组的定义方式与常规(单值)可观察属性完全相同
this.answers = ko.observableArray();
这两个框架都会处理何时向集合中添加或删除项目,以保持视图同步。
一些 Silverlight 技巧 – 选择绑定
在上一节中,我们展示了 Silverlight 和 Knockout 集合绑定如何用于渲染每个问题的潜在答案。但是当用户点击他们认为正确的答案时会发生什么呢?
在测验应用程序的 Silverlight 版本中,我们可以将 ListBox
的 SelectedItem
属性绑定到视图模型的一个属性。当测验完成时,我们可以简单地计算 question.SelectedAnswer.IsCorrect
为真的问题数量。
<ListBox ItemsSource="{Binding Path=Answers}"
SelectedItem="{Binding Path=SelectedAnswer, Mode=TwoWay}">
Knockout UI 是 HTML,它并不真正支持控件概念,因此用于渲染问题答案的 ul
/ li
元素不支持选择概念。使用 Knockout 实现,我们必须添加一个单击事件绑定,该绑定调用我们视图模型之一上的方法
<script id="answerView" type="text/x-jquery-tmpl">
<li data-bind="click: $parent.answerClicked">
<span data-bind="text: String.fromCharCode(index + 64)" class="index"/>.
<span data-bind="text: text" class="answerText"/>
</li>
</script>
(我们将在后面的章节中了解 $parent
特殊伪变量的作用)。
answerClicked
函数在 QuestionViewModel
上定义,它只设置 selectedAnswer
可观察对象,产生与 Silverlight ListBox.SelectedItem
绑定相同的结果
QuestionViewModel = function (index, config) {
var that = this;
// ------ properties
// ...
this.selectedAnswer = ko.observable();
// ------ functions
this.answerClicked = function (answer) {
that.selectedAnswer(answer);
}
};
Knockout 确实有一个 selectedOptions 绑定,这几乎就是我们在这里想要的。作为一般观察,Silverlight 对 UI 控件有更强的概念,这些控件可以公开可绑定的控件特定属性。这允许通过一套通用控件(Silverlight SDK 中约有 30 个,第三方供应商提供更多)快速构建复杂的 UI。
鼠标悬停效果 – 视觉状态 对比 CSS3 过渡
目前的测验应用程序有点平淡,我们将尝试通过在用户悬停在每个答案上方时添加微妙的高亮效果,使其更有趣(至少在我艺术能力范围内!)。
使用 Silverlight,可以使用视觉状态管理器指定过渡。控件可以定义视觉状态,例如 checked
、focussed
和 mouseover
,然后您可以使用视觉状态管理器定义每个状态的 UI 以及状态之间的过渡方式。对于我们的测验应用程序,我们将在鼠标悬停时创建微妙的高亮效果
<Style TargetType="ListBoxItem" x:Key="ListBoxItemStyle">
<Setter Property="Padding" Value="3" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Top" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="TabNavigation" Value="Local" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid Background="{TemplateBinding Background}">
<vsm:VisualStateManager.VisualStateGroups>
<vsm:VisualStateGroup x:Name="CommonStates">
<vsm:VisualState x:Name="Normal" >
<Storyboard>
<DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0:0:0.5" To="0"/>
</Storyboard>
</vsm:VisualState>
<vsm:VisualState x:Name="MouseOver">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="fillColor" Storyboard.TargetProperty="Opacity" Duration="0:0:0.5" To="1"/>
</Storyboard>
</vsm:VisualState>
</vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>
<Rectangle x:Name="fillColor" Opacity="0" Fill="#44000000" IsHitTestVisible="False" RadiusX="1" RadiusY="1"/>
<ContentPresenter
x:Name="contentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
上面的样式定义了 ListBoxItem
的模板和视觉状态转换,该模板由 ListBox
生成,作为列表中每个项目的容器。为了使用上面的样式,我们设置了 ItemContainerStyle
<ListBox ItemsSource="{Binding Path=Answers}"
SelectedItem="{Binding Path=SelectedAnswer, Mode=TwoWay}" Grid.Row="1"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
这为每个答案提供了微妙的鼠标悬停效果,作为这些项目可点击的方便视觉提示:
视觉状态是 Silverlight“控件”概念的另一个方面,因此,Knockout 不共享此功能。然而,由于 Knockout UI 定义为 HTML,我们可以使用 CSS 实现相同的效果。
我们可以使用 hover 伪选择器来指定每个答案的鼠标悬停颜色。但我们如何创建微妙的淡入/淡出效果呢?使用 CSS3,这非常简单。我们可以简单地为 background-color
属性指定一个过渡
ul.answers li
{
...
-webkit-transition: background-color 0.4s;
-moz-transition: background-color 0.4s;
}
ul.answers li:hover
{
background-color: rgba(0,0,0,0.2);
}
该过渡表明背景颜色属性的任何变化都应在 400 毫秒内从之前的颜色过渡到新颜色。
Silverlight 和 Knockout 实现相同效果的方式大相径庭,Silverlight 版本要冗长得多。那么这是为什么呢?原因有很多;为了定义视觉状态过渡,您必须重新模板化一个控件,复制其现有模板。Silverlight 动画是强类型的,因此我们必须为每个属性选择正确的动画类型。最后,Silverlight 使用 XAML 定义模板和故事板,这总是比其他方法更冗长。
我知道我更喜欢哪种方法!CSS3 方法的简洁性,其中过渡属性的类型不会改变语法,实现起来要容易得多。缺乏类型顾虑是 Knockout 开发的常见主题。此外,CSS3 过渡将确保属性值无论使用何种机制设置其值都会过渡。最后,HTML 和 CSS 提供的样式和标记之间的分离远远优于 Silverlight 中可以实现的分离。
虽然,为 Silverlight 辩护,视觉状态的概念是完全可扩展的,您可以定义任何对您的控件有意义的视觉状态,并提供它们之间的转换。
下一题 – 命令 对比 函数
QuizWizardViewModel
通过其 CurrentQuestion
属性公开当前问题视图模型。在 Silverlight 和 Knockout 测验应用程序中,用户都会点击一个“下一题”链接,该链接将测验推进到下一个问题。
Silverlight 应用程序中的 QuizWizardViewModel
公开了一个 NextQuestion
命令
public ICommand NextQuestionCommand
{
get
{
return new NextQuestionCommand(this);
}
}
其中命令的实现只是调用视图模型上的 NextQuestion
方法
public class NextQuestionCommand : ICommand
{
private QuizWizardViewModel _quiz;
public NextQuestionCommand(QuizWizardViewModel quiz)
{
_quiz = quiz;
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
_quiz.NextQuestion();
}
}
在一个更复杂的应用程序中,这很适合使用通用的“委托命令”,但由于此应用程序只有一个命令,我们将遵循 YAGNI 原则,并使用上述实现。
在视图中,我们包含一个绑定到此命令的按钮,以便单击时调用上述代码
<Button Command="{Binding Parent.NextQuestionCommand}"
Style="{StaticResource NextButtonStyle}"/>
(我们将在下一节中查看上述绑定中的“父级”)
Knockout 没有命令的概念,而是直接将事件绑定到视图模型上定义的函数
<a href="#" data-bind="click: $parent.nextQuestion"
class="next">
Next
</a>
(同样,我们将在下一节中查看 $parent
!)
nextQuestion
函数在视图模型上定义如下
QuizWizardViewModel = function (config) {
// ...
this.nextQuestion = function () {
//...
}
}
};
Knockout 对此功能的实现再次比 Silverlight 版本简单得多。Silverlight 的命令概念比简单的事件到函数连接更强大,它具有命令属性和 ICommand.CanExecute
属性等功能,可用于在命令无法执行时自动禁用按钮(或其他 UI 控件)。尽管在实践中,这些额外功能通常是不需要的,而且,在 Silverlight API 中,命令绑定可能有点零散,一些真正应该支持命令的控件却不支持,这导致人们使用诸如 MVVM Light 的 EventToCommand 行为之类的解决方案。为了避免整个命令的复杂性,Blend Interactivity SDK 包含事件触发器和 CallMethodAction
,允许您将事件直接连接到方法调用,就像 Knockout 一样。
一些 Knockout 技巧 - $parent 绑定和计算型可观察对象
到目前为止,我们的比较主要集中在 Silverlight 框架和 Knockout 中以某种形式存在的功能上。在本节中,我们将简要离题,看看 Knockout 拥有的一些相当简洁的功能,而 Silverlight 缺乏等效功能。
在上一节中,我们讨论了命令绑定,其中 QuizWizardViewModel
负责从一个问题导航到下一个问题。在 Silverlight 和 Knockout 应用程序中,“下一题”按钮都存在于 QuestionViewModel
的模板中,那么当我们的 UI 的数据上下文是一个 QuestionViewModel
实例时,我们如何将此按钮连接起来以在 QuizWizardViewModel
上调用命令呢?
在 Silverlight 中,此场景没有直接支持。然而,这个问题的解决方案相对简单,我们从 QuestionViewModel
到 QuizWizardViewModel
添加一个关系,如下所示
public class QuestionViewModel : ViewModelBase
{
...
public QuestionViewModel(QuizWizardViewModel parent)
{
Parent = parent;
}
public QuizWizardViewModel Parent { get; private set;}
...
}
这允许我们从 QuestionView
用户控件内部绑定到父视图模型公开的命令:
<Button Command="{Binding Parent.NextQuestionCommand}"
Style="{StaticResource NextButtonStyle}"/>
您还可以通过某种花哨的相对源绑定来解决此问题。
尽管解决方案相对简单,但我宁愿在代码中不需要维护双向关系。如果您需要将子元素从一个父元素移动到另一个父元素,它们可能会导致各种有趣的错误。
Knockout 解决这个问题的方案再简单不过了。每当绑定上下文通过 template
绑定(或 with
绑定)改变时,父视图模型都可以通过 $parent
伪变量访问。您甚至可以通过 $parents
数组找到第 n 个父元素,或者通过 $root
找到最顶层的视图模型。因此,“下一题”按钮点击的 Knockout 绑定不需要对我们的视图模型进行任何更改
<a href="#" data-bind="click: $parent.nextQuestion"
class="next">
Next
</a>
Knockout 的另一个非常有用的特性是计算型可观察对象(以前称为依赖型可观察对象)。Silverlight 和 Knockout 实现的 QuizWizardViewModel
都将当前问题存储为问题数组中的索引。Silverlight 的 QuizWizardViewModel
公开当前问题如下
public QuestionViewModel CurrentQuestion
{
get
{
return _questionIndex >= Questions.Count ? null : Questions[_questionIndex];
}
}
这非常直接,然而,在创建从视图模型的其他属性派生出的属性时,必须小心正确地触发属性更改。在上面的示例中,无论何时 _questionIndex
发生变化,我们都需要为 CurrentQuestion
属性触发属性更改事件。在上面的示例中,这非常简单,但是对于具有从多个其他属性派生出的属性的视图模型,这可能会变成一个维护噩梦。
Knockout 对此问题有一个非常优雅的解决方案,您只需创建一个计算型可观察对象,定义一个用于确定其值的函数
this.currentQuestion = ko.computed(function () {
return this.currentQuestionIndex() < this.questions().length ?
this.questions()[this.currentQuestionIndex()] : null;
}, this);
……剩下的就交给 Knockout 吧!
该框架确保每当影响计算型可观察对象值的可观察属性发生变化时,计算型可观察对象的任何订阅者都会收到更改通知,从而更新绑定。
那么 Knockout 是如何实现这种魔法的呢?实际上非常简单。当计算型可观察对象首次创建时,Knockout 会调用评估函数(即您提供给 Knockout 的定义属性的函数)。在调用期间,Knockout 会记录任何被调用的可观察属性 getter,因此它可以确定计算型可观察对象的依赖关系。很棒。
显示结果 – Linq 对比 $.grep
当用户回答完所有问题后,将统计正确答案的数量并显示结果。为了实现这一点,QuizWizardViewModel
构建并公开了一个 ResultsViewModel
实例,该实例导致视图使用我们在文章前面看到的技术渲染结果模板。然而,计算正确答案数量的方式突出了 Silverlight 和 Knockout 之间的另一个有趣区别。
Silverlight 实现使用 Linq Count
查询来计算正确答案的数量
public void NextQuestion()
{
_questionIndex++;
if (_questionIndex >= Questions.Count)
{
int correctQuestions = Questions.Count(q => q.SelectedAnswer.IsCorrect);
Results = new ResultsViewModel(correctQuestions, Questions.Count);
}
OnPropertyChanged("CurrentQuestion");
}
由于设置了 Results
属性,ResultsView
被渲染
Knockout 实现看起来很相似
this.nextQuestion = function () {
that.currentQuestionIndex(that.currentQuestionIndex() + 1);
if (that.currentQuestionIndex() >= that.questions().length) {
var correctAnswers = $.grep(that.questions(), function (question) {
return question.selectedAnswer().isCorrect;
});
that.results(new ResultsViewModel(that.questions().length, correctAnswers.length));
}
}
然而,计算正确答案数量的方式突出了框架之间的一个有趣区别。Silverlight 应用程序可以访问大量实用程序类、API 和工具,这些都是 .NET 基类库 (BCL) 的一部分。上面代码中使用的 Linq Count
查询是此标准库的一部分,可以在 WPF、ASP.NET、WP7 或任何其他 .NET 应用程序中使用。
相比之下,JavaScript 语言缺乏与 BCL 等效的库,只有相对较少的一组内置函数。对于任何非简单的 JavaScript 应用程序,您可能需要一套更广泛的库函数。jQuery 提供少量实用函数,在上述示例中,我使用了它提供的 grep
函数。然而,像 underscore 这样的库提供了更广泛的工具集。
这里得出的结论是,Silverlight 应用程序可以访问一套全面的 API,可用于实现视图模型逻辑,而对于 JavaScript,您必须找到一个或一组合适的库来弥补 JavaScript 语言的不足。您不太可能找到像 .NET BCL 那样丰富的库。
提供测验数据 – 输入参数 & XML 对比 JavaScript & JSON
到目前为止,我们已经研究了定义用户如何与测验应用程序交互的视图模型,但我们尚未触及这些视图模型是如何构建的。如果希望我们的测验应用程序具有通用性和可重用性,那么将特定测验的数据外部化是有意义的。
使用 Silverlight 应用程序,测验以 XML 格式定义
<?xml version="1.0" encoding="utf-8"?>
<quiz title="Eco-Quiz">
<question text="How much household waste does each person create a year?"
category="rubbish"
interestingFacts="That's almost 10 times the weight of an average person. Just think how much waste is created throughout your lifetime ... how can you reduce the 50,000 kg of waste you might leave behind?">
<answer text="150 kg"
isCorrect="false"/>
<answer text="513 kg"
isCorrect="true"/>
<answer text="1025 kg"
isCorrect="false"/>
</question>
<question text="How much less energy does it take to make an aluminium can by recycling that creating a new one?"
category="cans"
interestingFacts="That's an incredible energy saving! The energy saved is enough to power a television for 3 hours.">
<answer text="10 %"
isCorrect="false"/>
<answer text="40 %"
isCorrect="false"/>
<answer text="95 %"
isCorrect="true"/>
</question>
<question text="True or false: Does a 100 watt bulb produce the same amount of light as two 50 watt bulbs?"
category="lightbulb"
interestingFacts="Not many people know that 100 watt bulbs are more efficient that 50 watt bulbs and produce more light from the energy the consume, so a simple green tip is to use fewer, higher powered bulbs.">
<answer text="True"
isCorrect="false"/>
<answer text="False"
isCorrect="true"/>
</question>
<question text="What percentage of the average households electricity bill is from appliances left on standby?"
category="washing"
interestingFacts="To save the environment ... and save you money ... before you go to bed each night, turn all your appliances off at the wall socket.">
<answer text="0-2 %"
isCorrect="false"/>
<answer text="8-10 %"
isCorrect="true"/>
<answer text="12-14 %"
isCorrect="true"/>
</question>
</quiz>
每个视图模型都负责将其对应的 XML 元素转换为所需的属性值。例如,QuizWizardViewModel
获取测验标题属性,然后从每个问题元素构造一个 QuestioNViewModel
实例
public class QuizWizardViewModel : ViewModelBase
{
public QuizWizardViewModel(XDocument xml)
{
Title = xml.Root.Attribute("title").Value;
Questions = xml.Descendants("question")
.Select((questionElement, index) => new QuestionViewModel(index, questionElement, this))
.ToList();
}
// ...
}
QuestionViewModel
构造函数执行相同的操作
public class QuestionViewModel : ViewModelBase
{
public QuestionViewModel(int index, XElement questionElement, QuizWizardViewModel parent)
{
Index = index + 1;
Parent = parent;
Text = questionElement.Attribute("text").Value;
InterestingFacts = questionElement.Attribute("interestingFacts").Value;
Category = questionElement.Attribute("category").Value;
Answers = questionElement.Descendants("answer")
.Select((answerElement, i) => new AnswerViewModel(i, answerElement))
.ToList();
}
// ...
}
从 XML 数据创建视图模型相当简单。但是,我们的应用程序最终用户如何提供这个 XML 呢?我们添加为资源或嵌入式资源的任何文件都将编译在 XAP 文件或应用程序 DLL 中,因此我们需要找到其他方法来外部化这些数据。
实例化 Silverlight 应用程序时,您可以指定许多输入参数。该
EcoQuiz 应用程序期望一个“数据”参数,该参数标识 XML 文件的位置:
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
width="100%" height="100%">
<param name="source" value="ClientBin/EcoQuiz.xap" />
<param name="initParams" value="data=quiz.xml" />
...
</object>
为了使用此参数,我们在应用程序启动时读取其值,然后使用 WebClient
下载 XML 文件
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MainPage();
string xmlFile = e.InitParams["data"];
WebClient xmlClient = new WebClient();
xmlClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(WebClient_DownloadStringCompleted);
xmlClient.DownloadStringAsync(new Uri(xmlFile, UriKind.RelativeOrAbsolute));
}
private void WebClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
XDocument xml = XDocument.Parse(e.Result);
QuizWizardViewModel quiz = new QuizWizardViewModel(xml);
((FrameworkElement)this.RootVisual).DataContext = quiz;
}
使用 Knockout 实现,视图模型构造函数也期望传递配置数据,QuizWizardViewModel
同样设置其标题属性并创建问题实例:
QuizWizardViewModel = function (config) {
var that = this;
// ------ properties
this.title = config.title;
// ...
// ------ construction logic
// create the child QuestionViewModel instances
$.each(config.questions, function (index, question) {
that.questions.push(new QuestionViewModel(index + 1, question));
});
// ...
}
同样,QuestionViewModel
设置其属性值并构建答案视图模型
QuestionViewModel = function (index, config) {
var that = this;
// ------ properties
this.index = index;
this.text = config.text;
this.category = config.category;
this.interestingFact = config.interestingFact;
this.answers = [];
this.selectedAnswer = ko.observable();
// ------ construction logic
$.each(config.answers, function (index, answer) {
// add an index to each answer, and add to our observable array
answer.index = index + 1;
that.answers.push(answer);
// identify the correct answer
if (answer.isCorrect) {
that.correctAnswer = answer;
}
});
// ...
};
那么,对于 Knockout,我们如何创建用于构建视图模型的测验数据呢?对于 JavaScript,XML 不是最实用的数据格式,大多数应用程序使用 JSON(JavaScript 对象表示法),它的优点是可以直接解析以构建 JavaScript 对象。创建测验就像下面这样简单
var quiz =
{
title : "Eco-Quiz",
questions : [
{
text: "How much household waste does each person create a year?",
category: "rubbish",
interestingFact : "That's almost 10 times the weight of an average person. Just think how much waste is created throughout your lifetime ... how can you reduce the 50,000 kg of waste you might leave behind?",
answers: [
{ text: "150 kg", isCorrect: false },
{ text: "250 kg", isCorrect: false },
{ text: "500 kg", isCorrect: true }
]
},
{
text: "How much less energy does it take to make an aluminium can by recycling that creating a new one?",
category: "cans",
interestingFact : "That's an incredible energy saving! The energy saved is enough to power a television for 3 hours.",
answers: [
{ text: "10 %", isCorrect: false },
{ text: "40 %", isCorrect: false },
{ text: "95 %", isCorrect: true }
]
},
{
text: "True or false: Does a 100 watt bulb produce the same amount of light as two 50 watt bulbs?",
category: "lightbulb",
interestingFact : "Not many people know that 100 watt bulbs are more efficient that 50 watt bulbs and produce more light from the energy the consume, so a simple green tip is to use fewer, higher powered bulbs.",
answers: [
{ text: "true", isCorrect: false },
{ text: "false", isCorrect: true }
]
},
{
text: "What percentage of the average households electricity bill is from appliances left on standby?",
category: "standby",
interestingFact : "To save the environment ... and save you money ... before you go to bed each night, turn all your appliances off at the wall socket.",
answers: [
{ text: "0-2 %", isCorrect: false },
{ text: "8-10 %", isCorrect: true },
{ text: "12-14 %", isCorrect: false }
]
}
]
};
$(document).ready(function () {
viewModel = new QuizWizardViewModel(quiz);
ko.applyBindings(viewModel);
});
这使得创建用户可以提供自己的数据的可重用测验应用程序变得更加容易。
然而,为 Silverlight 辩护,XML 是一种更“健壮”的数据格式,允许对格式良好或有效的文档添加检查(如果提供了模式)。尽管如此,这并不能解决 Silverlight 和 HTML/JavaScript 之间接口(即托管页面)可能感觉有些笨拙的事实。
结论
Silverlight 和 Knockout 有很多共同的功能,对于希望学习 JavaScript 开发的 Silverlight 开发人员来说,Knockout 是一个非常好的选择。然而,尽管它们共享大多数相同的概念,但两者的实现方式却大相径庭,Knockout 通常比 Silverlight 的等效实现简洁得多。
再次强调,我在本文中给出的许多 Knockout 比 Silverlight 简洁的例子,都源于 JavaScript、HTML 和 CSS,而不是直接源于 Knockout 框架。
所以……“Knockout 和 Silverlight 哪个更好?”
好问题!我会通过说“这取决于情况”来回避这个问题。我仍然认为 JavaScript 技术更适合简单的应用程序,而 Silverlight 更适合构建大型企业应用程序(我在我的文章 Flex, Silverlight 或 HTML5,是时候决定了…… 中更详细地讨论了这一点)。
相反,我将回答一个稍微简单的问题:“对于测验应用程序,哪种技术更好?Knockout 还是 Silverlight?”
我可以自信地说,对于本文中描述的测验应用程序,Knockout 是一个更好的选择。尽管我是一名经验更丰富的 Silverlight 开发人员,但我完成 Knockout 实现所需的时间大约是 Silverlight 的一半。这完全归因于它的简单性。
我强烈建议任何 Silverlight 开发人员学习 Knockout。你不会后悔的。