HTML5, JavaScript, Knockout, JQuery,Silverlight/WPF/C# 戒断指南。第二部分 - 使用 SVG、Knockout 和 MVVM 模式构建的太阳系动画。






4.98/5 (32投票s)
使用 HTML5/JavaScript 创建太阳系动画
重要提示
朋友们,如果您喜欢这篇文章,请投它一票。我也很想听听您对改进的看法以及您还想了解的内容。谢谢!引言
这是 HTML5 系列的第二部分。第一部分可以在 HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM. 找到。几年前,我偶然看到了 Bea Stollnitz(Costa)将一个普通的 ListView 转换为行星系统的示例,并对其着迷。您可以在 The power of Styles and Templates in WPF 中找到她的文章和代码。尽管 MVVM 模式在那时还没有正式化,但她严格区分了视图模型和视图的模板和样式。
我决定在 HTML5/JavaScript 中构建一个类似的应用程序,同样严格遵守 MVVM 模式。您可以在 Solar System Demo 看到结果。请确保使用支持 HTML5 的浏览器查看演示。如果您使用 Chrome 或 Firefox,您甚至可以看到行星绕太阳运行(IE 9 不支持 SVG 动画,因此如果您使用 IE 9,行星将不会移动)。演示中的行星图像速度是随机选择的,与实际行星速度无关。我将在下面详细讨论此演示。
为了实现视觉和非视觉组件之间的分离,我使用了 Knockoutjs 框架。 Knockoutjs 是一个出色的开源框架,它使开发人员能够将 HTML 中的属性和事件绑定到代表视图模型的 JavaScript 实体。 Knockoutjs 绑定类似于 WPF 和 Silverlight 的绑定。在我看来,Knockoutjs 是构建包含大量业务逻辑的 HTML 网站的必备工具。
本文绝不是关于 Knockoutjs 的详细教程,尽管我会尝试介绍它的一些最重要的功能。也许,在后续的文章中,我会提供更多关于 Knockoutjs 的信息,但在此期间,Knockoutjs 网站提供了出色的文档和教程。此外,还有两门关于 Knockoutjs 的优秀的 Pluralsite 课程:John Papa 的 Building HTML5 and JavaScript Apps with MVVM and Knockout 以及 Steve Michelotti 的 Knockout Fundamentals。(您需要订阅 pluralsite.com 才能访问教程)。
为了绘制非文本视觉元素,例如轨道,并进行动画,我使用了 SVG。SVG 是 HTML5 规范的一部分,它提供了 HTML 标签来创建各种形状,对它们进行变换以及创建动画。SVG 非常类似于 WPF/Silverlight 的 Shape/Path、变换和动画功能。同样,本文也不是 SVG 的教程,它只是演示了一些 SVG 概念。也许在未来的文章中,我会更详细地讨论 SVG。
演示
如上所述,该演示可以在 AWebPros.com/#Demos.HTML5Demos 查看。您需要一个支持 HTML5 的浏览器才能查看演示,并且要看到行星绕太阳旋转,您不应该使用 IE9(因为它不支持 SVG 动画)。特别是,我用 Chrome 和 Firefox 测试了它。这是演示的快照
页面顶部的“切换动画”按钮允许用户停止和重新启动动画。单击任何太阳系天体都可以选择它:它周围会出现一个黄色的圆圈,并且它的描述将显示在屏幕顶部,位于“切换动画”按钮的右侧。上图包含地球作为选定的太阳系天体:它被一个黄色的圆圈包围,并且在屏幕顶部,您可以看到它的描述:“地球:地球,我们的家园,是我们太阳系中唯一已知有生命的行星”。
让我们简要看一下代码(一旦使用更简单的示例解释了 Knockoutjs 的功能,我们将更仔细地查看代码)。演示的代码位于 SolarSystem 项目下。
首先,看看 Scripts 文件夹。您可以看到我们已安装了 JQuery 和 Knockout 库。在系列的第一部分中已经描述了如何将 JavaScript 库作为 NuGet 包安装:HTML5, JavaScript, Knockout, JQuery, Guide for Recovering Silverlight/WPF/C# Addicts. Part 1 - JavaScript and DOM. 此项目仅使用 JQuery 来检测文档何时加载,但 Knockoutjs 被广泛用于将 HTML 视图绑定到视图模型。
有 3 个自定义 JavaScript 文件
- SolarSystem.js 包含演示的视图模型。视图模型由一个数组
solarSystemObjects
组成。该数组包含对应于各个太阳系天体信息的对象。视图模型还有几个公共函数:toggleAnimation()
和setSelected()
(VM 中还有更多函数,但它们不在 VM 外部使用)。我从 Bea 的 WPF 项目中复制了所有太阳系天体的参数和描述,并为动画添加了随机速度(请注意,动画中的行星速度与实际行星速度无关)。 - AnimationControlBinding.js 和 XlinkHrefCustomBinding.js 包含一些自定义的 Knockoutjs 绑定,我们稍后将讨论。
HTML 文件夹下的 SolarSystem.html 文件是包含 HTML/SVG 标记代码、所需脚本引用以及少量 JavaScript 的主 HTML 文件,其目的是检测浏览器是否支持 SVG 和 SVG 动画,并将 HTML 视图绑定到视图模型。
最后,Images 文件夹下有一堆行星图像(也从 Bea 的 WPF 演示中复制)。
请注意文件的代码量非常少:SolarSystem.htm 文件(包含大量注释和完善的浏览器兼容性检查)不到 110 行;视图模型文件 SolarSystem.js 也包含详细注释,不到 70 行。两个自定义绑定文件也很小,并且应用程序不包含任何样式表。还要注意视觉部分和非视觉部分之间的分离以及代码的整体清晰度。如果没有 Knockoutjs 框架,这种代码简洁性和清晰度是不可能实现的。
面向 WPF 和 Silverlight 开发者的 Knockoutjs 框架简述
为了理解演示代码,让我们先讨论 Knockoutjs 绑定框架并提供一些简单的示例。对于那些对更深入的教程感兴趣的人,我强烈推荐 Knockoutjs 网站和上面引用的 Pluralsite 课程。简单的 Knockout 绑定示例
您可以在 SimpleKnockoutSample 项目下找到一个简单的 Knockoutjs 示例的代码。如果您运行该示例,您将看到以下屏幕
如果您尝试更改行星名称或详细信息,您会看到蓝色的完整描述也会随着您敲击键盘而改变。我为这个示例创建了两个小的自定义文件
- Scripts 文件夹下的 SolarSystemBodyVM.js 包含视图模型。
- HTML 文件夹下的 SolarSystemBody.html 包含演示的视图。
这是 SolarSystemBodyVM.js 文件的内容
/// <reference path="knockout-2.1.0.debug.js" />
// setting up the namespace
var solarSystemBodyNmspace = solarSystemBodyNmspace || {};
// set the view models to an empty object
solarSystemBodyNmspace.solarSystemBodyVM = {};
// let us have the solar system object contain only name and details
// as well as the computed fullDescription field, for the sake of simplicity
solarSystemBodyNmspace.solarSystemBodyVM = {
name : ko.observable("Earth"),
details: ko.observable("the planet we live on")
};
// computed observables should be defined outside
// of JSON object definition in order for
// 'this' reference to be correct.
// first argument to the ko.computed function is the function
// for calculating the computed observable property.
// Second argument passes an object that becomes "this" pointer inside the function.
// This is needed, since the "this" pointer in JavaScript is much more fluid than
// "this" pointer or reference in C++ or C#/Java.
solarSystemBodyNmspace.solarSystemBodyVM.fullDescription = ko.computed(function () {
return this.name() + ": " + this.details();
}, solarSystemBodyNmspace.solarSystemBodyVM);
它在 solarSystemBodyVM
视图模型中定义了两个可观察的简单“属性”:name
和 details
,以及一个计算可观察“属性”:fullDescription
。我将“属性”一词放在引号中,因为事实上,它们不是属性,而是当没有参数传递给它们时返回一个值,而在有参数传递时设置值的函数。 Knockoutjs 使用函数而不是属性的原因是因为较旧的浏览器不支持 JavaScript 属性。在构造函数中传递给函数的该值是可观察属性的默认值。(一旦我解释了可观察属性实际上是函数,我将删除“属性”一词周围的引号)。我们需要使用可观察属性的原因是,我们希望 Knockoutjs 能够检测到属性何时发生变化并修改相应的 HTML 文本或值,反之亦然——我们希望在 HTML 属性更新时更新 VM 属性。因此,可观察属性等同于 WPF 属性,当更新时会触发 PropertyChanged
事件,并且它们与 HTML 属性的绑定类似于双向 WPF 绑定。如果我们希望 HTML 只接收原始值,而不关心 VM 与视图保持同步,我们就不需要创建可观察属性——我们可以在 VM 中简单地使用 JavaScript 字段。这将映射到 WPF 的一次性绑定或绑定到不触发 PropertyChanged
事件的简单属性。
在上面显示的视图模型代码中,fullDescription
可观察属性是从 name
和 details
可观察属性计算得出的
solarSystemBodyNmspace.solarSystemBodyVM.fullDescription = ko.computed(function () { return this.name() + ": " + this.details(); }, solarSystemBodyNmspace.solarSystemBodyVM);
在 WPF 中,计算属性通常只有一个 getter,并且其 PropertyChanged
事件在它所依赖的属性中触发。这里有一个 solarySystemBodyVM
的 WPF 模拟,其中包含一个依赖于 Name
和 Detail
属性的计算属性 FullDescription
public class SolarSystemBodyVM : INotifyPropertyChanged { #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion protected void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } #region Name Property private string _name; public string Name { get { return this._name; } set { if (this._name == value) { return; } this._name = value; this.OnPropertyChanged("Name"); // we need to notify that FullDescription // property possibly changed. this.OnPropertyChanged("FullDescription"); } } #endregion Name Property #region Details Property private string _details; public string Details { get { return this._details; } set { if (this._details == value) { return; } this._details = value; this.OnPropertyChanged("Details"); // we need to notify that FullDescription // property possibly changed. this.OnPropertyChanged("FullDescription"); } } #endregion Details Property // computed property FullDescription dependent // on Name and Details properties. #region FullDescription Property public string FullDescription { get { return _name + ": " + _details; } } #endregion FullDescription Property }您可以看到,在 WPF 中,我们必须在计算属性所依赖的属性中添加对
OnPropertyChanged("FullDescription");
的调用。然而,在 Knockoutjs 中,框架会自动找出哪些属性依赖于哪些属性,从而无需在代码中引入一些不必要的额外依赖。 Knockoutjs 跟踪依赖关系的方式——第一次,它会计算出计算属性,无论如何。在其计算过程中,它会检查调用了哪些其他可观察属性,并为每个属性设置订阅,以便当该属性更改时,计算属性会重新计算。
现在让我们将注意力转向 HTML/SolarSystemBody.htm 文件中的 HTML 代码
<body> <div> Enter Solar System Body Name: <input data-bind="value:name, valueUpdate:'afterkeydown'" /> </div> <div> Enter Solar System Body Details: <input data-bind="value:details, valueUpdate:'afterkeydown'" /> </div> <div> Solar System Body full Description <span style="color:Blue" data-bind="text:fullDescription"></span> </div> </body> </html> <script src="../Scripts/jquery-1.8.1.min.js" type="text/javascript"></script> <script src="../Scripts/knockout-2.1.0.debug.js" type="text/javascript"></script> <script src="../Scripts/SolarSystemBodyVM.js" type="text/javascript"></script> <script type="text/javascript"> $(document).ready(function () { /* bind the whole DOM to the view model. If only part of the DOM needs to be bound, you can pass the DOM tag as a second parameter to ko.applyBindings function */ ko.applyBindings(solarSystemBodyNmspace.solarSystemBodyVM); }); </scrip>
有两个用于输入太阳系天体名称和详细信息的文本输入字段。它们绑定到视图模型上的 name
和 details
可观察属性。还有一个只读文本字段用于显示绑定到视图模型 fullDescription
计算可观察属性的完整描述。
视图和视图模型之间的绑定是通过 DOM 加载后调用的 ko.applyBindings
函数创建的。
让我们仔细看看绑定——它们定义在相应 HTML 标签的 data-bind 属性中,例如
data-bind="value:name, valueUpdate:'afterkeydown'"在此绑定中,
value
是内置绑定类型。 Knockoutjs 框架中有一个名为 value
的内置绑定,它知道如何绑定到“text”类型的 HTML <input/> 元素,也许还绑定到其他一些 HTML 元素。冒号后面,name
是要绑定的视图模型中的可观察字段的名称。逗号之后,我们可以添加其他绑定或当前绑定的某些属性。在我们的例子中,valueUpdate
是 value
绑定的一个属性。valueUpdate
属性的值设置为“afterkeydown”,这意味着当在键盘上按下按键时(当然,焦点在字段内),视图模型中的可观察属性应该会更新。WPF 中 valueUpdate:'afterkeydown'
的模拟是在 TextBlock
的 Text
属性的绑定中设置 UpdateSourceTrigger=PropertyChanged
。如果没有 valueUpdate:'afterkeydown'
,Knockoutjs 绑定将与没有 UpdateSourceTrigger=PropertyChanged
的 WPF Text
绑定工作方式相同,即它只在“tab 退出”或“enter”时更新,而不是在每次按键时更新。Knockoutjs 和 WPF/Silverlight 绑定之间存在一个细微但非常重要的区别。在 WPF 中,绑定绑定两个属性——一个在绑定源上,一个在绑定目标上,而在 Knockoutjs 中,绑定将源上的可观察属性绑定到目标,而不是目标属性。绑定类型决定了当源属性触发时目标的变化——例如,“value”绑定类型将改变 HTML <input/> 元素的值,而“text”绑定类型将改变 <div/> 或 <span/> 元素文本。也可以创建改变 HTML 元素多个属性的绑定类型。
计算属性 fullDescription
使用 text
内置 Knockoutjs 绑定到 span
元素
<div> Solar System Body full Description <span style="color:Blue" data-bind="text:fullDescription"></span> </div>请注意,与 WPF 绑定不同,Knockoutjs 绑定接受 JavaScript 表达式作为值,例如,我们可以写成这样,而不是绑定到
fullDescription
属性Solar System Body full Description <span style="color:Blue" data-bind="text:name() + ': ' + details()"></span>当表达式很小时,它工作得很好,但对于较大的表达式,最好在 JavaScript 中创建一个计算属性。
使用可观察数组创建太阳系天体的可选列表
现在我们将创建更接近最终结果的内容——一个太阳系天体对象的列表。我们还将允许用户在列表中选择一个对象。选定的对象将被高亮显示,并且选定太阳系天体的描述将显示在列表下方
您可以看到图片中,地球是选定的太阳系天体(其背景比其他项目更暗,前景色为白色)。您还可以看到列表下方的文本对应于地球的描述。
示例代码位于 SelectingSolarSystemBodies 项目下。该项目有两个自定义文件:Scripts/SolarSystem.js 包含视图模型,HTML/SolarSystem.htm 包含 HTML 视图。视图模型很简单
/// <reference path="knockout-2.1.0.js" />
// define namespace
var selectingSolarSystemBodiesNmspace = selectingSolarSystemBodiesNmspace || {};
// define the view model as an empty object
selectingSolarSystemBodiesNmspace.vm = {};
// constructor for solar system objects
var SolarSystemObject = function (name, details) {
this.name = name;
this.details = details;
}
// this function creates and populates the observable array of
// solar system objects
// it also creates selectedSolarSystemBody observable property and
// setSeleced function.
populateSolarSystem = function () {
var self = selectingSolarSystemBodiesNmspace.vm;
// populate observable array to contain SolarSystemObjects
// corresponding to the sun and the planets within our solar system.
self.solarSystemObjects =
ko.observableArray([
new SolarSystemObject("Sun", "The yellow dwarf star in the center of our solar system."),
new SolarSystemObject("Mercury", "The small and rocky planet Mercury is the closest planet to the Sun."),
new SolarSystemObject("Venus", "At first glance, if Earth had a twin, it would be Venus."),
new SolarSystemObject("Earth", "Earth, our home planet, is the only planet in our solar system known to harbor life."),
new SolarSystemObject("Mars", "The red planet Mars has inspired wild flights of imagination over the centuries."),
new SolarSystemObject("Jupiter", "With its numerous moons and several rings, the Jupiter system is a \"mini-solar system.\""),
new SolarSystemObject("Saturn", "Saturn is the most distant of the five planets known to ancient stargazers."),
new SolarSystemObject("Uranus", "Uranus gets its blue-green color from methane gas above the deeper cloud layers."),
new SolarSystemObject("Neptune", "Neptune was the first planet located through mathematical predictions."),
new SolarSystemObject("Pluto", "Long considered to be the smallest, coldest, and most distant planet from the Sun."),
]);
// create an observable property for a selected solar system body
self.selectedSolarSystemBody = ko.observable("");
// create a function setSelected that sets the selectedSolarSystemBody
// observable property
self.setSelected = function (selectedItem) {
self.selectedSolarSystemBody(selectedItem);
};
} ();
视图模型有一个 SolarSystemObject 对象的观察者数组。视图将绑定到它,为数组中的每个对象显示一个绿色矩形。这是视图的代码<head> <!-- get the styles from styles.css file --> <link href="styles.css" rel="stylesheet" type="text/css" /> <title></title> </head> <body> <!-- move slightly down from the top of the browser --> <div style="height:20px"></div> <!-- foreach built-in binding will bind to an array of objects --> <div data-bind="foreach:solarSystemObjects"> <!-- whatever is placed within the tag bound with foreach binding, will serv as a template for individual items within the list --> <!-- for each item within the list we use text binding to show the name of the solar system body click binding to call function setSelected on the parent view model to select a solar system body css binding to choose the selectedItem style in case the solar system body is selected --> <span class="selectableItem" data-bind="text:name, click:$parent.setSelected, css:{selectedItem: ($parent.selectedSolarSystemBody()) && (name === $parent.selectedSolarSystemBody().name)}" style="padding:20px 20px 20px 20px;"> </span> </div> <br /> <div> <!-- we show the description of the selected solar system body under the list of solar bodies via using text binding --> <span data-bind="text:selectedSolarSystemBody().details" /> </div> </body> </html> <script src="../Scripts/jquery-1.8.1.min.js" type="text/javascript"></script> <script src="../Scripts/knockout-2.1.0.debug.js" type="text/javascript"></script> <script src="../Scripts/SolarSystem.js" type="text/javascript"></script> <script type="text/javascript"> $(function () { // create the bindings between the DOM elements and // the View Model ko.applyBindings(selectingSolarSystemBodiesNmspace.vm); }); </script>
我们使用 foreach
绑定来绑定到观察者数组
<div data-bind="foreach:solarSystemObjects"> <span class="selectableItem" data-bind="text:name, click:$parent.setSelected, css:{selectedItem: ($parent.selectedSolarSystemBody()) && (name === $parent.selectedSolarSystemBody().name)}" style="padding:20px 20px 20px 20px;"> </span> </div>
此绑定将元素内的所有内容视为单个项目的模板(在我们的例子中,span
元素会为可观察数组中的每个项目重复)。
在 WPF 中,不需要特殊的绑定来绑定到可观察集合——所有从 ItemsControl
派生的视觉元素都假定其 ItemsSource
属性已绑定到集合对象。然而,在 Knockoutjs 中,“foreach”绑定绑定到集合。
与 WPF 中的情况一样,列表中单个视图项的数据上下文对应于可观察数组中的单个对象。在我们的例子中,这意味着每个 span
元素都连接到相应的 SolarSystemObject
对象。然而,与 WPF 不同的是,Knockoutjs 允许使用 $parent
表示法在绑定中引用父视图模型。在我们的例子中,单个列表项中的 $parent
指的是 selectingSolarSystemBodiesNmspace.vm
视图模型。
这是我们绑定每个列表项的方式
data-bind="text:name, click:$parent.setSelected, css:{selectedItem: ($parent.selectedSolarSystemBody()) && (name === $parent.selectedSolarSystemBody().name)}"涉及三个绑定
- “text”绑定到视图模型的
name
属性。这意味着太阳系天体的名称将显示在span
元素中。 - “click”绑定到父视图模型上的
setSelected()
函数。这意味着当用户鼠标单击span
元素时,将在selectingSolarSystemBodiesNmspace.vm
视图模型上调用setSelected
函数。 - “css”绑定是最复杂的。它意味着当条件
($parent.selectedSolarSystemBody()) && (name === $parent.selectedSolarSystemBody().name)
满足时,span 的类将更改为selectedItem
。此条件表示selectedSolarSystemBody
可观察项在视图模型上定义,并且其名称与当前项目的名称相同。
文件 HTML/styles.css 包含演示的非常简单的样式
/* sets the background for the items that are not selected */ .selectableItem { background-color:#33CC33; } /* sets the background style for an item that are not selected but has the mouse pointer hovering over it */ .selectableItem:hover { background-color:rgba(0,255,0,0.5); } /* sets the background to be darker and foreground to be white for a selected item */ .selectedItem { background-color: #3AAA3A; color: White; } /* changes the background to get a little lighter for a selected item that has the mouse pointer hovering over it */ .selectedItem:hover { background-color:#5ABB5A; }
主太阳系应用程序代码
现在我们准备讨论位于 SolarSystem 项目下的主应用程序代码。请注意,Scripts/SolarSystem.js 文件中定义的视图模型与上面介绍的 SelectingSolarSystemBodies 项目的视图模型非常相似。在 SolarSystem 项目的视图模型中添加了以下功能
- 单个
SolarSystemObject
对象具有更多属性来定义其轨道的半径、旋转速度(随机选择)以及相应的图像文件位置。 - 视图模型上定义了一个
runAnimation
可观察属性,用于打开和关闭动画。 - 有一个
toggleAnimation
方法用于切换runAnimation
值。
<div style="position:fixed"> <!-- button to stop and restart the animation --> <button id="toggleAnimationButton" data-bind="click:toggleAnimation">Toggle Animation</button> <!-- warning text to show in case the browser does not support SVG animations --> <span id="browserDoesNotSupportSVGAnimationsText" style="display:none;color:Red;font-weight:bold; font-size:30px">Your browser does not support SVG animations</span> <!-- warning text to show in case the browser does not support SVG --> <span id="browserDoesNotSupportSVGText" style="display:none;color:Red;font-weight:bold; font-size:30px">Your browser does not support SVG</span> <!-- displays information about selected solar system body --> <span id="selectedSolarSystemBodyInfo" style="margin:0px 50px;color:White;" > <span style="color:Aqua; font-weight:bold" data-bind="text:selectedSolarSystemBody().name"></span>: <span data-bind="text:selectedSolarSystemBody().details"></span> </span> </div> <svg id="solarSystem" overflow="auto" height="1300" width="1300"> <!-- foreach binding creates a visual element for each item within the View Model's observable array --> <svg data-bind="foreach:solarSystemObjects"> <!-- orbit circle --> <!-- fill="none" make it possible to click-through the svg control see point events--> <circle cx="500" cy="305" data-bind="attr:{r: radius}" stroke="yellow" stroke-width="1" fill="none"/> <!-- svg group tag (groups element under it). It allows applying transforms and animations to all the grouped element at the same time --> <g> <!-- this is built-in SVG transform animation. It animates rotation (see the "type" attribute). "from" and "to" attributes for "rotation" consist of angle in degrees followed by the x and y coordinates of the rotation center. "begin" attribute states that the rotation should start right away. "repeatCount" set to "indefinite" means that the rotation should continue for ever. Knockout bindings bind the rotation duration ("dur" attribute) to "fullCircleDuration" property of the individual solar system body View Model. Also we use animationControl custom binding connected to "runAnimation" property on the whole solar system View Model to control the animation start and stop --> <animateTransform attributeName="transform" type="rotate" from="0 500 305" to="360 500 305" begin="0s" data-bind="attr:{dur: fullCircleDuration}, animationControl: $parent.runAnimation" repeatCount="indefinite"/> <!-- this svg tag groups the elements under it and shifts them horizontally by 475 pixels to align them with the center of the orbits--> <svg x="475"> <!-- this svg tag groups the elements under it and shifts them vertically by 280 pixels to align them with the center of the orbits. Also it shifts the individual solar system body visual objects along axis x. The shift is determined by the radius of its orbit (see data-bind="attr:{x: radius}") --> <svg y="280" data-bind="attr:{x: radius}"> <!-- solar system body image to be displayed. --> <image data-bind="click:$parent.setSelected, xlinkHref:imagePath" width="20" height="20" x="15" y="15" /> <!-- yellow circle around the selected image. Displays only for the image that has been selected --> <circle data-bind="visible: ($parent.selectedSolarSystemBody()) && (name === $parent.selectedSolarSystemBody().name)" cx="25" cy="25" r="23" stroke="yellow" stroke-width="2" fill-opacity="0" /> </svg> </svg> </g> </svg> </svg>
对于视图模型中的每个 SolarSystemObject
,我们创建一个 SVG 圆圈来表示其轨道
<circle cx="500" cy="305" data-bind="attr:{r: radius}" stroke="yellow" stroke-width="1" fill="none"/>我们使用“attr” Knockoutjs 绑定将 SVG 圆圈的
r
属性绑定到相应的 SolarSystemObject
的 radius
参数。看看 <image/> 元素
<image data-bind="click:$parent.setSelected, xlinkHref:imagePath" width="20" height="20" x="15" y="15" />
单击图像将调用 setSelected()
函数,将相应的视图模型对象设置为 selectedSolarSystemBody
。为了设置 SVG <image/> 的文件路径,我们需要使用 SVG <image/> 属性 xlink:href
。此属性中包含一个冒号,因此如果我们尝试为此属性使用“attr” Knockoutjs 绑定,它会感到困惑。因此,我创建了一个自定义绑定“xlinkHref”来代替。该绑定的代码位于 Scripts/XlinkHrefCustomBinding.js 文件中。
ko.bindingHandlers.xlinkHref = { // init function updates a property value on an element only once - after the binding is set on it init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { var value = valueAccessor(); element.href.baseVal = value; } };
现在让我们看看 SVG 动画及其绑定
<animateTransform attributeName="transform" type="rotate" from="0 500 305" to="360 500 305" begin="0s" data-bind="attr:{dur: fullCircleDuration}, animationControl: $parent.runAnimation" repeatCount="indefinite"/>
animationTransform
的“rotate”类型意味着动画将执行旋转。 “from”和“to”属性由角度(以度为单位)后跟旋转中心的 x 和 y 坐标组成,因此动画将从 0 度旋转到 360 度,旋转中心点为 (500, 305)。“begin”属性设置为 0 秒,表示动画立即开始。“repeatCount”设置为“indefinite”表示动画将永远运行(当然,除非它在 JavaScript 中被显式停止)。“attr”绑定将 <animateTransform/> 元素的“dur”属性绑定到相应视图模型的 fullCircleDuration
。
我还使用了自定义绑定“animationControl”,该绑定绑定到 SolarSystem
视图模型上的 runAnimation
标志。让我们多谈谈“animationControl”自定义绑定。在 WPF 中,如果您希望在 VM 发生变化时在控件中执行某些操作,最不具侵入性的方法是创建一个附加属性,在其回调中,当控件发生变化时强制控件发生变化。此附加属性可以绑定到视图模型属性,以便视图模型可以控制何时触发更改。通过 Knockoutjs 自定义绑定,可以实现类似的不具侵入性的 HTML 对象行为更改。
特别是,在这里,当 runAnimation
可观察属性更改时,我们希望停止或重新启动动画。停止或重新启动动画可以通过 JavaScript 调用 element.endElement()
函数来停止动画,并调用 element.beginElement()
函数来重新启动动画,其中 element
是动画元素。自定义绑定“animationControl”实现了这一点。
ko.bindingHandlers.animationControl = { // update function executes every time update of the binding is triggered by // a change in the View Model observable property update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) { // element passed here is the SVG animation var startOrStop = valueAccessor()(); // if the browser does not support SVG animations - return if (!element.beginElement) return; if (startOrStop) { // if startOrStop flag is true - start the animation element.beginElement(); } else { // if startOrStop falg is false - stop the animation element.endElement(); } } };它绑定到
runAnimation
标志。当标志更改时,它会触发绑定的 update()
函数。传递给此函数的第一个参数是定义绑定的元素,即我们案例中的 SVG <animationTransform/>。第二个属性 valueAccessor
允许我们提取视图模型 runAnimation
标志的更改值。根据此值,我们决定是调用 element.endElement()
还是 element.beginElement()
函数。