使用 AngularJS 实现 WebBinding





5.00/5 (14投票s)
本文展示了如何为 AngularJS 库实现 WebBinding,并使用它将 AngularJS 客户端对象绑定到 ASP.NET 服务器端对象。
目录
引言
在上一篇文章(WebBinding - 如何将客户端 JavaScript 对象绑定到服务器 .NET 对象)中,我们实现了一个通用的解决方案,用于将 .NET 服务器对象绑定到 JavaScript 客户端对象。对于该解决方案,我们为Knockout 库提供了专用实现。由于AngularJS已成为一个非常流行的库,我认为也有必要为该库提供实现。
乍一看,这似乎非常容易。只需实现几个小函数并为所需库提供专用实现,您就拥有了一个工作的库的 `WebBinding`。这听起来很简单。这也是我实现 `WebBinding` 解决方案时所想的。但是,正如我们在本文中将看到的,对我们(AngularJS 库)来说,它并不像听起来那么简单。
背景
为了启用特定库的专用实现,`WebBinding` 客户端脚本提供了一组应被覆盖的函数
createObjectHolder
:创建属性的对象持有者(处理属性值访问的对象)。createArrayHolder
:为保存数组的属性创建对象持有者。getObjectValue
:从对象持有者获取属性值。getArrayValue
:从数组对象持有者获取数组属性值。setObjectValue
:使用对象持有者设置属性值。setArrayValue
:使用数组对象持有者设置数组属性值。registerForPropertyChanges
:设置一个函数,该函数应在属性值更改时被调用。registerForArrayChanges
:设置一个函数,该函数应在数组属性值更改时被调用。
在某些情况下(如本文的情况),我们可能希望更改其他公共函数中的行为
addBindingMapping
:为给定的绑定映射构建一个绑定的客户端模型。applyBindings
:注册构建的绑定客户端模型以进行更改通知。beginServerChangesRequests
:启动绑定通信。createBoundObjectForPropertyPath
:创建一个客户端对象,该对象与 `WebBinding` 机制绑定。
本文展示了如何通过实现具有适当专用代码的这些函数(而不更改原始 `WebBinding` 代码的任何内容)来为 AngularJS 库实现 `WebBinding`。
本文假定您对 JavaScript 语言和 AngularJS 库有基本了解。我们的一些实现需要对 Angular 的内部原理有更深入的理解。我们将在适当的地方提到每个问题。
工作原理
AngularJS 对象的持有者
用对象包装 AngularJS 作用域的属性
在我开发 WebBinding 解决方案时,我使用了 Knockout 库。在 Knockout 库中,每个属性(我们想应用绑定的)都用一个可观察对象进行包装。使用该对象,我们可以获取属性值,设置属性值(并通知属性更改),以及订阅属性更改。遵循该设计,我设计了 `WebBinding` 的通用实现,使其基于对象持有者(包装相关属性的可观察对象),这些对象处理属性值的访问。
使用 Knockout,由于属性是可观察值的包装器,我们使用属性本身作为对象持有者。使用 AngularJS,情况有所不同。在 AngularJS 库中,我们有一个作用域对象,其普通属性可以通过Angular 表达式访问。为了实现 AngularJS 的 `WebBinding`,我们也需要 AngularJS 的对象持有者(可观察值包装器)。
为了实现这一目标,我们创建
- 一个对象用于保存绑定的 Angular 作用域(绑定的根对象),以及一个对象用于包含相应的对象持有者(作用域的属性)
function RootObjectWrapper(rootObj) { this.orgRootObj = rootObj; this.wrapperRootObj = {}; }
- 一个对象用于实现作用域属性的对象持有者
function PropertyObjectHolder() { this.pathExp = ""; this.rootObj = {}; this.isArray = false; this.innerValue = null; }
在 `PropertyObjectHolder` 对象中,我们存储根对象和相关属性的表达式。通过遍历 `RootObjectWrapper` 对象中的所有 `PropertyObjectHolder` 属性,并根据属性树构建表达式,我们可以为每个对象持有者设置适当的属性表达式,如下所示:
function validateRootObjectWrappers() {
for (var objWrapInx = 0; objWrapInx < rootObjectWrappers.length; objWrapInx++) {
var objWrapper = rootObjectWrappers[objWrapInx];
if (objWrapper instanceof RootObjectWrapper) {
objWrapper.validateProperties();
}
}
}
RootObjectWrapper.prototype.validateProperties = function () {
for (var prop in this.wrapperRootObj) {
var objHolder = this.wrapperRootObj[prop];
if (objHolder instanceof PropertyObjectHolder) {
objHolder.validateProperties(this.orgRootObj, prop);
}
}
};
PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) {
this.rootObj = rootObj;
this.pathExp = pathExpression;
if (this.isArray) {
if (this.innerValue instanceof Array) {
for (var elemInx = 0; elemInx < this.innerValue.length; elemInx++) {
var objHolder = this.innerValue[elemInx];
if (objHolder instanceof PropertyObjectHolder) {
var subPropExp = pathExpression + '[' + elemInx + ']';
objHolder.validateProperties(rootObj, subPropExp);
}
}
}
} else {
if (this.innerValue) {
for (var prop in this.innerValue) {
var objHolder = this.innerValue[prop];
if (objHolder instanceof PropertyObjectHolder) {
var subPropExp = pathExpression + '.' + prop;
objHolder.validateProperties(rootObj, subPropExp);
}
}
}
}
};
获取属性值
解析 Angular 表达式
在为我们的作用域构建好对象持有者树之后,由于每个对象持有者都包含作用域对象和适当的 Angular 表达式(针对特定属性),我们所需要的只是一个解析给定作用域的 Angular 表达式的方法。幸运的是,AngularJS 库中已经有了这个机制。使用 Angular 的$parse 服务,我们可以获取用于获取和设置属性值的函数。
使用 `$parse` 服务,我们可以使用 Angular 表达式获取 getter 函数,并使用 getter 函数和适当的对象获取属性值,如下所示:
var getter = $parse(expression);
var propertyValue = getter(scopeObject);
要设置属性值,我们可以使用 getter 函数获取 setter 函数,并设置属性值,如下所示:
var getter = $parse(expression);
var setter = getter.assign;
setter(scopeObject, propertyValue);
AngularJS 依赖注入
在前一节中,我们讨论了 `$parse` 服务。但是,`$parse` 是什么?我们如何获取它?通常,我们可以通过将一个名为 `$parse` 的参数添加到我们的 Angular 组件(控制器、指令等)的构造函数中来获取它。当 AngularJS 构建我们的组件时,它会将所需的 خدمة注入到适当的参数中。有关 Angular 依赖注入及其工作原理的更多详细信息,您可以访问以下链接:
- https://docs.angularjs.org/guide/di
- http://www.alexrothenberg.com/2013/02/11/the-magic-behind-angularjs-dependency-injection.html
由于我们的 WebBinding 实现不是由 AngularJS 构建的,因此我们必须手动注入 `$parse` 服务。为此,我们可以使用 `ng` 模块获取Angular 注入器,并使用它来获取 `$parse` 服务,如下所示:
var angInjector = angular.injector(["ng"]);
var angParser = angInjector.get("$parse");
实现 getter 函数
在我们拥有 `$parse` 服务后,就可以使用它来获取所需的属性值。我们可以这样做:
- 为每个属性设置 getter 函数
PropertyObjectHolder.prototype.getGetterFunction = function() { var ret = angParser(this.pathExp); return ret; }; PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { // ... this.getterFn = this.getGetterFunction(); // ... };
- 为每个对象持有者实现一个获取属性值的函数
PropertyObjectHolder.prototype.getValue = function () { var res = ""; if (this.isOfSimpleType()) { if (this.validate()) { res = this.getterFn(this.rootObj); } } else { res = this.innerValue; } return res; }; PropertyObjectHolder.prototype.validate = function () { if (!this.isValid()) { validateRootObjectWrappers(); if (!this.isValid()) { /*--- The object is still invalid, after the validation... ---*/ return false; } } return true; }; PropertyObjectHolder.prototype.isValid = function () { if (!this.rootObj || !this.pathExp || this.pathExp.length == 0) { return false; } return true; }; PropertyObjectHolder.prototype.isOfSimpleType = function () { if (this.isArray || this.hasPropertyObjectHolderProperties()) { return false; } if (this.innerValue) { return isSimpleType(this.innerValue); } return true; }; PropertyObjectHolder.prototype.hasPropertyObjectHolderProperties = function () { if (!this.innerValue) { return false; } for (var prop in this.innerValue) { if (this.innerValue[prop] instanceof PropertyObjectHolder) { return true; } } return false; }; function isSimpleType(val) { return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean'; }
在 `isOfSimpleType` 函数中,我们检查对象持有者所包装的属性的类型是否为简单类型。在我们的例子中,任何不持有数组、不包含内部对象持有者并且其包装属性是原生类型(字符串、数字或布尔值)的对象持有者都被视为简单类型。
在 `getValue` 函数中,我们根据类型获取属性值。如果值的类型是简单类型,我们返回 `$parse` 检索到的 getter 函数的结果。否则(如果值的类型不是简单类型),我们返回对象持有者的内部对象。此内部对象包含包装属性的内部对象持有者(用于子属性或数组元素)。
设置属性值
将值更改应用到 AngularJS
为了获取属性值,我们只是调用了 `$parse` 服务检索到的 getter 函数。要设置属性值,我们可以调用相应的 setter 函数(由 getter 函数的 `assign` 属性检索)。但是,在设置作用域属性的值时,我们通常希望更改也能反映到绑定的 DOM 元素上。
通常,在使用 AngularJS 组件时,这是透明完成的。但是,它是如何完成的?所有的魔术都隐藏在作用域的$apply 和$digest 函数中。当我们在 AngularJS 中运行我们的代码时(例如,通过ng-click 指令),我们的代码会使用作用域的 `$apply` 函数进行包装。此函数运行我们的代码并调用作用域的 `$digest` 函数,以便将我们的更改反映到相关的绑定。当我们的代码不是由 AngularJS 运行时(就像在我们的例子中一样),我们应该手动调用 `$apply` 函数。
实现 setter 函数
使用 `$parse` 服务和 `$apply` 函数,我们可以设置所需的属性值。这可以按如下方式完成:
- 为每个属性设置 setter 函数
PropertyObjectHolder.prototype.getSetterFunction = function () { var getter = angParser(this.pathExp); return getter.assign; }; PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) { // ... this.setterFn = this.getSetterFunction(); // ... };
- 为每个对象持有者实现一个设置属性值的函数
PropertyObjectHolder.prototype.setValue = function (val) { this.innerValue = val; if (this.isOfSimpleType()) { if (this.validate()) { var self = this; if (isScope(self.rootObj)) { /*--- Sometimes we should check the AngularJS scope's $$phase to ensure that we aren't already in middle of an $apply or a $digest process. But, since our script runs outside of AngularJS, we don't have to be bothered on that issue. ---*/ self.rootObj.$apply(function () { self.setterFn(self.rootObj, val); }); } else { self.setterFn(self.rootObj, val); } } } }; function isScope(obj) { if (obj && obj.$apply && obj.$watch && obj.$watchCollection) { return true; } return false; }
在 `setValue` 函数中,我们将给定值设置为对象持有者的内部值,如果值的类型是简单类型,我们调用 `$parse` 检索到的 setter 函数并将给定值作为参数。
注册 AngularJS 作用域更改
监视 AngularJS 更改
在前面的部分,我们提到了作用域的 `$digest` 函数。使用该函数,我们通知绑定的组件有关作用域的更改。但是,`$digest` 函数是如何工作的?AngularJS 如何知道哪些组件应该被通知?答案是,组件本身告诉作用域它们希望被通知哪些更改。这是通过作用域的$watch、$watchGroup 和$watchCollection 函数完成的。
在开发 Angular 组件时(例如,指令等),我们应该使用这些函数来注册相关的更改。在 `$digest` 阶段,AngularJS 会处理所有作用域注册的监视器。
注册属性更改
在我们的例子中,我们希望被通知所需的属性更改。这可以按如下方式完成:
- 实现一个函数来注册属性值更改的监视器
PropertyObjectHolder.prototype.subscribeForPropertyChange = function (propNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { this.rootObj.$watch(this.pathExp, function (newValue, oldValue) { propNotificationFunc(); }); return true; } else { this.pendingNotificationFunc = propNotificationFunc; } return false; };
- 实现一个函数来注册数组更改的监视器
PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { this.rootObj.$watchCollection(this.pathExp, function (newValue, oldValue) { arrNotificationFunc(); }); return true; } else { this.pendingNotificationFunc = arrNotificationFunc; } return false; };
这些函数的算法相当简单。如果对象持有者有效(作用域和表达式已设置),则使用给定函数注册一个监视。否则,将给定函数存储起来,直到对象有效为止。
在 `validateProperties` 函数中,我们使用存储的函数(如果存在)注册一个监视。
PropertyObjectHolder.prototype.validateProperties = function (rootObj, pathExpression) {
// ...
if (this.isArray) {
// ...
if (this.pendingNotificationFunc) {
/*--- There is a notifications function that is pending for registration. ---*/
if (this.subscribeForArrayChange(this.pendingNotificationFunc)) {
this.pendingNotificationFunc = null;
}
}
} else {
// ...
if (this.pendingNotificationFunc) {
/*--- There is a notifications function that is pending for registration. ---*/
if (this.subscribeForPropertyChange(this.pendingNotificationFunc)) {
this.pendingNotificationFunc = null;
}
}
}
};
处理数组更改
由于我们为作用域的每个属性维护一个对应的对象持有者,因此我们必须保持这两个对象同步。对于简单属性(非数组类型),我们无需进行任何特殊处理即可实现同步。但是,对于数组属性,我们需要使两个对象中的元素数量相同。
当设置一个元素数量更多的数组时,同步已在新值反映时完成。但是,当设置一个元素数量更少的数组时,我们必须手动删除多余的元素。这可以按如下方式完成:
PropertyObjectHolder.prototype.setValue = function (val) {
// ...
if (this.isArray && val instanceof Array) {
if (this.validate()) {
var self = this;
var realArr = self.getterFn(self.rootObj);
if (realArr instanceof Array) {
var realArrOldLength = realArr.length;
if (val.length < realArrOldLength) {
/*--- The new array's length is smaller than the old one... ---*/
var lengthDiff = realArrOldLength - val.length;
if (isScope(self.rootObj)) {
self.rootObj.$apply(function () {
realArr.splice(val.length, lengthDiff);
});
} else {
realArr.splice(val.length, lengthDiff);
}
}
}
}
}
};
同样的同步也需要在第二个方向进行。这可以在数组更改监视器中完成,如下所示:
PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) {
// ...
var self = this;
this.rootObj.$watchCollection(this.pathExp, function (newValue, oldValue) {
if (newValue instanceof Array &&
oldValue instanceof Array && newValue.length < oldValue.length) {
var lengthDiff = oldValue.length - newValue.length;
if (self.innerValue instanceof Array) {
self.innerValue.splice(newValue.length, lengthDiff);
}
}
arrNotificationFunc();
});
// ...
};
释放未使用的数据
在移除未使用的数组元素时,我们还必须移除它们关联的数据。在我们的实现中,每个对象持有者都在 AngularJS 作用域中注册了一个监视器。每个 AngularJS 监视器注册函数(`$watch`、`$watchGroup` 和 `$watchCollection`)都会返回一个可用于取消注册已注册监视的函数。我们可以使用该函数来取消注册对象持有者的监视,如下所示:
- 为每个对象持有者存储反注册函数
PropertyObjectHolder.prototype.subscribeForPropertyChange = function (propNotificationFunc) { // ... this.watchDeregistrationFunc = this.rootObj.$watch(this.pathExp, function (newValue, oldValue) { // ... }); // ... }; PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) { // ... this.watchDeregistrationFunc = this.rootObj.$watchCollection(this.pathExp, function (newValue, oldValue) { // ... }); // ... };
- 在注册新监视时以及在释放对象持有者时,反注册 `watch` 函数
PropertyObjectHolder.prototype.subscribeForPropertyChange = function (propNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { if (this.watchDeregistrationFunc) { /*--- De-register old watch. ---*/ this.watchDeregistrationFunc(); } // ... } // ... }; PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) { if (isScope(this.rootObj) && this.isValid()) { if (this.watchDeregistrationFunc) { /*--- De-register old watch. ---*/ this.watchDeregistrationFunc(); } // ... } // ... }; PropertyObjectHolder.prototype.dispose = function () { if (this.watchDeregistrationFunc) { this.watchDeregistrationFunc(); this.watchDeregistrationFunc = null; } if (this.isArray) { /*--- Dispose PropertyObjectHolder elements of innerValue. ---*/ if (this.innerValue instanceof Array) { for (var elemInx = 0; elemInx < this.innerValue.length; elemInx++) { if (this.innerValue[elemInx] instanceof PropertyObjectHolder) { this.innerValue[elemInx].dispose(); } } } } else { /*--- Dispose PropertyObjectHolder properties of innerValue. ---*/ if (this.innerValue) { for (var prop in this.innerValue) { if (this.innerValue[prop] instanceof PropertyObjectHolder) { this.innerValue[prop].dispose(); } } } } };
- 释放已移除的对象持有者元素
PropertyObjectHolder.prototype.applyInnerValueChanges = function () { if (this.isArray && this.innerValue instanceof Array) { if (!this.innerValueElementsShadow) { this.innerValueElementsShadow = []; } var oldLength = this.innerValueElementsShadow.length; var newLength = this.innerValue.length; if (newLength > oldLength) { /*--- New elements have been added - Add them to the shadow. ---*/ for (var elemInx = oldLength; elemInx < newLength; elemInx++) { this.innerValueElementsShadow.push(this.innerValue[elemInx]); } } else if (newLength < oldLength) { /*--- Elements have been removed - Dispose them. ---*/ var removedElements = this.innerValueElementsShadow.splice(newLength, oldLength - newLength); for (var elemInx = 0; elemInx < removedElements.length; elemInx++) { if (removedElements[elemInx] instanceof PropertyObjectHolder) { removedElements[elemInx].dispose(); } } } } }; PropertyObjectHolder.prototype.setValue = function (val) { this.innerValue = val; this.applyInnerValueChanges(); // ... }; PropertyObjectHolder.prototype.subscribeForArrayChange = function (arrNotificationFunc) { // ... this.watchDeregistrationFunc = this.rootObj.$watchCollection(this.pathExp, function (newValue, oldValue) { if (newValue instanceof Array && oldValue instanceof Array && newValue.length < oldValue.length) { var lengthDiff = oldValue.length - newValue.length; if (self.innerValue instanceof Array) { self.innerValue.splice(newValue.length, lengthDiff); self.applyInnerValueChanges(); } } arrNotificationFunc(); }); // ... };
在 `applyInnerValueChanges` 函数中,我们维护一个对象持有者 `innerValue` 的影子(用于数组属性)。如果数组的长度增加,我们将新元素添加到其影子中。如果数组的长度减小,我们将移除的元素释放掉。
在 `setValue` 和 `subscribeForArrayChange` 函数中,我们在更新 `innerValue` 后调用 `applyInnerValueChanges` 函数。
WebBinding 的 AngularJS 实现
实现 WebBinding 函数以使用 AngularJS 对象持有者
实现 WebBinding 客户端的专用部分
创建了 Angular 对象持有者之后,我们就可以使用它们来实现 WebBinding 通用实现的专用部分。
function WebBinding_ApplyAngularDedicateImplementation(wbObj) {
wbObj.createObjectHolder = function () {
var res = new PropertyObjectHolder();
return res;
};
wbObj.createArrayHolder = function () {
var res = new PropertyObjectHolder();
res.isArray = true;
res.setValue([]);
return res;
};
wbObj.getObjectValue = function (objHolder) {
return objHolder.getValue();
};
wbObj.getArrayValue = function (arrHolder) {
return arrHolder.getValue();
};
wbObj.setObjectValue = function (objHolder, val) {
objHolder.setValue(val);
};
wbObj.setArrayValue = function (arrHolder, val) {
arrHolder.setValue(val);
};
wbObj.registerForPropertyChanges = function (objHolder, propNotificationFunc) {
objHolder.subscribeForPropertyChange(propNotificationFunc);
};
wbObj.registerForArrayChanges = function (arrHolder, arrNotificationFunc) {
arrHolder.subscribeForArrayChange(arrNotificationFunc);
};
}
在 `createObjectHolder` 和 `createArrayHolder` 函数中,我们创建了我们的 Angular 对象持有者实例。在其他函数(`getObjectValue`、`getArrayValue`、`setObjectValue`、`setArrayValue`、`registerForPropertyChanges` 和 `registerForArrayChanges`)中,我们调用相应的对象持有者函数。
与 AngularJS 引导集成
由于我们的对象持有者依赖于包装 Angular 作用域对象,因此在构建客户端模型时,我们需要适当的作用域。但是,当我们设置 `WebBinding` 客户端模型时,Angular 模型尚未加载,也没有作用域对象被创建。`WebBinding` 客户端脚本在页面处理脚本时立即执行,而 AngularJS 则在 `DOMContentLoaded` 事件触发时才初始化。因此,为了在 AngularJS 加载(并且我们的作用域已创建)后构建我们的客户端模型,我们应该在 `DOMContentLoaded` 事件触发后运行构建脚本。
为了构建绑定的客户端模型,我们调用 `addBindingMapping` 函数(通过调用WebBinder 方法自动生成脚本)。`addBindingMapping` 函数的原始声明是:
this.addBindingMapping = function (_bindingId_, rootObj, bindingMappingObj) {
// ...
};
此函数接受 3 个参数:
_bindingId_
:绑定映射的标识符。(单个页面可以有多个绑定映射。)rootObj
:客户端上应用绑定的根对象。bindingMappingObj
:描述应绑定的属性的对象。
在我们的例子中,根对象(第 2 个参数)是 AngularJS 的作用域。由于在调用 `addBindingMapping` 函数时作用域尚不存在,因此我们不发送作用域本身,而是发送一个获取作用域的函数。
为了在 AngularJS 作用域创建后构建客户端模型,我们不直接在函数体内创建客户端模型,而是将参数存储起来供以后使用。这是通过覆盖 `addBindingMapping` 函数来实现的,如下所示:
function BindingMappingRegistration(bindingId, scopeGetter, bindingMappingObj) {
this.bindingId = bindingId;
this.scopeGetter = scopeGetter;
this.bindingMappingObj = bindingMappingObj;
}
var bindingMappingsRegistrations = [];
wbObj.angImp_orgAddBindingMapping = wbObj.addBindingMapping;
wbObj.addBindingMapping = function (bindingId, scopeGetter, bindingMappingObj) {
var reg = new BindingMappingRegistration(bindingId, scopeGetter, bindingMappingObj);
bindingMappingsRegistrations.push(reg);
};
在 `applyBindings` 函数中,我们使用存储的数据构建客户端对象(调用原始的 `addBindingMapping` 函数)。由于 `applyBindings` 函数也在 `DOMContentLoaded` 事件之前调用,因此我们在此实现之前等待该事件。覆盖后的函数是:
wbObj.angImp_orgApplyBindings = wbObj.applyBindings;
wbObj.applyBindings = function () {
isApplyBindingsCalled = true;
if (!isDOMContentLoaded) {
/*--- The 'DOMContentLoaded' event hasn't been fired (and, AngularJS hasn't been loaded)... ---*/
return;
}
var bindingMappingsRegistrationsCount = bindingMappingsRegistrations.length;
for (var regInx = 0; regInx < bindingMappingsRegistrationsCount; regInx++) {
var reg = bindingMappingsRegistrations[regInx];
var scope = reg.scopeGetter();
if (scope) {
var rootObjWrapper = new RootObjectWrapper(scope);
rootObjectWrappers.push(rootObjWrapper);
wbObj.angImp_orgAddBindingMapping(reg.bindingId,
rootObjWrapper.wrapperRootObj, reg.bindingMappingObj);
}
}
/*--- Clear binding-mappings registrations. ---*/
bindingMappingsRegistrations.splice(0, bindingMappingsRegistrationsCount);
wbObj.angImp_orgApplyBindings();
};
调用 `beginServerChangesRequests` 函数来启动绑定通信。由于该函数也在 `DOMContentLoaded` 事件之前调用,因此我们也等待 `DOMContentLoaded` 事件来处理该函数。
wbObj.angImp_orgBeginServerChangesRequests = wbObj.beginServerChangesRequests;
wbObj.beginServerChangesRequests = function () {
isBeginServerChangesRequestsCalled = true;
if (!isDOMContentLoaded) {
/*--- The 'DOMContentLoaded' event hasn't been fired
(and, AngularJS hasn't been loaded)... ---*/
return;
}
wbObj.angImp_orgBeginServerChangesRequests();
};
最后,我们添加一个 `DOMContentLoaded` 事件监听器来调用挂起的函数。
function onDOMContentLoaded() {
isDOMContentLoaded = true;
if (isBeginServerChangesRequestsCalled) {
/*--- The 'beginServerChangesRequests' function
was called before the 'DOMContentLoaded' event... ---*/
wbObj.beginServerChangesRequests();
}
if (isApplyBindingsCalled) {
/*--- The 'applyBindings' function was called before the 'DOMContentLoaded' event... ---*/
wbObj.applyBindings();
}
}
window.addEventListener('DOMContentLoaded', onDOMContentLoaded);
创建绑定对象
到目前为止,我们已经覆盖了 `WebBinding` 客户端的所有公共函数,除了一个——`createBoundObjectForPropertyPath` 函数。该函数用于创建与 `WebBinding` 机制绑定的客户端对象。在我开发此函数时,我使用了 Knockout 库,并且由于 Knockout 的模型是通过每个可观察属性的“对象持有者”构建的,因此我也将相同的模型用于 `WebBinding` 的模型。使用 AngularJS,由于 `WebBinding` 的模型与 AngularJS 的模型不同,因此我们也必须覆盖此函数。
原始的 `createBoundObjectForPropertyPath` 函数声明如下:
this.createBoundObjectForPropertyPath = function (rootObj, _propPath_) {
// ...
};
该函数接受 2 个参数:
rootObj
:客户端上应用绑定的根对象。_propPath_
:一个 `string`,表示从根对象到实际属性的路径。
在我们的例子中(其中 `WebBinding` 的模型与 AngularJS 的模型不同),提供的根对象(第 1 个参数)不是 `WebBinding` 的根对象(它是 Angular 的作用域)。因此,为了处理这个问题,我们实现了 `createBoundObjectForPropertyPath` 函数,以检索 `WebBinding` 的属性对象持有者(根据提供的 AngularJS 作用域和属性路径),并使用 AngularJS 的 `$parse` 服务获取属性值。
RootObjectWrapper.prototype.retrieveBoundObjectForPropertyPath = function (_propPath_) {
var resObj = this.wrapperRootObj;
var currPropPath = "";
var propPathExt = _propPath_;
while (propPathExt.length > 0) {
var currPropName;
var firstDotIndex = propPathExt.indexOf(".");
var firstBracketIndex = propPathExt.indexOf("[");
var isArrayElement = false;
if (firstBracketIndex >= 0 && (firstDotIndex < 0 || firstBracketIndex < firstDotIndex)) {
/*--- There is a bracket before the dot. - This is an array property. ---*/
if (firstBracketIndex == 0) {
/*--- This is an array's element... ---*/
var firstCloseBracketIndex = propPathExt.indexOf("]");
currPropName = propPathExt.substr(1, firstCloseBracketIndex - firstBracketIndex - 1);
/*--- If there is a dot directly after the closing bracket, we should skip it. ---*/
propPathExt = propPathExt.substr(firstCloseBracketIndex +
(((firstDotIndex - firstCloseBracketIndex) == 1) ? 2 : 1));
isArrayElement = true;
} else {
currPropName = propPathExt.substr(0, firstBracketIndex);
propPathExt = propPathExt.substr(firstBracketIndex);
}
} else {
if (firstDotIndex >= 0) {
currPropName = propPathExt.substr(0, firstDotIndex);
propPathExt = propPathExt.substr(firstDotIndex + 1);
} else {
currPropName = propPathExt;
propPathExt = "";
}
}
if (isArrayElement) {
currPropPath += '[' + currPropName + ']';
/*--- For and array element the property's name is an element's index... ---*/
currPropName = parseInt(currPropName);
} else {
if (currPropPath.length > 0) {
currPropPath += '.';
}
currPropPath += currPropName;
}
if (!resObj[currPropName] || !(resObj[currPropName] instanceof PropertyObjectHolder)) {
resObj[currPropName] =
wbObj.angImp_orgCreateBoundObjectForPropertyPath(this.wrapperRootObj, currPropPath);
/*--- The WebBinding's property-path's syntax is same as the AngularJS expression's syntax.
So, we can use it as the property-expression too. ---*/
resObj[currPropName].validateProperties(this.orgRootObj, currPropPath);
}
resObj = resObj[currPropName].getValue();
}
return resObj;
};
wbObj.angImp_orgCreateBoundObjectForPropertyPath = wbObj.createBoundObjectForPropertyPath;
wbObj.createBoundObjectForPropertyPath = function (rootObj, _propPath_) {
var res = null;
for (var wrapperInx = 0; wrapperInx < rootObjectWrappers.length; wrapperInx++) {
var objWrapper = rootObjectWrappers[wrapperInx];
if (objWrapper instanceof RootObjectWrapper && objWrapper.orgRootObj === rootObj) {
objWrapper.retrieveBoundObjectForPropertyPath(_propPath_);
}
}
if (rootObj) {
var getter = angParser(_propPath_);
res = getter(rootObj);
}
return res;
};
在 `retrieveBoundObjectForPropertyPath` 函数中,我们确保从 `webBinding` 的根对象到属性的对象持有者有一条有效的绑定路径(如果不存在则创建它)。
在 `createBoundObjectForPropertyPath` 函数中,我们根据提供的作用域找到适当的 `RootObjectWrapper`,并使用它来创建绑定路径。
实现一个类来定义 AngularJS WebBinding
最后一步,在实现了我们的 AngularJS WebBinding 脚本之后,就是将其注入到我们的页面中。这可以通过我们注入 Knockout WebBinding 脚本的相同方式来完成,即实现一个派生自 `BinderDefinitions` 的专用类,用于 AngularJS 的绑定定义。
public class AngularBinderDefinitions : BinderDefinitions
{
private const string _originalApplyDedicateImplementationFunctionName =
"WebBinding_ApplyAngularDedicateImplementation";
public AngularBinderDefinitions()
{
ApplyDedicateImplementationFunctionName = "WebBinding_ApplyAngularDedicateImplementation";
}
#region Properties
public string ApplyDedicateImplementationFunctionName { get; set; }
#endregion
#region BinderDefinitions implementation
protected override string GetApplyDedicateImplementationScript()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine(GetDedicateImplementationScript());
sb.AppendFormat("{0}({1});", ApplyDedicateImplementationFunctionName, BinderClientObjectName);
return sb.ToString();
}
#endregion
private string GetDedicateImplementationScript()
{
string res = string.Empty;
Uri resUri = new Uri
("/WebBinding.Angular;component/Scripts/AngularDedicateImplementation.js", UriKind.Relative);
lock (ResourcesLocker)
{
StreamResourceInfo resInfo = Application.GetResourceStream(resUri);
if (resInfo != null)
{
using (StreamReader sr = new StreamReader(resInfo.Stream))
{
res = sr.ReadToEnd();
}
}
}
res = Regex.Replace
(res, _originalApplyDedicateImplementationFunctionName, ApplyDedicateImplementationFunctionName);
if (MinimizeClientScript)
{
// Remove comments.
res = Regex.Replace(res, "/\\*-{3}([\\r\\n]|.)*?-{3}\\*/", string.Empty);
// Remove lines' spaces
res = Regex.Replace(res, "[\\r\\n][\\r\\n \\t]*", string.Empty);
// Remove additional spaces
res = Regex.Replace(res, " ?([=\\+\\{\\},\\(\\)!\\?:\\>\\<\\|&\\]\\[-]) ?", "$1");
}
return res;
}
}
如何使用它
在页面上应用 WebBinding
为了演示使用 AngularJS 库的 `WebBinding`,我们使用与演示使用 Knockout 库的 `WebBinding` 库相同的示例。由于唯一的变化在于客户端(用 AngularJS 而不是 Knockout 编写),我们省略了对服务器端代码的讨论,而专注于 AngularJS 库的特定更改。
在页面上应用 `WebBinding` 的第一步是获取适当的作用域(我们希望绑定到 .NET 对象的那些作用域)。这可以通过在控制器构造函数中将变量设置为控制器的作用域来完成。
// Unique view-model
var uniqueVmScope;
function uniqueVmController($scope) {
uniqueVmScope = $scope;
}
// Shared view-model
var sharedVmScope;
function sharedVmController($scope) {
sharedVmScope = $scope;
}
在我们的示例中,我们有两个作用域(由两个控制器处理)。一个用于绑定到特定页面唯一的视图模型,另一个用于绑定到整个页面共享的视图模型。为了获取这些作用域,我们添加了另外两个函数。
function getUniqueVmScope() {
return uniqueVmScope;
}
function getSharedVmScope() {
return sharedVmScope;
}
当控制器构造时(在Angular 初始化期间),我们设置适当的作用域变量。当 `WebBinding` 客户端构建时(在 `DOMContentLoaded` 事件上,Angular 初始化之后),作用域变量已经设置,并且函数返回适当的作用域。
在拥有获取作用域的函数后,我们就可以使用它们来创建一个包含相关绑定映射的 `BinderDefinitions` 对象。
@{
BinderDefinitions bd = new AngularBinderDefinitions();
bd.AddBinding("getUniqueVmScope", ViewData.Model);
bd.AddBinding("getSharedVmScope", ExampleContext.Instance.CommonBindPropertiesExampleViewModel);
}
为了使用创建的 `BinderDefinitions` 在页面上应用 `WebBinding`,我们调用 `WebBinder` 扩展方法。
@Html.WebBinder(bd)
展示示例
应用示例控制器
为了展示我们的示例,我们添加了一个 `article` 标签并将其应用于共享视图模型的控制器。
<article ng-controller="sharedVmController">
</article>
示例 1:共享视图模型 vs. 唯一视图模型
在第一个示例中,我们绑定到视图模型的一个实例,该视图模型包含一个 web 绑定的 `Text` 属性。其中一个实例与整个页面共享,另一个实例是特定页面的唯一实例。为了展示我们的示例,我们添加了两个 `input` 标签来显示绑定的 `Text` 属性(一个用于共享视图模型,一个用于唯一视图模型)。
<section>
<h3>Example 1: Shared view-model vs. unique view-model</h3>
<p class="exampleDescription">In this example, we compare between shared
(with the other pages) view-model and, unique (to this page) view-model.
We can see how the change on the shared view-model is reflected to the other pages
(open this page in some tabs/windows),
while the change on the unique view-model stays unique to that page.</p>
<h4>Shared view-model</h4>
<p>
Text: <input type="text" ng-model="text"/> -
Entered value: <span style="color :blue">{{text}}</span>
</p>
<h4>Unique view-model</h4>
<p ng-controller="uniqueVmController">
Text: <input type="text" ng-model="text"/> -
Entered value: <span style="color :blue">{{text}}</span>
</p>
</section>
服务器端代码和结果与原始示例相同。
示例 2:二维集合
在第二个示例中,我们展示了一个 web 绑定的二维整数集合。集合的值由服务器随机更新(并反映到所有客户端)。为了展示我们的示例,我们添加了两个 `input` 标签来设置集合的维度,以及一个 `table` 来显示集合。
<section>
<h3>Example 2: 2 dimensional collection</h3>
<p class="exampleDescription">In this example,
we change the columns' number and the rows' number of a 2D collection.
In addition to that, the cells' values are changed randomly by the server.
We can see how the values are synchronized with the other pages.</p>
<p>
Rows count: <input type="text" ng-model="numbersBoardRowsCount"/> -
Entered value: <span style="color :blue">{{numbersBoardRowsCount}}</span>
<br />
Columns count: <input type="text" ng-model="numbersBoardColumnsCount"/> -
Entered value: <span style="color :blue">{{numbersBoardColumnsCount}}</span>
<br />
</p>
<table style="background:lightgray;border:gray 1px solid;width:100%">
<tbody>
<tr ng-repeat="row in numbersBoard">
<!-- Since duplicates in a repeater
are not allowed (https://docs.angularjs.org/error/ngRepeat/dupes),
we use the 'track by' expression. -->
<td style="background:lightyellow;border:goldenrod 1px solid"
ng-repeat="col in row track by $index">
<span style="color :blue">{{col}}</span>
</td>
</tr>
</tbody>
</table>
</section>
服务器端代码和结果与原始示例相同。
示例 3:字符串作为集合
在第三个示例中,我们以两种方式展示了一个 web 绑定的 `string`:一种是作为 `string`,另一种是作为字符集合。为了展示我们的示例,我们添加了一个 `input` 标签来将我们的 `string` 显示为 `string`,以及一个 `table` 来将我们的 `string` 显示为字符集合。
<section>
<h3>Example 3: String as a collection</h3>
<p class="exampleDescription">In this example, we show a string as a collection of characters.</p>
<h4>The string</h4>
<p>
StringEntry: <input type="text" ng-model="StringEntry"/> -
Entered value: <span style="color :blue">{{StringEntry}}</span>
</p>
<h4>The string's characters</h4>
<table style="background:lightgray;border:gray 1px solid;width:100%">
<tbody>
<tr>
<td style="background:lightyellow;border:goldenrod 1px solid"
ng-repeat="c in StringEntryCharacters track by $index">
<span style="color :blue">{{c}}</span>
</td>
</tr>
</tbody>
</table>
</section>
服务器端代码和结果与原始示例相同。
示例 4:从客户端更改集合
在第四个示例中,我们展示了一个 web 绑定的集合,其类型比简单类型(如 `string`、`int` 等)更复杂。我们可以在客户端添加或删除这些集合中的项(并查看更改如何反映到其他客户端)。
为了在客户端创建新的集合项,以便其更改能够反映到服务器,我们必须创建已注册属性更改的对象的。为此,我们公开了一个创建 web 绑定对象的函数。
@{
BinderDefinitions bd = new AngularBinderDefinitions();
bd.CreateBoundObjectFunctionName = "createWebBoundObject";
// ...
}
为了在客户端启用添加或删除集合项,我们向作用域添加了适当的函数。
function sharedVmController($scope) {
sharedVmScope = $scope;
// Actions for example 4.
$scope.removePerson = function (person) {
var peopleArr = this.people;
var foundIndex = -1;
for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
if (peopleArr[personInx] == person) {
foundIndex = personInx;
}
}
if (foundIndex >= 0) {
peopleArr.splice(foundIndex, 1);
}
};
$scope.removeChild = function (child) {
var peopleArr = this.people;
var foundIndex = -1;
for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
var childrenArr = peopleArr[personInx].children;
for (var childInx = 0; childInx < childrenArr.length && foundIndex < 0; childInx++) {
if (childrenArr[childInx] == child) {
foundIndex = childInx;
}
}
if (foundIndex >= 0) {
childrenArr.splice(foundIndex, 1);
}
}
};
$scope.addPerson = function () {
var peopleArr = $scope.people;
var newIndex = peopleArr.length;
var propPath = "people[" + newIndex + "]";
// After the call to 'createWebBoundObject'
// the array element has already been created in the scope.
var person = createWebBoundObject($scope, propPath);
person.name.firstName = "Added_First" + (newIndex + 1);
person.name.lastName = "Added_Last" + (newIndex + 1);
person.age = 40 + newIndex;
};
$scope.addChild = function (person) {
// Find person's index.
var peopleArr = $scope.people;
var foundIndex = -1;
for (var personInx = 0; personInx
< peopleArr.length && foundIndex < 0; personInx++) {
if (peopleArr[personInx] == person) {
foundIndex = personInx;
}
}
// Add child to the found person.
if (foundIndex >= 0) {
var childrenArr = peopleArr[foundIndex].children;
var newIndex = childrenArr.length;
var propPath = "people[" + foundIndex + "].children[" + newIndex + "]";
// After the call to 'createWebBoundObject'
// the array element has already been created in the scope.
var child = createWebBoundObject($scope, propPath);
child.name.firstName = "Added_First" + (foundIndex + 1) + "_" + (newIndex + 1);
child.name.lastName = "Added_Last" + (foundIndex + 1) + "_" + (newIndex + 1);
child.age = 20 + newIndex;
}
};
}
为了展示我们的示例,我们添加了一个列表来显示我们的集合,以及按钮来应用适当的操作。
<section>
<h3>Example 4: Change collections from the client side</h3>
<p class="exampleDescription">In this example, we add and remove collecion's elements
(from the client side).
We can see how the changes are reflected to the other pages.</p>
<h4>People collection</h4>
<ol>
<li ng-repeat="p in people">Name:
<span style="color :blue">{{p.name.firstName}}</span>
<span style="color :brown">, </span>
<span style="color :blue">{{p.name.lastName}}</span>
Age: <span style="color :blue">{{p.age}}</span>
<button ng-click="$parent.removePerson(p)">Remove</button>
<br />
Children:
<ol>
<li ng-repeat="c in p.children">
Name: <span style="color :blue">{{c.name.firstName}}</span>
<span style="color :brown">, </span>
<span style="color :blue">{{c.name.lastName}}</span>
Age: <span style="color :blue">{{c.age}}</span>
<button ng-click="$parent.$parent.removeChild(c)">Remove</button>
</li>
</ol>
<button ng-click="$parent.addChild(p)">Add child</button>
</li>
</ol>
<button ng-click="addPerson()">Add person</button>
</section>
服务器端代码和结果与原始示例相同。