WebBinding - 如何将客户端 JavaScript 对象绑定到服务器 .NET 对象






4.96/5 (26投票s)
本文分步解释了如何实现一种绑定机制,将 .NET 服务器对象绑定到 JavaScript 客户端对象,并将其用于创建 MVVM Web 应用程序。
目录
引言
单页 Web 应用程序如今非常常见。为了简化视图(HTML 元素)和模型(JavaScript 对象)之间的绑定,我们可以使用其中一个 JavaScript 库(例如 Knockout、Angular、Backbone、Ember 等)来帮助我们在页面上应用该绑定。
有时,我们还希望将 Web 页面与服务器端发生的一些更改同步。 SignalR 库能够从服务器端调用客户端函数(反之亦然)。这对于该目的非常有帮助,但这不是我想要的绑定。
使用 WPF(与 MVVM 模式),我们可以通过更改视图模型中的相应值来将业务逻辑模型的更改反映到视图(并且,由于 WPF 绑定,更改会自动反映到 UI)。该方法适用于 Windows 应用程序,但当处理 Web 应用程序时,情况则不同。在 Windows 应用程序中,.NET 模型(客户端)和视图在同一侧(客户端),而在 Web 世界中,.NET 模型在服务器侧(Web 服务器),视图是在客户端的 Web 浏览器中呈现的 HTML 页面。
由于我希望继续像在 WPF 中一样编程(将视图绑定到 .NET 视图模型),即使在 Web 应用程序中也是如此,我认为如果我们也为 Web 应用程序提供相同的机制会很好。但是,让我们看看我们已经拥有什么。我们可以将 JavaScript 对象的属性绑定到 HTML 元素(使用帮助我们应用该绑定的 JavaScript 库之一)。我们可以将 .NET 对象的属性(依赖属性)绑定到其他 .NET 对象的属性(使用 WPF 绑定)。因此,我们所需要做的就是完成一个将客户端 JavaScript 对象绑定到相应服务器 .NET 对象的方法。
本文展示了如何实现一种机制,用于将 .NET 服务器对象绑定到 JavaScript 客户端对象,并将其应用于 HTML 元素。
背景
本文假设您熟悉 C# 语言、JavaScript 语言和 ASP.NET 框架。
由于我们使用 Knockout 库实现客户端脚本的专用部分(由于客户端脚本的通用实现,它也可以用其他库实现...),因此建议您也基本熟悉该库。
工作原理
处理服务器绑定
描述绑定映射
创建我们的 Web 绑定机制的第一步是,创建一个数据结构,用于描述服务器对象属性和客户端对象属性之间的映射。
public enum BindingMappingMode
{
None = 0,
OneWay = 1,
OneWayToSource = 2,
TwoWay = 3
}
public class PropertyMapping
{
public PropertyMapping()
{
MappingMode = BindingMappingMode.TwoWay;
}
public string ClientPropertyPath { get; set; }
public string ServerPropertyPath { get; set; }
public bool IsCollection { get; set; }
public BindingMappingMode MappingMode { get; set; }
}
public class BindingMapping
{
public BindingMapping()
{
}
public BindingMapping(PropertyMapping rootPropertyMapping)
{
_rootMapping = rootPropertyMapping;
}
#region RootMapping
private PropertyMapping _rootMapping;
public PropertyMapping RootMapping
{
get { return _rootMapping ?? (_rootMapping = new PropertyMapping()); }
}
#endregion
}
RootMapping
属性保存服务器对象属性和客户端对象属性之间的映射。该属性的值足以描述具有简单类型(例如 string
、int
等)的属性,但是当处理具有其自身属性的更复杂类型时,我们需要一种方法来描述子属性的映射。为此,我们添加了 SubPropertiesMapping
和 HasSubPropertiesMapping
属性。
#region SubPropertiesMapping
private List<BindingMapping> _subPropertiesMapping;
public List<BindingMapping> SubPropertiesMapping
{
get { return _subPropertiesMapping ?? (_subPropertiesMapping = new List<BindingMapping>()); }
}
#endregion
#region HasSubPropertiesMapping
public bool HasSubPropertiesMapping
{
get { return _subPropertiesMapping != null && _subPropertiesMapping.Count != 0; }
}
#endregion
另一种必须支持的情况是集合。对于简单类型集合的情况,RootMapping
属性的值可能就足够了。但是,当处理更复杂类型的集合时,我们需要一种方法来描述集合元素类型的映射。为此,我们添加了 CollectionElementMapping
和 HasCollectionElementMapping
属性。
#region CollectionElementMapping
private List<BindingMapping> _collectionElementMapping;
public List<BindingMapping> CollectionElementMapping
{
get { return _collectionElementMapping ?? (_collectionElementMapping = new List<BindingMapping>()); }
}
#endregion
#region HasCollectionElementMapping
public bool HasCollectionElementMapping
{
get { return _collectionElementMapping != null && _collectionElementMapping.Count != 0; }
}
#endregion
处理属性绑定
所以,我们有一个用于保存绑定映射的数据结构。现在,我们需要一种机制来应用该绑定。幸运的是,我们已经有一个内置于 .NET 框架中的相同机制 - WPF 数据绑定。我们也可以将该机制用于我们的目的。
为了处理服务器对象属性的绑定,我们添加一个 DependencyObject
,并对其 DependencyProperty
应用 WPF 绑定。
public class PropertyBindingHandler : DependencyObject, IDisposable
{
#region Fields
private BindingsHandler _rootBindingsHandler;
private BindingMapping _mapping;
private object _rootObject;
private Binding _propertyBinding;
#endregion
public PropertyBindingHandler(BindingsHandler rootBindingsHandler, BindingMapping mapping, object rootObject,
string serverPropertyPath, string clientPropertyPath)
{
_rootBindingsHandler = rootBindingsHandler;
_mapping = mapping;
_rootObject = rootObject;
ServerPropertyPath = serverPropertyPath;
ClientPropertyPath = clientPropertyPath;
_propertyBinding = new Binding
{
Source = rootObject,
Path = new PropertyPath(serverPropertyPath),
Mode = BindingMappingModeToBindingMode(mapping.RootMapping.MappingMode)
};
BindingOperations.SetBinding(this, CurrentValueProperty, _propertyBinding);
}
private BindingMode BindingMappingModeToBindingMode(BindingMappingMode bmm)
{
if (bmm == BindingMappingMode.TwoWay)
{
return BindingMode.TwoWay;
}
if (bmm == BindingMappingMode.OneWayToSource)
{
return BindingMode.OneWayToSource;
}
return BindingMode.OneWay;
}
public string ClientPropertyPath { get; set; }
public string ServerPropertyPath { get; set; }
#region CurrentValue
protected object CurrentValue
{
get { return GetValue(CurrentValueProperty); }
set { SetValue(CurrentValueProperty, value); }
}
public static readonly DependencyProperty CurrentValueProperty =
DependencyProperty.Register("CurrentValue", typeof(object),
typeof(PropertyBindingHandler), new UIPropertyMetadata(null));
#endregion
#region IDisposable implementation
public void Dispose()
{
BindingOperations.ClearBinding(this, PropertyBindingHandler.CurrentValueProperty);
}
#endregion
}
这样,我们的服务器对象属性就绑定到了 CurrentValue
属性。此外,为了在服务器对象属性发生更改时收到通知,我们为 CurrentValue
属性的每次更改引发一个事件。
public class ValueChangedEventArgs : EventArgs
{
public object OldValue { get; set; }
public object NewValue { get; set; }
}
public class PropertyBindingHandler : DependencyObject, IDisposable
{
// ...
public static readonly DependencyProperty CurrentValueProperty =
DependencyProperty.Register("CurrentValue", typeof(object),
typeof(PropertyBindingHandler), new UIPropertyMetadata(null, OnCurrentValueChanged));
private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
PropertyBindingHandler pbh = o as PropertyBindingHandler;
if (null == pbh)
{
return;
}
EventHandler<ValueChangedEventArgs> handler = pbh.CurrentValueChanged;
if (null != handler)
{
ValueChangedEventArgs args = new ValueChangedEventArgs
{
OldValue = e.OldValue,
NewValue = e.NewValue
};
handler(pbh, args);
}
}
public event EventHandler<ValueChangedEventArgs> CurrentValueChanged;
// ...
}
在本文后面,我们将看到如何使用 CurrentValue
属性将该绑定应用于客户端(JavaScript)对象属性。
处理对象绑定
目前,我们有一个类来处理单个属性的绑定。下一步是创建一个类来处理整个对象的绑定。
public class BindingsHandler : DependencyObject, IDisposable
{
#region Fields
// Dictionary of (client sub-property-path, property binding handler).
private readonly Dictionary<string, PropertyBindingHandler> _propertiesHandlers;
#endregion
public BindingsHandler(object serverObject, BindingMapping bm)
{
_propertiesHandlers = new Dictionary<string, PropertyBindingHandler>();
Locker = new object();
InitializePropertiesHandlers(serverObject, bm);
}
#region Properties
public string BindingId { get; set; }
public object Locker { get; private set; }
#endregion
#region InitializePropertiesHandlers
private void InitializePropertiesHandlers(object serverObject, BindingMapping bm)
{
AddPropertiesHandlers(serverObject, bm, string.Empty, string.Empty);
}
public void AddPropertiesHandlers(object serverObject, BindingMapping bm,
string baseServerPropertyPath, string baseClientPropertyPath)
{
if (bm.RootMapping.MappingMode == BindingMappingMode.None)
{
return;
}
string serverPropertyPath =
string.IsNullOrEmpty(baseServerPropertyPath)
? bm.RootMapping.ServerPropertyPath
: string.IsNullOrEmpty(bm.RootMapping.ServerPropertyPath)
? baseServerPropertyPath
: string.Format("{0}.{1}", baseServerPropertyPath, bm.RootMapping.ServerPropertyPath);
string clientPropertyPath =
string.IsNullOrEmpty(baseClientPropertyPath)
? bm.RootMapping.ClientPropertyPath
: string.IsNullOrEmpty(bm.RootMapping.ClientPropertyPath)
? baseClientPropertyPath
: string.Format("{0}.{1}", baseClientPropertyPath, bm.RootMapping.ClientPropertyPath);
if (bm.HasSubPropertiesMapping)
{
foreach (BindingMapping subPropertyMapping in bm.SubPropertiesMapping)
{
AddPropertiesHandlers(serverObject, subPropertyMapping, serverPropertyPath, clientPropertyPath);
}
}
else
{
PropertyBindingHandler pbh = null;
if (CheckAccess())
{
pbh = new PropertyBindingHandler(this, bm, serverObject, serverPropertyPath, clientPropertyPath);
}
else
{
Dispatcher.Invoke(
new Action(
() =>
pbh =
new PropertyBindingHandler(this, bm, serverObject, serverPropertyPath, clientPropertyPath)));
}
pbh.CurrentValueChanged += OnPropertyCurrentValueChanged;
PropertyBindingHandler oldHandler = null;
lock (Locker)
{
if (_propertiesHandlers.ContainsKey(clientPropertyPath))
{
oldHandler = _propertiesHandlers[clientPropertyPath];
_propertiesHandlers.Remove(clientPropertyPath);
}
_propertiesHandlers.Add(clientPropertyPath, pbh);
}
if (oldHandler != null)
{
oldHandler.Dispose();
}
}
}
void OnPropertyCurrentValueChanged(object source, ValueChangedEventArgs e)
{
EventHandler<PropertyValueChangedEventArgs> handler = PropertyValueChanged;
if (handler != null)
{
PropertyBindingHandler pbh = source as PropertyBindingHandler;
PropertyValueChangedEventArgs arg =
new PropertyValueChangedEventArgs
{
NewValue = e.NewValue,
OldValue = e.OldValue,
ServerPropertyPath = pbh != null ? pbh.ServerPropertyPath : string.Empty,
ClientPropertyPath = pbh != null ? pbh.ClientPropertyPath : string.Empty
};
handler(this, arg);
}
}
#endregion
public event EventHandler<PropertyValueChangedEventArgs> PropertyValueChanged;
#region IDisposable implementation
public void Dispose()
{
List<PropertyBindingHandler> propHandlers;
lock (Locker)
{
propHandlers = _propertiesHandlers.Select(p => p.Value).ToList();
_propertiesHandlers.Clear();
}
propHandlers.ForEach(p => p.Dispose());
}
#endregion
}
public class PropertyValueChangedEventArgs : ValueChangedEventArgs
{
public string ServerPropertyPath { get; set; }
public string ClientPropertyPath { get; set; }
}
在该类中,我们有一个 Dictionary
,它为每个客户端属性(_propertiesHandlers
)保存属性绑定处理程序。创建该 Dictionary
后,我们根据给定的 BindingMapping
对其进行初始化(在 InitializePropertiesHandlers
方法中)。
请注意,为了将对绑定依赖属性的所有访问同步到单个线程,PropertyBindingHandler
实例是使用 Dispatcher
的线程创建的。这将在下一节中详细讨论。
处理页面绑定
调度程序线程
如上一节所述,我们使用 Dispatcher
来同步我们的一些操作。这个主题更多地与 WPF 相关而不是 ASP.NET,但是,由于这是我们绑定机制的主要部分,我认为值得多说几句。
处理 依赖属性(在我们的例子中,是 PropertyBindingHandler.CurrentValue
属性)时,我们必须处理的问题之一是,它们只能由创建包含它们的依赖对象的线程(又称“所有者线程”)使用。为了解决这个问题,每个 DispatcherObject
(DependencyObject
的基类)都有一个 Dispatcher
属性,它保存创建该对象的线程的调度程序。使用该调度程序,我们可以在适当的线程上调用带有依赖属性的操作,即使是从不拥有该对象的线程。
为了应用这种行为,我们创建了一个 单例 类,该类维护一个用于我们操作的调度程序线程。
public class BinderContext : IDisposable
{
#region Singleton implementation
private BinderContext()
{
InitializeBinderDispatcher();
}
private readonly static BinderContext _instance = new BinderContext();
public static BinderContext Instance { get { return _instance; } }
#endregion
#region Binder Dispatcher
private System.Windows.Threading.Dispatcher _binderDispatcher;
private Thread _binderDispatcherThread;
private void InitializeBinderDispatcher()
{
AutoResetEvent threadCreateEvent = new AutoResetEvent(false);
_binderDispatcherThread =
new Thread(() =>
{
_binderDispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
threadCreateEvent.Set();
System.Windows.Threading.Dispatcher.Run();
});
_binderDispatcherThread.Start();
threadCreateEvent.WaitOne();
}
private void ShutdownBinderDispatcher()
{
if (_binderDispatcherThread != null && _binderDispatcher != null)
{
_binderDispatcher.InvokeShutdown();
_binderDispatcherThread.Join();
_binderDispatcher = null;
_binderDispatcherThread = null;
}
}
public void Invoke(Action a)
{
if (_binderDispatcher != null)
{
_binderDispatcher.Invoke(a);
}
}
public void BeginInvoke(Action a)
{
if (_binderDispatcher != null)
{
_binderDispatcher.BeginInvoke(a);
}
}
#endregion
#region IDisposable implementation
public void Dispose()
{
ShutdownBinderDispatcher();
}
#endregion
}
在该类中,我们运行一个线程来管理我们的调度程序循环,并公开方法来同步 (Invoke
) 和异步 (BeginInvoke
) 调用调度程序线程上的操作。
注册页面绑定
为了注册我们页面的绑定,我们
- 添加一个
Dictionary
用于保存页面的绑定处理程序// Dictionary of (page-id, (binding-id, bindings-handler)). private readonly Dictionary<string, Dictionary<string, BindingsHandler>> _pagesBindings;
- 添加一个方法用于注册页面的绑定
public object Locker { get; private set; } public bool RegisterBinding(string pageId, string bindingId, object serverObject, BindingMapping objectBindingMapping, bool overrideIfExist) { if (string.IsNullOrEmpty(pageId) || string.IsNullOrEmpty(bindingId) || null == serverObject || null == objectBindingMapping) { return false; } BindingsHandler bh = null; Invoke(() => bh = new BindingsHandler(serverObject, objectBindingMapping)); bh.BindingId = bindingId; lock (Locker) { _pagesLastActionTime[pageId] = DateTime.Now; Dictionary<string, BindingsHandler> pages; if (_pagesBindings.ContainsKey(pageId)) { pages = _pagesBindings[pageId]; } else { pages = new Dictionary<string, BindingsHandler>(); _pagesBindings[pageId] = pages; } if (!pages.ContainsKey(bindingId) || overrideIfExist) { pages[bindingId] = bh; } } return true; }
处理更改通知
客户端对象模型
数据结构
到目前为止,我们已经讨论了服务器的对象模型。现在,让我们创建客户端 JavaScript 代码。我们做的第一件事是创建一个脚本文件 (BinderClient.js),其中包含一个对象(构造函数),用于处理客户端的绑定。
/*---
Since we want to remove the comments when minimizing the code,
the whole of the comments (including this one) are wrapped with a similar pattern.
Variables names can be contained in other variables names
(for example: the 'bindingObject' name is contained in the 'bindingObjectId' name).
Since when we minimizing the code we perform a search for the variables names,
a variable name that contains the name of another variable, can lead to an unexpected result.
Therefore, in order to prevent this behavior, some of the variables' names are decorated with a '_' prefix and suffix.
---*/
function _WebBindingBinderClient_(id) {
var self = this;
this.pageId = id;
}
在该对象中,我们添加对象来存储所需的数据
- 一个用于存储绑定对象的对象
/*--- Holds the root object, for each root-object id. ---*/ var _rootBindingObjects_ = {};
- 一个用于存储每个绑定的绑定对象的标识符的对象
/*--- Holds the root-object id, for each binding id. ---*/ var _bindingsObjectsIds_ = {};
- 一个用于存储每个绑定属性的创建者函数对象
/*--- Holds the objects' properties' creator functions, for each root-object id. ---*/ var _objectsCreators_ = {};
- 一个用于存储每个绑定属性对象的属性名称的对象(如果它不是简单类型)
/*--- Holds the objects' properties' names, for each root-object id. ---*/ var _objectsPropertiesNames_ = {};
除了这些对象之外,我们还添加了一些用于获取和设置其数据的功能。
function _getBindingObjectId_(_bindingId_) {
return _bindingsObjectsIds_[_bindingId_];
}
function _getBindingObject_(_bindingId_) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var res = _rootObjId_ ? _rootBindingObjects_[_rootObjId_] : null;
return res;
}
function _setBindingObject_(_bindingId_, rootObj) {
var _rootObjId_ = null;
for (var objId in _rootBindingObjects_) {
if (_rootBindingObjects_[objId] == rootObj) {
_rootObjId_ = objId;
}
}
if (!_rootObjId_) {
_rootObjId_ = _bindingId_ + "O";
_rootBindingObjects_[_rootObjId_] = rootObj;
}
_bindingsObjectsIds_[_bindingId_] = _rootObjId_;
return _rootObjId_;
}
function _getBindingObjectsCreators_(_rootObjId_) {
return _objectsCreators_[_rootObjId_];
}
function _setObjectCreator_(_rootObjId_, _propId_, objCreator) {
var bindingObjectsCreators = _retrieveObjectProperty_(_objectsCreators_, _rootObjId_, {});
bindingObjectsCreators[_propId_] = objCreator;
}
function _getObjectCreator_(_rootObjId_, _propId_) {
var res;
var bindingObjectsCreators = _getBindingObjectsCreators_(_rootObjId_);
if (_propId_ && bindingObjectsCreators && bindingObjectsCreators[_propId_]) {
res = bindingObjectsCreators[_propId_];
} else {
res = _createEmptyObject_;
}
return res;
}
function _getObjectPropertiesNames_(_rootObjId_, _propId_) {
var bindingObjectsPropertiesNames = _objectsPropertiesNames_[_rootObjId_];
return bindingObjectsPropertiesNames ? bindingObjectsPropertiesNames[_propId_] : null;
}
function _setObjectPropertiesNames_(_rootObjId_, _propId_, objProperties) {
var bindingObjectsPropertiesNames = _retrieveObjectProperty_(_objectsPropertiesNames_, _rootObjId_, {});
bindingObjectsPropertiesNames[_propId_] = objProperties;
}
function _retrieveObjectProperty_(obj, propName, defualtValue) {
var p = obj[propName];
if (!p) {
p = defualtValue;
obj[propName] = p;
}
return p;
}
通用实现
如前所述,我们的解决方案是将服务器 .NET 对象绑定到客户端 JavaScript 对象。对于将 JavaScript 对象绑定到 HTML DOM 元素,我们有其他 JavaScript 库(例如 Knockout 等)可以使用。为了使我们的解决方案与任何 JavaScript 库兼容,我们将专用代码(针对特定库)的实现分开。为此,我们添加了用于保存专用函数的变量。
- 用于创建对象持有者(特定库的包装对象)的函数
this.createObjectHolder = function() { return {}; }; this.createArrayHolder = function() { return []; };
- 用于从对象持有者获取或设置对象的函数
this.getObjectValue = function(objHolder) { return {}; }; this.getArrayValue = function(arrHolder) { return []; }; this.setObjectValue = function(objHolder, val) {}; this.setArrayValue = function(arrHolder, val) {};
- 用于注册更改通知的函数
this.registerForPropertyChanges = function(objHolder, propNotificationFunc) {}; this.registerForArrayChanges = function(arrHolder, arrNotificationFunc) {};
为了使用 knockout 库实现这些函数,我们添加了一个脚本文件 (KnockoutDedicateImplementation.js),其中包含专用实现。
function WebBinding_ApplyKnockoutDedicateImplementation(wbObj) {
wbObj.getObjectValue = function (objHolder) {
return objHolder();
};
wbObj.getArrayValue = function (arrHolder) {
return arrHolder();
};
wbObj.setObjectValue = function (objHolder, val) {
objHolder(val);
};
wbObj.setArrayValue = function (arrHolder, val) {
arrHolder(val);
};
wbObj.createObjectHolder = function () {
return ko.observable();
};
wbObj.createArrayHolder = function () {
return ko.observableArray([]);
};
wbObj.registerForPropertyChanges = function (objHolder, propNotificationFunc) {
objHolder.subscribe(function (newValue) {
propNotificationFunc();
});
};
wbObj.registerForArrayChanges = function (arrHolder, arrNotificationFunc) {
arrHolder.subscribe(function (changes) {
arrNotificationFunc();
}, null, "arrayChange");
};
}
这是我们唯一需要编写的专用代码。所有其他实现都只使用标准 JavaScript。稍后,我们将看到它们如何结合在一起。
构建客户端模型
为了根据绑定映射为绑定属性创建对象,我们添加了一个使用适当存储的创建者函数的函数。
function _createObject_(_rootObjId_, _propId_) {
var creatorFunc = _getObjectCreator_(_rootObjId_, _propId_);
var res = creatorFunc();
return res;
}
为了设置适当的创建者函数,我们添加
- 一个用于为非数组属性设置创建者函数的函数
function _addCreatorForObjectId_(_rootObjId_, _propId_, objPropNames) { /*--- We create a constructor function only for the root property - the sub-properties will be created by the constructor function ---*/ /*--- There can be another binding that uses the same property. So, get the exist creator function. ---*/ var initialObjectCreator = _getObjectCreator_(_rootObjId_, _propId_); if (objPropNames.length > 0) { var objCreator = function () { var objHolder = initialObjectCreator(); var obj = self.getObjectValue(objHolder); for (var propInx = 0; propInx < objPropNames.length; propInx++) { var currPropName = objPropNames[propInx]; var currPropId = _propId_ + "." + currPropName; obj[currPropName] = _createObject_(_rootObjId_, currPropId); } return objHolder; }; _setObjectCreator_(_rootObjId_, _propId_, objCreator); } else { /*--- If there is no property paths for the array's element it is a simple type. Just return an empty object. ---*/ _setObjectCreator_(_rootObjId_, _propId_, initialObjectCreator); } }
- 一个用于为数组属性设置创建者函数的函数
function _addCreatorsForArrayId_(_rootObjId_, _propId_, elementPropNames, isArrayOfArray) { /*--- Add creator for the array ---*/ _setObjectCreator_(_rootObjId_, _propId_, _createEmptyArray_); /*--- If the element of the array is array itself, its creator will be added by another call... ---*/ if (!isArrayOfArray) { /*--- Add creator for the array's element ---*/ var arrayElementId = _propId_ + "[]"; _addCreatorForObjectId_(_rootObjId_, arrayElementId, elementPropNames); } } function _createEmptyArray_() { var arrHolder = self.createArrayHolder(); self.setArrayValue(arrHolder, []); return arrHolder; }
为了为每个绑定映射设置适当的数据,我们
- 添加一个方法,用于从
BindingMapping
生成客户端对象字符串。public string ToClientBindingMappingObjectString() { StringBuilder sb = new StringBuilder(); AppendClientBindingMappingObjectString(sb); return sb.ToString(); } private void AppendClientBindingMappingObjectString(StringBuilder sb) { string clientPropPath = RootMapping.ClientPropertyPath ?? string.Empty; string[] clientPathParts = clientPropPath.Split('.'); if (clientPropPath.Length > 1) { // Create binding-mapping with the separated client property-path. bool isLowerPart = true; BindingMapping separatedMapping = null; for (int partInx = clientPathParts.Length - 1; partInx >= 0; partInx--) { BindingMapping currMapping; if (isLowerPart) { currMapping = FromPropertyMapping(clientPathParts[partInx], RootMapping.ServerPropertyPath, RootMapping.MappingMode); currMapping.RootMapping.IsCollection = RootMapping.IsCollection; if (HasSubPropertiesMapping) { SubPropertiesMapping.ForEach(p => currMapping.SubPropertiesMapping.Add(p)); } if (HasCollectionElementMapping) { CollectionElementMapping.ForEach(p => currMapping.CollectionElementMapping.Add(p)); } isLowerPart = false; } else { currMapping = FromPropertyMapping(clientPathParts[partInx], string.Empty, RootMapping.MappingMode); currMapping.SubPropertiesMapping.Add(separatedMapping); } separatedMapping = currMapping; } if (separatedMapping != null) { separatedMapping.AppendSimpleClientBindingMappingObjectString(sb); } } else { AppendSimpleClientBindingMappingObjectString(sb); } } private void AppendSimpleClientBindingMappingObjectString(StringBuilder sb) { // In order to minimize the client's object string, // we minimize the properties' names: // PP - PropertyPath // SPM - SubPropertiesMapping // CEM - CollectionElementMapping // IC - IsCollection sb.Append("{PP:\""); sb.Append(RootMapping.ClientPropertyPath); sb.Append("\",SPM:["); if (HasSubPropertiesMapping) { bool isFirstSubProperty = true; foreach (BindingMapping subPropertyMapping in SubPropertiesMapping) { if (isFirstSubProperty) { isFirstSubProperty = false; } else { sb.Append(','); } subPropertyMapping.AppendClientBindingMappingObjectString(sb); } } sb.Append("],CEM:["); if (HasCollectionElementMapping) { bool isFirstElementProperty = true; foreach (BindingMapping elemMapping in CollectionElementMapping) { if (isFirstElementProperty) { isFirstElementProperty = false; } else { sb.Append(','); } elemMapping.AppendClientBindingMappingObjectString(sb); } } sb.Append("],IC:"); sb.Append(RootMapping.IsCollection ? "true" : "false"); sb.Append('}'); } public static BindingMapping FromPropertyMapping(string clientPropertyName, string serverPropertyPath, BindingMappingMode mappingMode = BindingMappingMode.TwoWay) { BindingMapping bm = new BindingMapping(); bm.RootMapping.ClientPropertyPath = clientPropertyName; bm.RootMapping.ServerPropertyPath = serverPropertyPath; bm.RootMapping.MappingMode = mappingMode; return bm; }
- 添加一个函数,用于根据客户端对象字符串更新客户端模型。
this.addBindingMapping = function (_bindingId_, rootObj, bindingMappingObj) { /*--- Add binding object mapping. ---*/ var _rootObjId_ = _setBindingObject_(_bindingId_, rootObj); /*--- Add objects' creators functions and, properties arrays. ---*/ _addObjectsCreatorsAndProperties_(_rootObjId_, bindingMappingObj); }; function _addObjectsCreatorsAndProperties_(_rootObjId_, bindingMappingObj) { _addSubObjectsCreatorsAndProperties_(_rootObjId_, "", bindingMappingObj); } function _addSubObjectsCreatorsAndProperties_(_rootObjId_, basePropId, subBindingMappingObj) { /*--- In order to minimize the client's object string, the properties' names are minimized: PP - PropertyPath SPM - SubPropertiesMapping CEM - CollectionElementMapping IC - IsCollection ---*/ var _propId_ = basePropId; if (_propId_ != "" && subBindingMappingObj.PP != "") { _propId_ += "."; } _propId_ += subBindingMappingObj.PP; if (subBindingMappingObj.IC) { /*--- It is an array ---*/ var colElemMapping = subBindingMappingObj.CEM; var isArrayOfArray = colElemMapping.length == 1 && colElemMapping[0].IC; var elemPropNames = []; for (var elemPropInx = 0; elemPropInx < colElemMapping.length; elemPropInx++) { var currElemMapping = colElemMapping[elemPropInx]; var currElemProp = currElemMapping.PP; if (currElemProp != "") { elemPropNames.push(currElemProp); } _addSubObjectsCreatorsAndProperties_(_rootObjId_, _propId_ + "[]", currElemMapping); } _addCreatorsForArrayId_(_rootObjId_, _propId_, elemPropNames, isArrayOfArray); _addObjectPropertiesNames_(_rootObjId_, _propId_ + "[]", elemPropNames); } else { /*--- It isn't an array ---*/ var subPropMapping = subBindingMappingObj.SPM; var subPropNames = []; for (var subPropInx = 0; subPropInx < subPropMapping.length; subPropInx++) { var currSubMapping = subPropMapping[subPropInx]; var currSubProp = currSubMapping.PP; if (currSubProp != "") { subPropNames.push(currSubProp); } _addSubObjectsCreatorsAndProperties_(_rootObjId_, _propId_, currSubMapping); } _addCreatorForObjectId_(_rootObjId_, _propId_, subPropNames); _addObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames); } } function _addObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames) { var currPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_); if (currPropNames && currPropNames.length > 0) { for (var newPropInx = 0; newPropInx < subPropNames.length; newPropInx++) { var newPropName = subPropNames[newPropInx]; var isPropAlreadyExist = false; for (var oldPropInx = 0; oldPropInx < currPropNames.length; oldPropInx++) { if (newPropName == currPropNames[oldPropInx]) { isPropAlreadyExist = true; } } if (!isPropAlreadyExist) { currPropNames.push(newPropName); } } } else { _setObjectPropertiesNames_(_rootObjId_, _propId_, subPropNames); } }
处理数据请求
为了从客户端向服务器发送请求,我们添加了一个发送 AJAX POST 请求的函数。
function _sendAjaxPost_(postData, handleResponseFunc) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4) {
if (handleResponseFunc) {
handleResponseFunc(xmlhttp);
}
}
};
var url = "/webBindingHandler";
xmlhttp.open("POST", url, true);
xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xmlhttp.send(postData);
}
为了通过 ASP.NET 路由 路由绑定器客户端请求,以便 由自定义 HTTP 处理程序处理,我们创建了一个 HTTP 处理程序来处理绑定器请求,并创建了一个 HTTP 路由器来路由到该 HTTP 处理程序。
internal class BinderHttpHandler : IHttpHandler
{
#region IHttpHandler implementation
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
}
#endregion
}
internal class BinderRouteHandler : IRouteHandler
{
public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new BinderHttpHandler();
}
}
创建 HTTP 路由器后,我们必须注册我们的路由。我们可以通过向 RouteTable
添加新的 Route
来实现。我们可以通过将注册代码添加到 Global.asax.cs 文件中的 Application_Start
方法(或应用程序启动中的其他位置)来实现此目标。以这种方式,我们必须为使用我们技术的任何应用程序添加注册代码。在我们的案例中,为了避免这项任务,我们采用了不同的方法,并在第一次注册页面绑定时自动注册它。
public class BinderContext : IDisposable
{
// ...
private static bool _isBinderHandlerRegistered = false;
public bool RegisterBinding(string pageId, string bindingId,
object serverObject, BindingMapping objectBindingMapping, bool overrideIfExist)
{
// ...
// Register the HTTP route handler.
if (!_isBinderHandlerRegistered)
{
var binderRoute = new Route("webBindingHandler", new BinderRouteHandler());
RouteTable.Routes.Insert(0, binderRoute);
_isBinderHandlerRegistered = true;
}
// ...
}
// ...
}
稍后,我们将实现我们的 HTTP 处理程序来处理我们的绑定器客户端请求。
通知客户端属性更改
发送属性更改通知
为了将客户端的属性更改通知服务器,我们必须发送包含相关更改的通知消息。这可以通过以下方式完成:
/*--- Holds the properties' paths of the properties that have been changed, for each binding id. ---*/
var _pendingChangedPropertiesPathes_ = {};
function _sendPropertiesChangeNotification_() {
var propertiesNotification = "[";
var hasNotifications = false;
var isFirstBindingId = true;
for (var _bindingId_ in _pendingChangedPropertiesPathes_) {
var pendingProperties = _pendingChangedPropertiesPathes_[_bindingId_];
if (pendingProperties && pendingProperties.length > 0) {
if (isFirstBindingId) {
isFirstBindingId = false;
} else {
propertiesNotification += ",";
}
hasNotifications = true;
propertiesNotification += '{"BindingId":"' + _bindingId_ + '","PropertiesNotifications":[';
var isFirstPropPath = true;
while (pendingProperties.length > 0) {
if (isFirstPropPath) {
isFirstPropPath = false;
} else {
propertiesNotification += ",";
}
var _propPath_ = pendingProperties.pop();
propertiesNotification += _createPropertyPathNotification_(_bindingId_, _propPath_);
}
propertiesNotification += ']}';
}
}
propertiesNotification += "]";
if (hasNotifications) {
var postData = "requestId=propertiesChangedNotification" +
"&pageId=" + self.pageId + "&propertiesNotification=" + propertiesNotification;
_sendAjaxPost_(postData, function (xmlhttp) { });
}
}
function _createPropertyPathNotification_(_bindingId_, _propPath_) {
var objHolder = _getPropertyObject_(_bindingId_, _propPath_);
var propVal = objHolder ? self.getObjectValue(objHolder) : null;
var res = '{"PropertyPath":"' + _propPath_ + '", "NewValue":"' + propVal + '"}';
return res;
}
在 _createPropertyPathNotification_
函数中,我们构建一个 JSON 字符串,其中包含属性的属性路径和属性的新值。
在 _sendPropertiesChangeNotification_
函数中,我们构建一个通知消息,其中包含所有待处理(已更改但尚未发送通知)属性更改,并发送一个包含该消息的 AJAX 请求。
处理特殊字符
正如你们中一些人可能已经注意到的,我们使用 *url* 格式发送 AJAX 请求的内容。使用 *url* 格式,有些字符具有特殊含义(如 &
、%
等...)。为了将这些字符作为纯文本发送,它们应该被编码。此外,由于我们使用 JSON 格式,一些字符(如 \
、"
等...)也必须更改。因此,为了正确发送属性值,它们的某些字符必须被编码。这可以按以下方式完成:
function _NoteReplacement_(orgNote, replacementString) {
this.orgNote = orgNote;
this.replacementString = replacementString;
}
/*--- Initialize special notes encoding, for notifications messages. ---*/
var _notesReplacementsForSend_ = [];
_notesReplacementsForSend_.push(new _NoteReplacement_(' ', '%20'));
_notesReplacementsForSend_.push(new _NoteReplacement_('`', '%60'));
_notesReplacementsForSend_.push(new _NoteReplacement_('~', '%7E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('!', '%21'));
_notesReplacementsForSend_.push(new _NoteReplacement_('@', '%40'));
_notesReplacementsForSend_.push(new _NoteReplacement_('#', '%23'));
_notesReplacementsForSend_.push(new _NoteReplacement_('$', '%24'));
_notesReplacementsForSend_.push(new _NoteReplacement_('%', '%25'));
_notesReplacementsForSend_.push(new _NoteReplacement_('^', '%5E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('&', '%2F%2D%26%2D%2F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('*', '%2A'));
_notesReplacementsForSend_.push(new _NoteReplacement_('(', '%28'));
_notesReplacementsForSend_.push(new _NoteReplacement_(')', '%29'));
_notesReplacementsForSend_.push(new _NoteReplacement_('_', '%5F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('-', '%2D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('+', '%2B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('=', '%3D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('[', '%5B'));
_notesReplacementsForSend_.push(new _NoteReplacement_(']', '%5D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('{', '%7B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('}', '%7D'));
_notesReplacementsForSend_.push(new _NoteReplacement_('|', '%7C'));
_notesReplacementsForSend_.push(new _NoteReplacement_('\\', '%5C%5C'));
_notesReplacementsForSend_.push(new _NoteReplacement_('"', '%5C%22'));
_notesReplacementsForSend_.push(new _NoteReplacement_('\'', '%27'));
_notesReplacementsForSend_.push(new _NoteReplacement_(':', '%3A'));
_notesReplacementsForSend_.push(new _NoteReplacement_(';', '%3B'));
_notesReplacementsForSend_.push(new _NoteReplacement_('?', '%3F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('/', '%2F'));
_notesReplacementsForSend_.push(new _NoteReplacement_('>', '%3E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('.', '%2E'));
_notesReplacementsForSend_.push(new _NoteReplacement_('<', '%3C'));
_notesReplacementsForSend_.push(new _NoteReplacement_(',', '%2C'));
function _getFixedValueForSend_(orgVal) {
var orgValStr = orgVal ? orgVal.toString() : "";
if (orgValStr === '[object Object]' && orgVal !== '[object Object]') {
/*--- This is an object... ---*/
orgValStr = "";
}
var newValStr = "";
for (var noteInx = 0; noteInx < orgValStr.length; noteInx++) {
var currNote = orgValStr.charAt(noteInx);
var isReplaced = false;
for (var replacementInx = 0; replacementInx < _notesReplacementsForSend_.length && !isReplaced; replacementInx++) {
var currReplacement = _notesReplacementsForSend_[replacementInx];
if (currNote == currReplacement.orgNote) {
newValStr += currReplacement.replacementString;
isReplaced = true;
}
}
if (!isReplaced) {
newValStr += currNote;
}
}
return newValStr;
}
function _createPropertyPathNotification_(_bindingId_, _propPath_) {
var objHolder = _getPropertyObject_(_bindingId_, _propPath_);
var propVal = objHolder ? self.getObjectValue(objHolder) : null;
var fixedVal = _getFixedValueForSend_(propVal);
var res = '{"PropertyPath":"' + _propPath_ + '", "NewValue":"' + fixedVal + '"}';
return res;
}
_notesReplacementsForSend_
对象包含特殊字符及其替换字符串。对于 '&'
备注,我们设置一个包装字符串 ('/-&-/'
) - 我们在服务器端将此 string
转换回来。
在 _getFixedValueForSend_
函数中,我们将给定值转换为编码的 string
。
在 _createPropertyPathNotification_
函数中,我们使用 _getFixedValueForSend_
函数来转换属性的值。
处理属性更改请求
如前所述,对于每个服务器属性,我们都有一个绑定的依赖属性(PropertyBindingHandler.CurrentValue
属性)。因此,为了为服务器属性应用新值,我们可以直接更改其绑定的依赖属性。
public class PropertyBindingHandler : DependencyObject, IDisposable
{
// ...
public void SetCurrentValue(object value)
{
object currValue = GetCurrentValue();
if (currValue == value)
{
return;
}
if (CheckAccess())
{
CurrentValue = value;
}
else
{
Dispatcher.Invoke(new Action(() => CurrentValue = value));
}
}
// ...
}
public class BindingsHandler : DependencyObject, IDisposable
{
// ...
public void SetCurrentValue(string clientPropertyPath, object value)
{
PropertyBindingHandler pbh = GetPropertyBindingHandler(clientPropertyPath);
if (pbh != null)
{
pbh.SetCurrentValue(value);
}
}
public PropertyBindingHandler GetPropertyBindingHandler(string clientPropertyPath)
{
PropertyBindingHandler res = null;
lock (Locker)
{
if (_propertiesHandlers.ContainsKey(clientPropertyPath))
{
res = _propertiesHandlers[clientPropertyPath];
}
}
return res;
}
// ...
}
由于依赖属性只能由其所有者线程更改,我们使用 Dispatcher
设置其值。
为了获取请求内容,我们创建了一个等效的数据结构(与 propertiesNotification
请求字段的 JSON string
相同的结构)。
public class PropertiesValuesNotificationData
{
public string BindingId { get; set; }
public List<PropertyValueData> PropertiesNotifications { get; set; }
}
public class PropertyValueData
{
public string PropertyPath { get; set; }
#region NewValue
private string _newValue;
public string NewValue
{
get { return _newValue; }
set
{
// Remove the wrapping for the '&' characters.
_newValue = Regex.Replace(value, "/-&-/", "&");
}
}
#endregion
}
使用该数据结构,我们可以按如下方式处理属性通知请求,以应用新的属性值:
internal class BinderHttpHandler : IHttpHandler
{
// ...
public void ProcessRequest(HttpContext context)
{
string requestId = context.Request.Params["requestId"];
switch (requestId)
{
case "propertiesChangedNotification":
{
HandleClientPropertiesChangedNotificationRequest(context);
break;
}
}
}
// ...
protected bool HandleClientPropertiesChangedNotificationRequest(HttpContext context)
{
string pageId = context.Request.Params["pageId"];
string propertiesNotificationStr = context.Request.Params["propertiesNotification"];
JavaScriptSerializer jss = new JavaScriptSerializer();
PropertiesValuesNotificationData[] propertiesNotification =
jss.Deserialize<PropertiesValuesNotificationData[]>(propertiesNotificationStr);
BinderContext.Instance.PostPropertiesUpdate(pageId, propertiesNotification);
return true;
}
}
public class BinderContext : IDisposable
{
public BindingsHandler GetBindingsHandler(string pageId, string bindingId)
{
if (string.IsNullOrEmpty(pageId) ||
string.IsNullOrEmpty(bindingId))
{
return null;
}
BindingsHandler res = null;
lock (Locker)
{
if (_pagesBindings.ContainsKey(pageId))
{
Dictionary<string, BindingsHandler> pages =
_pagesBindings[pageId];
if (pages.ContainsKey(bindingId))
{
res = pages[bindingId];
}
}
}
return res;
}
public void UpdateProperties(string pageId, PropertiesValuesNotificationData[] propertiesNotification)
{
foreach (PropertiesValuesNotificationData pn in propertiesNotification)
{
BindingsHandler bh = GetBindingsHandler(pageId, pn.BindingId);
if (bh != null)
{
foreach (PropertyValueData pv in pn.PropertiesNotifications)
{
bh.SetCurrentValue(pv.PropertyPath, pv.NewValue);
}
}
}
}
public void PostPropertiesUpdate(string pageId, PropertiesValuesNotificationData[] propertiesNotification)
{
BeginInvoke(() => UpdateProperties(pageId, propertiesNotification));
}
}
在 UpdateProperties
方法中,我们为每个绑定 ID 获取相应的 BindingsHandler
,并设置更改属性的新值。
在 HandleClientPropertiesChangedNotificationRequest
方法中,我们根据给定请求的 propertiesNotification
参数创建一个 PropertiesValuesNotificationData
对象集合,并异步运行 UpdateProperties
方法。为了防止由于线程竞态条件导致较晚请求的代码在较早请求的代码之前运行的情况,我们将所有更新操作同步到调度程序线程(通过使用 BeginInvoke
方法)。
通知服务器属性更改
请求服务器更改通知
为了在客户端属性上应用服务器属性的更改,我们需要对每个属性的更改进行通知。我们可以通过发送 Ajax 请求并在其响应中获取所需的通知来实现此目标。这可以按如下方式完成:
this.lastChangesResult = "[]";
function _requestServerChanges_() {
var postData = "requestId=propertyChangeRequest" +
"&pageId=" + self.pageId +
"&lastChangesResult=" + self.lastChangesResult;
_sendAjaxPost_(postData, _handleServerChangesResult_);
}
function _handleServerChangesResult_(xmlhttp) {
if (xmlhttp.status == 200) {
_applyServerChangesResponse_(xmlhttp.responseText);
}
setTimeout(function() {
_requestServerChanges_();
}, 0);
}
function _applyServerChangesResponse_(response) {
var parsedResponse = eval("(" + response + ")");
for (var elementInx = 0; elementInx < parsedResponse.length; elementInx++) {
var currElement = parsedResponse[elementInx];
var currProperties = currElement ? currElement.PropertiesValues : null;
if (currProperties) {
for (var propertyResultInx = 0; propertyResultInx < currProperties.length; propertyResultInx++) {
var currPropertyResult = currProperties[propertyResultInx];
_applyServerPropertyChange_(currElement.BindingId, currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
}
}
}
self.lastChangesResult = response;
}
function _applyServerPropertyChange_(_bindingId_, _propPath_, newValue) {
var propObject = _getPropertyObject_(_bindingId_, _propPath_);
if (propObject) {
self.setObjectValue(propObject, newValue);
}
}
function _getPropertyObject_(_bindingId_, _propPath_) {
var propObject = null;
var currVal = _getBindingObject_(_bindingId_);
var propPathExt = _propPath_;
while (propPathExt.length > 0) {
var firstDotIndex = propPathExt.indexOf(".");
var currPathPart;
if (firstDotIndex > 0) {
currPathPart = propPathExt.substr(0, firstDotIndex);
propPathExt = propPathExt.substr(firstDotIndex + 1);
} else {
currPathPart = propPathExt;
propPathExt = "";
}
propObject = _getSubPropertyObject_(currVal, currPathPart);
currVal = propObject ? self.getObjectValue(propObject) : null;
}
return propObject;
}
function _getSubPropertyObject_(obj, subPropPath) {
var propObject = null;
if (obj && subPropPath && subPropPath.length > 0) {
var propNamePart;
var arrayIndicesPart;
var firstBracketIndex = subPropPath.indexOf("[");
if (firstBracketIndex > 0) {
propNamePart = subPropPath.substr(0, firstBracketIndex);
arrayIndicesPart = subPropPath.substr(firstBracketIndex);
} else {
propNamePart = subPropPath;
arrayIndicesPart = "";
}
propObject = obj[propNamePart];
while (arrayIndicesPart.length > 0) {
var firstCloseBracketIndex = arrayIndicesPart.indexOf("]");
var currIndexStr;
if (firstCloseBracketIndex > 0) {
currIndexStr = arrayIndicesPart.substr(1, firstCloseBracketIndex - 1);
arrayIndicesPart = arrayIndicesPart.substr(firstCloseBracketIndex + 1);
} else {
currIndexStr = arrayIndicesPart.substr(1, arrayIndicesPart.length - 2);
arrayIndicesPart = "";
}
var currIndex = parseInt(currIndexStr);
var arrVal = propObject ? self.getArrayValue(propObject) : null;
if (arrVal && arrVal.length > currIndex) {
propObject = arrVal[currIndex];
} else {
propObject = null;
}
}
}
return propObject;
}
在 _getPropertyObject_
函数中,我们根据给定的属性路径获取属性。
在 _requestServerChanges_
函数中,我们发送一个 AJAX 请求以获取服务器属性更改。
在 _handleServerChangesResult_
中,我们根据响应应用属性更改,并请求进行另一次更改。
在每次更改请求中,我们还发送上次更改的响应。通过这种方式,我们可以更新服务器关于客户端属性状态的信息。
获取服务器更改通知
为了创建关于更改值的通知,我们必须知道哪些值已更改(未与客户端同步)。为此,对于每个属性,我们添加计数器以指示客户端和服务器之间的差异(如果值不同,则属性未同步)。
public uint ChangeCounter { get; set; }
public uint ClientChangeCounter { get; set; }
public bool HasUpdatedValue { get { return ChangeCounter != ClientChangeCounter; } }
使用这些计数器,我们可以生成更改响应。
public class ServerPropertyValueRespose
{
public string SubPropertyPath { get; set; }
public string NewValue { get; set; }
public uint ChangeCounter { get; set; }
}
public class PropertyBindingHandler : DependencyObject, IDisposable
{
// ...
public ServerPropertyValueRespose GetUpdatedValueResponse()
{
// If this is a collection, the NewValue is the new collection's elements count,
// otherwise, the NewValue is the current binded value.
return new ServerPropertyValueRespose
{
SubPropertyPath = ClientPropertyPath,
ChangeCounter = ChangeCounter,
NewValue = GetCurrentValueString()
};
}
public string GetCurrentValueString()
{
object val = GetCurrentValue();
return val != null ? val.ToString() : string.Empty;
}
// ...
}
public class BindingsHandler : DependencyObject, IDisposable
{
// ...
public List<ServerPropertyValueRespose> GetUpdatedValuesResponses()
{
List<PropertyBindingHandler> changedProperties;
lock (Locker)
{
// Since we want the iteration on '_propertiesHandlers' to be performed inside the 'lock',
// we convert the query's result to a list. If we don't do it,
// because of the LINQ's differed-execution, the iteration will occur outside of the 'lock'.
changedProperties =
_propertiesHandlers.Where(p => p.Value.HasUpdatedValue).OrderBy(p => p.Key).Select(p => p.Value).
ToList();
}
// When we get the value of the 'PropertyBindingHandler.CurrentValue' property,
// we wait to the action to be performed on the dispatcer's thread.
// If we do it inside the 'lock' and, there is another operation on the dispatcher's thread,
// that is waiting for locking the same object, we can encounter a deadlock.
// Therefore, we get the updated values list, outside of the 'lock'.
List<ServerPropertyValueRespose> res = changedProperties.Select(p => p.GetUpdatedValueResponse()).ToList();
return res;
}
// ...
}
为了指示属性的更改,我们每次属性更改时都增加 ChangeCounter
。
private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
// ...
pbh.ChangeCounter++;
// ...
}
异步 HTTP 处理程序
为了通知客户端服务器属性的更改,当有属性更改时,我们必须发送响应消息。如果我们在收到客户端请求时有更改,我们可以立即将这些更改发送回去。但是,如果此时没有更改,我们必须找到一种方法,在更改存在时将其发送回去。
处理这种情况的一种方法是,总是发送一个响应(即使是空的)。这种方法不是很好,因为它会用大量不必要的(大多数响应都是空的)流量使网络超载。
处理这种情况的另一种方法是同步等待直到更改存在,然后完成请求。这种方法也不是很好,因为请求可能会花费大量时间并暂停其他请求。
因此,我们希望等待直到有属性更改,但在此期间,我们希望释放 ASP.NET 来处理其他请求。为此,我们创建了一个异步 HTTP 处理程序。
internal class BinderHttpAsyncHandler : IHttpAsyncHandler
{
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
ChangeRequestAsyncResult res = new ChangeRequestAsyncResult(cb, context, extraData);
res.BeginProcess();
return res;
}
public void EndProcessRequest(IAsyncResult result)
{
}
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
}
}
internal class ChangeRequestAsyncResult : IAsyncResult
{
private bool _isCompleted;
private bool _isCompletedSynchronously;
private Object _state;
private AsyncCallback _callback;
private HttpContext _context;
public ChangeRequestAsyncResult(AsyncCallback callback, HttpContext context, Object state)
{
_callback = callback;
_context = context;
_state = state;
_isCompleted = false;
_isCompletedSynchronously = false;
}
#region IAsyncResult implementation
public object AsyncState
{
get { return _state; }
}
public System.Threading.WaitHandle AsyncWaitHandle
{
get { return null; }
}
public bool CompletedSynchronously
{
get { return _isCompletedSynchronously; }
}
public bool IsCompleted
{
get { return _isCompleted; }
}
#endregion
public void BeginProcess()
{
}
}
在该异步 HTTP 处理程序中,我们开始异步操作,以处理更改请求。此异步操作的实现如下:
internal class ChangeRequestAsyncResult : IAsyncResult
{
// ...
private List<BindingsHandler> _bindingsHandlers;
public void BeginProcess()
{
ThreadPool.QueueUserWorkItem(o => Process());
}
protected void Process()
{
string pageId = _context.Request.Params["pageId"];
string lastChangesResult = _context.Request.Params["lastChangesResult"]; // The change results from the previous request.
JavaScriptSerializer jss = new JavaScriptSerializer();
ServerBindingValuesRespose[] lastResponses = jss.Deserialize<ServerBindingValuesRespose[]>(lastChangesResult);
foreach (var sbvr in lastResponses)
{
BindingsHandler bh = BinderContext.Instance.GetBindingsHandler(pageId, sbvr.BindingId);
if (bh != null)
{
bh.UpdateClientCounters(sbvr.PropertiesValues);
}
}
_bindingsHandlers = BinderContext.Instance.GetBindingsHandlers(pageId);
if (_bindingsHandlers.Any(b => b.HasUpdatedValue))
{
Complete();
}
else
{
_bindingsHandlers.ForEach(b => b.PropertyValueChanged += OnPropertyValueChanged);
}
}
protected void OnPropertyValueChanged(object source, PropertyValueChangedEventArgs e)
{
AsyncComplete();
}
private void AsyncComplete()
{
_bindingsHandlers.ForEach(b => b.PropertyValueChanged -= OnPropertyValueChanged);
Complete();
}
protected void Complete()
{
BinderContext.Instance.BeginInvoke(
() =>
{
List<ServerBindingValuesRespose> resList = _bindingsHandlers.Where(b => b.HasUpdatedValue).
Select(b => new ServerBindingValuesRespose
{
BindingId = b.BindingId,
PropertiesValues = b.GetUpdatedValuesResponses()
}).ToList();
ThreadPool.QueueUserWorkItem(
o =>
{
JavaScriptSerializer jss = new JavaScriptSerializer();
string res = jss.Serialize(o);
_context.Response.Write(res);
_isCompleted = true;
_callback(this);
}, resList);
});
}
}
internal class ServerBindingValuesRespose
{
public string BindingId { get; set; }
public List<ServerPropertyValueRespose> PropertiesValues { get; set; }
}
在 Process
方法中,我们使用客户端属性状态更新更改计数器,并完成请求。如果存在属性更改,我们立即调用 Complete
方法。否则,我们会在属性更改时调用它。
在 Complete
方法中,我们获取所有属性更改,并使用它们创建响应。
为了将更改请求路由到我们的异步 HTTP 处理程序,我们可以按如下方式更改我们的 HTTP 路由器:
internal class BinderRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
string requestId = requestContext.HttpContext.Request.Params["requestId"];
if (requestId == "propertyChangeRequest")
{
return new BinderHttpAsyncHandler();
}
return new BinderHttpHandler();
}
}
与浏览器连接限制共存
所以,我们有了一个服务器端的异步机制,但是客户端呢?正如你们中一些人可能注意到的,我们只有一个 AJAX 请求,用于页面的所有属性更改。如果我们为每个属性执行一个 AJAX 请求,由于浏览器会一直保持一个打开的套接字直到请求完成,我们将很容易超出浏览器打开连接的限制。因此,对于单个页面,问题解决了。但是,如果我们打开许多标签页(带有保持连接的页面),我们会发现有些标签页无法加载(无法创建连接来处理请求)。为了解决这个问题,即使在超时之后,我们也会返回响应(并释放浏览器连接)。
internal class ChangeRequestAsyncResult : IAsyncResult
{
// ...
private System.Timers.Timer _waitIntervalTimer;
protected void Process()
{
// ...
_bindingsHandlers = BinderContext.Instance.GetBindingsHandlers(pageId);
if (_bindingsHandlers.Any(b => b.HasUpdatedValue))
{
Complete();
}
else
{
uint waitInterval = BinderContext.Instance.ChangeRequestWaitInterval;
if (waitInterval > 0)
{
_waitIntervalTimer = new System.Timers.Timer();
_waitIntervalTimer.AutoReset = false;
_waitIntervalTimer.Interval = waitInterval;
_waitIntervalTimer.Elapsed += OnWaitIntervalTimerElapsed;
_waitIntervalTimer.Start();
}
_bindingsHandlers.ForEach(b => b.PropertyValueChanged += OnPropertyValueChanged);
}
}
void OnWaitIntervalTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
BinderContext.Instance.Invoke(() =>
{
if (_waitIntervalTimer != null)
{
AsyncComplete();
}
});
}
private void AsyncComplete()
{
if (_waitIntervalTimer != null)
{
_waitIntervalTimer.Elapsed -= OnWaitIntervalTimerElapsed;
_waitIntervalTimer.Close();
_waitIntervalTimer = null;
}
_bindingsHandlers.ForEach(b => b.PropertyValueChanged -= OnPropertyValueChanged);
Complete();
}
// ...
}
通知客户端集合更改
发送集合更改通知
为了使服务器与客户端集合的状态同步,除了发送关于集合元素属性更改的通知外,我们还必须发送关于添加和删除元素的通知。乍一看,似乎只发送关于特定更改的通知就足够了(已删除元素的索引,已添加元素的索引,以及新元素的属性值)。如果只有一个客户端,这种方法可以很好地工作。但是,如果存在多个客户端,我们可能会得到意想不到的结果。
假设我们有一个包含 3 个元素的集合,以及 2 个客户端正在查看包含该集合的页面。现在,每个客户端同时删除集合的第二个元素。每个客户端都希望看到一个包含 2 个元素(第一个和第三个)的集合。但是,让我们看看实际发生了什么。当客户端删除元素时,它们会发送带有元素索引的通知。服务器收到第一个通知,并删除集合的第二个元素。然后,服务器收到第二个通知,并再次删除集合的第二个元素(它最初是第三个元素)。然后,服务器使用新集合(只包含一个元素...)更新客户端。结果,两个客户端都只显示一个元素。
为了防止此类事情发生,我们不只是发送关于特定更改的通知,而是发送新集合的完整快照。
/*--- Holds the properties' paths of the arrays that have been changed, for each binding id. ---*/
var _pendingChangedArraysPathes_ = {};
function _sendArraysChangeNotification_() {
var arraysNotification = "[";
var hasNotifications = false;
var isFirstBindingId = true;
for (var _bindingId_ in _pendingChangedArraysPathes_) {
var pendingArrays = _pendingChangedArraysPathes_[_bindingId_];
if (pendingArrays && pendingArrays.length > 0) {
if (isFirstBindingId) {
isFirstBindingId = false;
} else {
arraysNotification += ",";
}
hasNotifications = true;
arraysNotification += '{"BindingId":"' + _bindingId_ + '","CollectionsNotifications":[';
var isFirstArrayPath = true;
while (pendingArrays.length > 0) {
var currArrayPath = pendingArrays.shift();
var arrHolder = _getPropertyObject_(_bindingId_, currArrayPath);
var arrVal = arrHolder ? self.getArrayValue(arrHolder) : null;
if (arrVal) {
if (isFirstArrayPath) {
isFirstArrayPath = false;
} else {
arraysNotification += ",";
}
var currArrCount = arrVal.length;
arraysNotification += '{"PropertyPath":"' + currArrayPath + '","NewCount":' + currArrCount +
',"PropertiesNotifications":[';
var isFirstSubProp = true;
for (var elemInx = 0; elemInx < currArrCount; elemInx++) {
var currElemPath = currArrayPath + "[" + elemInx + "]";
var subPropPathes = _getSubPropertiesPathes_(_bindingId_, currElemPath);
if (subPropPathes.length > 0) {
for (var subPropInx = 0; subPropInx < subPropPathes.length; subPropInx++) {
var currSubPropPath = currElemPath + '.' + subPropPathes[subPropInx];
if (_isArray_(_bindingId_, currSubPropPath)) {
/*--- This sub-property is an array -
Push it for array change notification. ---*/
_addPendingChangedArrayPath_(_bindingId_, currSubPropPath);
} else {
if (isFirstSubProp) {
isFirstSubProp = false;
} else {
arraysNotification += ",";
}
arraysNotification += _createPropertyPathNotification_(_bindingId_, currSubPropPath);
}
}
} else {
/*--- There are no sub property-pathes... ---*/
if (_isArray_(_bindingId_, currElemPath)) {
/*--- This sub-property is an array -
Push it for array change notification. ---*/
_addPendingChangedArrayPath_(_bindingId_, currElemPath);
} else {
if (isFirstSubProp) {
isFirstSubProp = false;
} else {
arraysNotification += ",";
}
arraysNotification += _createPropertyPathNotification_(_bindingId_, currElemPath);
}
}
}
arraysNotification += "]}";
}
}
arraysNotification += ']}';
}
}
arraysNotification += "]";
if (hasNotifications) {
var postData = "requestId=arraysChangedNotification" +
"&pageId=" + self.pageId + "&arraysNotification=" + arraysNotification;
_sendAjaxPost_(postData, function (xmlhttp) { });
}
}
function _getSubPropertiesPathes_(_bindingId_, ownerPropPath) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var _propId_ = _removeArrayIndicesNumbers_(ownerPropPath);
var propPathes = [];
var objPropPathes = _getObjectPropertiesNames_(_rootObjId_, _propId_);
if (objPropPathes) {
for (var objPropPathInx = 0; objPropPathInx < objPropPathes.length; objPropPathInx++) {
propPathes.push(objPropPathes[objPropPathInx]);
}
}
var propInx = 0;
while (propInx < propPathes.length) {
var currPropPath = propPathes[propInx];
var subPropPathes = _getObjectPropertiesNames_(_rootObjId_, _propId_ + '.' + currPropPath);
if (subPropPathes && subPropPathes.length > 0) {
/*--- This property has sub-properties -
Replace it with its sub-properties' paths. ---*/
propPathes.splice(propInx, 1);
for (var subPropInx = 0; subPropInx < subPropPathes.length; subPropInx++) {
propPathes.push(currPropPath + '.' + subPropPathes[subPropInx]);
}
} else {
/*--- This property is a simple type -
Go to the next property. ---*/
propInx++;
}
}
return propPathes;
}
function _isArray_(_bindingId_, _propPath_) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var _propId_ = _removeArrayIndicesNumbers_(_propPath_) + '[]';
var objPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_);
return objPropNames ? true : false;
}
在 _getSubPropertiesPathes_
函数中,我们获取集合元素的属性路径。
在 _sendArraysChangeNotification_
函数中,我们构建一个通知消息,其中包含已更改集合的快照,并发送一个带有该消息的 AJAX 请求。
应用新集合的元素计数
为了更新集合的计数,我们可以向该集合添加或删除元素。这可以按如下方式完成:
public uint CollectionElementsCount { get; private set; }
public void SetCurrentCollectionElementsCount(uint newCount)
{
IList currCollection = GetCurrentValue() as IList;
if (null == currCollection)
{
return;
}
uint currCount = CollectionElementsCount;
// Remove old elements if needed.
if (currCount > newCount)
{
uint countDiff = currCount - newCount;
for (uint removedInx = 0; removedInx < countDiff; removedInx++)
{
currCollection.RemoveAt((int)newCount);
}
}
// Add new elements if needed.
if (newCount > currCount)
{
Type elementType = GetElementType(currCollection.GetType());
if (elementType != null)
{
uint countDiff = newCount - currCount;
for (uint addedInx = 0; addedInx < countDiff; addedInx++)
{
object addedItem = CreateObject(elementType);
currCollection.Add(addedItem);
}
}
}
}
private object CreateObject(Type t)
{
object res = null;
try
{
res = Activator.CreateInstance(t);
}
catch (Exception ex)
{
// There was a problem to construct the object using a default constructor.
// So, try the other constructors.
ConstructorInfo[] typeConstructors = t.GetConstructors();
foreach (ConstructorInfo ctor in typeConstructors)
{
object[] ctorParametersValues = new object[ctor.GetParameters().Count()];
try
{
res = Activator.CreateInstance(t, ctorParametersValues);
}
catch (Exception ex2)
{
}
if (res != null)
break;
}
}
return res;
}
除了添加或删除元素之外,我们还必须添加或删除适当的 PropertyBindingHandler
对象,以处理相应集合元素的绑定。这可以按如下方式完成:
public void SetCurrentCollectionElementsCount(uint newCount)
{
// ...
ApplyNewCollectionCount(newCount);
}
private void ApplyNewCollectionCount(uint newCount)
{
if (CollectionElementsCount != newCount)
{
UpdateCollectionElementsBindings(CollectionElementsCount, newCount);
CollectionElementsCount = newCount;
ChangeCounter++;
}
}
private void UpdateCollectionElementsBindings(uint oldCount, uint newCount)
{
if (oldCount < newCount)
{
for (uint addedInx = oldCount; addedInx < newCount; addedInx++)
{
AddCollectionElementBindings(addedInx);
}
}
else if (oldCount > newCount)
{
for (uint removedInx = newCount; removedInx < oldCount; removedInx++)
{
RemoveCollectionElementBindings(removedInx);
}
}
}
private void AddCollectionElementBindings(uint elementInx)
{
string elemClientPropertyPath = string.Format("{0}[{1}]", ClientPropertyPath, elementInx);
string elemServerPropertyPath = string.Format("{0}[{1}]", ServerPropertyPath, elementInx);
if (_mapping.HasCollectionElementMapping)
{
foreach (BindingMapping elemPropMapping in _mapping.CollectionElementMapping)
{
_rootBindingsHandler.AddPropertiesHandlers(_rootObject, elemPropMapping,
elemServerPropertyPath, elemClientPropertyPath);
}
}
else
{
_rootBindingsHandler.AddPropertiesHandlers(_rootObject,
BindingMapping.FromPropertyMapping(string.Empty, string.Empty, _mapping.RootMapping.MappingMode),
elemServerPropertyPath, elemClientPropertyPath);
}
}
private void RemoveCollectionElementBindings(uint elementInx)
{
string elemClientPropertyPath = string.Format("{0}[{1}]", ClientPropertyPath, elementInx);
_rootBindingsHandler.RemovePropertyBindingHandlersTree(elemClientPropertyPath);
}
处理集合更改请求
为了获取请求内容,我们创建了一个等效的数据结构(与 arraysNotification
请求字段的 JSON string
相同的结构)。
public class CollectionsValuesNotificationData
{
public string BindingId { get; set; }
public List<CollectionValuesData> CollectionsNotifications { get; set; }
}
public class CollectionValuesData
{
public string PropertyPath { get; set; }
public uint NewCount { get; set; }
public List<PropertyValueData> PropertiesNotifications { get; set; }
}
使用该数据结构,我们可以以与我们处理属性更改请求相同的方式处理集合通知请求,以应用新的集合值。
internal class BinderHttpHandler : IHttpHandler
{
// ...
public void ProcessRequest(HttpContext context)
{
string requestId = context.Request.Params["requestId"];
switch (requestId)
{
// ...
case "arraysChangedNotification":
{
HandleClientArraysChangedNotificationRequest(context);
break;
}
}
}
// ...
protected bool HandleClientArraysChangedNotificationRequest(HttpContext context)
{
string pageId = context.Request.Params["pageId"];
string arraysNotificationStr = context.Request.Params["arraysNotification"];
JavaScriptSerializer jss = new JavaScriptSerializer();
CollectionsValuesNotificationData[] arraysNotification =
jss.Deserialize<CollectionsValuesNotificationData[]>(arraysNotificationStr);
BinderContext.Instance.PostCollectionsUpdate(pageId, arraysNotification);
return true;
}
}
public class BinderContext : IDisposable
{
// ...
public void UpdateCollections(string pageId, CollectionsValuesNotificationData[] collectionsNotification)
{
foreach (CollectionsValuesNotificationData cnd in collectionsNotification)
{
BindingsHandler bh = GetBindingsHandler(pageId, cnd.BindingId);
if (bh != null)
{
foreach (var cn in cnd.CollectionsNotifications)
{
bh.SetCurrentCollectionElementsCount(cn.PropertyPath, cn.NewCount);
foreach (var pn in cn.PropertiesNotifications)
{
bh.SetCurrentValue(pn.PropertyPath, pn.NewValue);
}
}
}
}
}
public void PostCollectionsUpdate(string pageId, CollectionsValuesNotificationData[] collectionsNotification)
{
BeginInvoke(() => UpdateCollections(pageId, collectionsNotification));
}
// ...
}
通知服务器集合更改
为了创建关于已更改集合的通知,我们必须被告知集合的更改。如果完整集合已被替换(设置了另一个引用),我们可以以与获取其他属性通知相同的方式获取通知(通过在源对象中实现 INotifyPropertyChanged
接口)。但是,如果集合本身已更改(元素已添加或删除),我们需要另一种方式来获取通知。为此,我们有 INotifyCollectionChanged
接口。在属性更改的情况下,我们通过绑定自动获取通知(WPF 绑定注册到 INotifyPropertyChanged.PropertyChanged
事件)。在集合更改的情况下,我们必须自己实现它。
public class PropertyBindingHandler : DependencyObject, IDisposable
{
// ...
private static void OnCurrentValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
// ...
if (pbh.IsCollection)
{
INotifyCollectionChanged oldCollection = e.OldValue as INotifyCollectionChanged;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= pbh.OnCollectionChanged;
}
INotifyCollectionChanged newCollection = e.NewValue as INotifyCollectionChanged;
if (newCollection != null)
{
newCollection.CollectionChanged += pbh.OnCollectionChanged;
}
uint elementsCount = pbh.GetEnumerableCount(e.NewValue as IEnumerable);
pbh.ApplyNewCollectionCount(elementsCount);
}
// ...
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
uint newCollectionCount = GetEnumerableCount(sender as IEnumerable);
uint oldCollectionCount = CollectionElementsCount;
ApplyNewCollectionCount(newCollectionCount);
// Raise an event about the change.
EventHandler<ValueChangedEventArgs> handler = CurrentValueChanged;
if (null != handler)
{
ValueChangedEventArgs args = new ValueChangedEventArgs
{
OldValue = oldCollectionCount,
NewValue = newCollectionCount
};
// Run the event handler on the dispatcher's thread,
// in order to synchronize it with the change-request asynchronous result.
if (CheckAccess())
{
handler(this, args);
}
else
{
Dispatcher.BeginInvoke(new Action(() => handler(this, args)));
}
}
}
// ...
}
当 CurrentValue
属性的值更改时,我们注册 INotifyCollectionChanged.CollectionChanged
事件,并应用新集合的计数。
当集合更改时,我们应用新集合的计数,并引发关于属性更改的事件。
收到更新值响应时,我们设置集合的计数,而不是当前属性值(集合本身)。
public bool IsCollection { get { return _mapping.RootMapping.IsCollection; } }
public ServerPropertyValueRespose GetUpdatedValueResponse()
{
// If this is a collection, the NewValue is the new collection's elements count,
// otherwise, the NewValue is the current binded value.
return new ServerPropertyValueRespose
{
SubPropertyPath = ClientPropertyPath,
ChangeCounter = ChangeCounter,
NewValue = IsCollection ? CollectionElementsCount.ToString() : GetCurrentValueString(),
IsCollection = IsCollection
};
}
为了在客户端应用服务器更改,我们必须根据收到的通知更改客户端集合。这可以按如下方式完成:
function _applyServerChangesResponse_(response) {
// ...
for (var propertyResultInx = 0; propertyResultInx < currProperties.length; propertyResultInx++) {
var currPropertyResult = currProperties[propertyResultInx];
if (currPropertyResult.IsCollection) {
_applyServerCollectionChange_(currElement.BindingId,
currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
} else {
_applyServerPropertyChange_(currElement.BindingId,
currPropertyResult.SubPropertyPath, currPropertyResult.NewValue);
}
}
// ...
}
function _applyServerCollectionChange_(_bindingId_, _propPath_, newCount) {
var propObject = _getPropertyObject_(_bindingId_, _propPath_);
if (propObject) {
var arr = self.getArrayValue(propObject);
if (!arr) {
arr = [];
}
var orgCount = arr.length;
var lengthDiffernce = newCount - orgCount;
if (lengthDiffernce > 0) {
for (var elemInx = 0; elemInx < lengthDiffernce; elemInx++) {
arr.push(_createBoundObjectTree_(_bindingId_, _propPath_ + "[" + (orgCount + elemInx) + "]"));
}
self.setArrayValue(propObject, arr);
}
if (lengthDiffernce < 0) {
arr.splice(newCount, Math.abs(lengthDiffernce));
self.setArrayValue(propObject, arr);
}
}
}
function _createBoundObjectTree_(_bindingId_, _propPath_) {
/*--- Create the object ---*/
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
var objHolder = _createObject_(_rootObjId_, _propId_);
/*--- Subscribe for notifications ---*/
_registerForChanges_(_bindingId_, objHolder, _propPath_);
return objHolder;
}
在 _applyServerCollectionChange_
函数中,我们根据新集合的计数添加或删除元素。
在 _createBoundObjectTree_
函数中,我们为集合的元素创建一个新对象,并注册该对象的更改通知。
注册客户端更改通知
为了发送客户端对象更改通知,我们必须注册这些更改。这可以按如下方式完成:
this.applyBindings = function () {
/*--- Create the objects, if they aren't created yet and,
subscribe for notifications ---*/
/*--- Because some bindings can share a same root object,
some of the notifications can be sent also for unrelated bindings. -
Those notifications are ignored in the server. ---*/
for (var _bindingId_ in _bindingsObjectsIds_) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var objCreators = _getBindingObjectsCreators_(_rootObjId_);
if (objCreators) {
for (var _propId_ in objCreators) {
if (_propId_ != "" &&
_propId_.indexOf(".") < 0 &&
_propId_.indexOf("[") < 0) {
/*--- This is a root property.
(The property-id is equal to the property-name,
for a root property) ---*/
_bindRootPropertyTree_(_bindingId_, _propId_);
}
}
}
}
};
function _bindRootPropertyTree_(_bindingId_, propName) {
var rootObj = _getBindingObject_(_bindingId_);
if (rootObj) {
var objHolder = rootObj[propName];
if (!objHolder) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var _propId_ = _removeArrayIndicesNumbers_(propName);
objHolder = _createObject_(_rootObjId_, _propId_);
rootObj[propName] = objHolder;
}
/*--- Subscribe for notifications ---*/
_registerForChanges_(_bindingId_, objHolder, propName);
}
}
function _registerForChanges_(_bindingId_, objHolder, _propPath_) {
var _rootObjId_ = _getBindingObjectId_(_bindingId_);
var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
var arrayElemId = _propId_ + "[]";
var arrayElemPropNames = _getObjectPropertiesNames_(_rootObjId_, arrayElemId);
if (arrayElemPropNames) {
/*--- If there is an array for the elements properties' names,
this is an array. ---*/
/*--- Register to the array's changes ---*/
var arrayNotificationFunc = function () {
_addPendingChangedArrayPath_(_bindingId_, _propPath_);
setTimeout(function () {
_sendArraysChangeNotification_();
}, 0);
};
self.registerForArrayChanges(objHolder, arrayNotificationFunc);
var arrVal = self.getArrayValue(objHolder);
if (arrVal) {
for (var elemInx = 0; elemInx < arrVal.length; elemInx++) {
_registerForChanges_(_bindingId_, arrVal[elemInx], _propPath_ + '[' + elemInx + ']');
}
}
} else {
var objVal = self.getObjectValue(objHolder);
if (objVal) {
var subPropNames = _getObjectPropertiesNames_(_rootObjId_, _propId_);
if (subPropNames && subPropNames.length > 0) {
for (var subPropInx = 0; subPropInx < subPropNames.length; subPropInx++) {
var currSubPropPath = _propPath_;
var currSubPropName = subPropNames[subPropInx];
if (currSubPropPath != "" && currSubPropName != "") {
currSubPropPath += ".";
}
currSubPropPath += currSubPropName;
var propObjHolder = objVal[subPropNames[subPropInx]];
_registerForChanges_(_bindingId_, propObjHolder, currSubPropPath);
}
} else {
/*--- Register to the property's changes ---*/
var propNotificationFunc = function() {
_addPendingChangedPropertyPath_(_bindingId_, _propPath_);
setTimeout(function() {
_sendPropertiesChangeNotification_();
}, 0);
};
self.registerForPropertyChanges(objHolder, propNotificationFunc);
}
}
}
}
在 applyBindings
函数中,我们遍历所有根属性(绑定根对象的属性),并使用它们调用 _bindRootPropertyTree_
函数。
在 _bindRootPropertyTree_
函数中,我们获取绑定根对象的属性的对象持有者,并注册该对象的更改通知。
在 _registerForChanges_
函数中,我们注册给定属性路径及其子属性路径的更改通知。为了注册数组和简单属性的更改,我们创建一个函数(用于通知更改),并分别使用 registerForArrayChanges
和 registerForPropertyChanges
函数对其进行注册。由于我们有通用实现,这些函数可以针对不同的 JavaScript 库以不同方式实现。
当属性值被服务器更新更改时,我们不必将此值发送回服务器。由于我们排队等待稍后执行(使用 setTimeout
函数)通知的发送,因此我们可以取消不需要的通知。要取消属性的通知,我们可以从待处理更改数组中删除其属性路径。
function _applyServerPropertyChange_(_bindingId_, _propPath_, newValue) {
var propObject = _getPropertyObject_(_bindingId_, _propPath_);
if (propObject) {
// ...
/*--- The value of the property has been just updated from the server.
We don't need to send this value back... ---*/
_removePendingChangedPropertyPath_(_bindingId_, _propPath_);
}
}
function _applyServerCollectionChange_(_bindingId_, _propPath_, newCount) {
var propObject = _getPropertyObject_(_bindingId_, _propPath_);
if (propObject) {
// ...
/*--- The value of the array has been just updated from the server.
We don't need to send this value back... ---*/
_removePendingChangedArrayPath_(_bindingId_, _propPath_);
}
}
应用页面绑定
应用服务器绑定
由于一个页面可能存在多个绑定映射,我们需要一种方法来标识页面的每个绑定映射。为此,我们添加了一个类,该类保存页面的所有绑定映射,并为每个绑定映射注册一个不同的标识符。
public abstract class BinderDefinitions
{
private uint _bindingIdCounter;
public string PageId { get; set; }
#region Bindings
private List<BindingData> _bindings;
protected List<BindingData> Bindings
{
get { return _bindings ?? (_bindings = new List<BindingData>()); }
}
#endregion
protected void AddBinding(string clientObjectString,
object serverObject, BindingMapping objectBindingMapping)
{
_bindingIdCounter++;
string bindingId = string.Format("B{0}", _bindingIdCounter);
BindingData bd = new BindingData(bindingId, clientObjectString, serverObject, objectBindingMapping);
Bindings.Add(bd);
}
public void ApplyServerBindings()
{
foreach (BindingData bd in Bindings)
{
BinderContext.Instance.RegisterBinding(PageId, bd.BindingId, bd.ServerObject,
bd.ObjectBindingMapping, false);
}
}
}
public class BindingData
{
public BindingData(string bindingId, string clientObjectString,
object serverObject, BindingMapping objectBindingMapping)
{
BindingId = bindingId;
ClientObjectString = clientObjectString;
ServerObject = serverObject;
ObjectBindingMapping = objectBindingMapping;
}
#region Properties
public string BindingId { get; set; }
public string ClientObjectString { get; set; }
public object ServerObject { get; set; }
public BindingMapping ObjectBindingMapping { get; set; }
#endregion
}
注入客户端脚本
生成脚本
所以,我们有一个类来保存页面上所有的绑定映射。下一步是生成一个合适的客户端脚本。
我们必须提供的第一个脚本是客户端的对象模型实现。一种做法是提供一个包含该脚本的 JavaScript 文件。这样,我们的库的每个用户都可以添加一个带有对我们的脚本文件引用的 script
标签。但是,单独插入客户端脚本可能会导致一些问题:用户可能会忘记包含脚本文件,或者即使用户已经包含了脚本文件,脚本版本也可能与服务器端版本不兼容。
为了确保客户端脚本与服务器端代码兼容,我们采取了另一种方法,为每个页面生成客户端脚本。
为了生成客户端对象模型脚本,我们将脚本文件作为项目的资源添加,并使用它返回脚本的 string
。
protected static object ResourcesLocker { get; private set; }
private const string _originalBinderClientConstructorFunctionName = "_WebBindingBinderClient_";
public string BinderClientConstructorFunctionName { get; set; }
private string GetBinderClientScript()
{
string res = string.Empty;
Uri resUri = new Uri("/WebBinding;component/Scripts/BinderClient.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, _originalBinderClientConstructorFunctionName, BinderClientConstructorFunctionName);
return res;
}
由于客户端对象模型脚本是通用脚本,我们也必须注入专用实现。这可以通过为专用库实现一个专用类来完成(在我们的例子中:Knockout.js 库)。
public class KnockoutBinderDefinitions : BinderDefinitions
{
private const string _originalApplyDedicateImplementationFunctionName = "WebBinding_ApplyKnockoutDedicateImplementation";
public KnockoutBinderDefinitions()
{
ApplyDedicateImplementationFunctionName = "WebBinding_ApplyKnockoutDedicateImplementation";
}
#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.Knockout;component/Scripts/KnockoutDedicateImplementation.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);
return res;
}
}
在该类中,我们实现了一个虚拟方法 (GetApplyDedicateImplementationScript
),用于生成专用脚本。
拥有客户端对象模型脚本后,我们可以生成一个客户端脚本来应用页面的绑定映射。
public string BinderClientObjectName { get; set; }
public virtual string GetBinderScript()
{
StringBuilder clientScript = new StringBuilder();
// Add the client's object model script.
clientScript.Append(GetBinderClientScript());
clientScript.AppendLine();
// Create an instance of the client's object model.
clientScript.AppendFormat("var {0}=new {1}(\"{2}\");",
BinderClientObjectName, BinderClientConstructorFunctionName, PageId);
clientScript.AppendLine();
// Add dedicate implementation script.
clientScript.Append(GetApplyDedicateImplementationScript());
clientScript.AppendLine();
// Start receiving server changes notifications.
clientScript.AppendFormat("{0}.beginServerChangesRequests();", BinderClientObjectName);
clientScript.AppendLine();
// Construct the client model for each binding.
foreach (BindingData bd in Bindings)
{
clientScript.Append(GetBindingRegistrationScript(bd));
clientScript.AppendLine();
}
// Register for client changes notifications.
clientScript.AppendFormat("{0}.applyBindings();", BinderClientObjectName);
return clientScript.ToString();
}
protected abstract string GetApplyDedicateImplementationScript();
private string GetBindingRegistrationScript(BindingData bd)
{
StringBuilder sb = new StringBuilder();
string mappingObjectString = bd.ObjectBindingMapping.ToClientBindingMappingObjectString();
sb.AppendFormat("{0}.addBindingMapping(\"{1}\",{2},{3});",
BinderClientObjectName, bd.BindingId, bd.ClientObjectString, mappingObjectString);
return sb.ToString();
}
附加功能
除了基本功能之外,我们还添加了一个选项,如果绑定的根对象不存在,则自动创建它们。
public bool DefineRootObjectsIfNotExist { get; set; }
private string GetBindingRegistrationScript(BindingData bd)
{
// ...
if (DefineRootObjectsIfNotExist)
{
sb.Append("((function() {if (!this[\"").Append(bd.ClientObjectString).Append("\"]){this[\"").
Append(bd.ClientObjectString).Append("\"]={};}})());");
}
// ...
}
更推荐手动定义这些对象,但这对于简单情况可能是一个不错的解决方案。
有时,我们可能希望在绑定模型中创建新对象(例如添加集合元素)。由于我们处理的是绑定模型,我们可以在服务器端创建这些对象(使用 Web API 或类似工具),然后它们将反映到所有客户端。但是,如果仍然希望在客户端创建对象(并希望它们的更改反映到服务器),我们也必须注册它们的属性更改。为此,我们添加以下函数:
this.createBoundObjectForPropertyPath = function (rootObj, _propPath_) {
/*--- get the object id ---*/
var _rootObjId_ = null;
for (var objId in _rootBindingObjects_) {
if (_rootBindingObjects_[objId] == rootObj) {
_rootObjId_ = objId;
}
}
if (_rootObjId_) {
var _propId_ = _removeArrayIndicesNumbers_(_propPath_);
var objHolder = _createObject_(_rootObjId_, _propId_);
for (var _bindingId_ in _bindingsObjectsIds_) {
if (_bindingsObjectsIds_[_bindingId_] == _rootObjId_) {
_registerForChanges_(_bindingId_, objHolder, _propPath_);
}
}
return objHolder;
}
return null;
};
在该函数中,我们使用存储的创建者函数根据给定的属性路径创建一个对象,并为使用给定根对象的所有绑定注册属性更改。
我们可能希望在客户端使用的其他数据是当前页面标识符。我们可以生成用于创建绑定对象和获取页面标识符的函数,如下所示:
public string CreateBoundObjectFunctionName { get; set; }
public string GetPageIdFunctionName { get; set; }
private string GetAdditionalFunctionsScript()
{
StringBuilder sb = new StringBuilder();
if (!string.IsNullOrEmpty(CreateBoundObjectFunctionName))
{
sb.Append("function ").Append(CreateBoundObjectFunctionName).
Append("(o,p){return ").Append(BinderClientObjectName).
Append(".createBoundObjectForPropertyPath(o,p);}");
}
if (!string.IsNullOrEmpty(GetPageIdFunctionName))
{
sb.Append("function ").Append(GetPageIdFunctionName).
Append("(){return ").Append(BinderClientObjectName).Append(".pageId;}");
}
return sb.ToString();
}
最小化注入的 javascript 代码
为了最小化页面传输的数据量,标准 JavaScript 库(例如 jQuery、Knockout 等)也提供最小化的 JavaScript 文件。我们对我们的库也采取相同的方法。
private static readonly Dictionary<string, string> _defaultVariablesReplacementValues;
static BinderDefinitions()
{
// ...
_defaultVariablesReplacementValues =
new Dictionary<string, string>
{
{"_bindingsObjectsIds_", "a1"},
{"_rootBindingObjects_", "a2"},
{"_objectsCreators_", "a3"},
// ...
{"_applyServerChangesResponse_", "i7"},
{"_applyServerPropertyChange_", "i8"},
{"_applyServerCollectionChange_", "i9"}
};
}
protected BinderDefinitions()
{
// ...
VariablesReplacementValues = new Dictionary<string, string>(_defaultVariablesReplacementValues);
// ...
}
// Dictionary of <regex-pattern, replacement-string>, for replacements in the client script.
protected Dictionary<string, string> VariablesReplacementValues { get; private set; }
public bool MinimizeClientScript { get; set; }
private string GetBinderClientScript()
{
// ...
if (MinimizeClientScript)
{
res = GetMinimizedBinderClientScript(res);
}
// ...
}
private string GetMinimizedBinderClientScript(string originalScript)
{
// Remove comments.
string res = Regex.Replace(originalScript, "/\\*-{3}([\\r\\n]|.)*?-{3}\\*/", string.Empty);
// Replace variables names
foreach (var variableReplacement in VariablesReplacementValues)
{
res = Regex.Replace(res, variableReplacement.Key, variableReplacement.Value);
}
// Remove lines' spaces
res = Regex.Replace(res, "[\\r\\n][\\r\\n \\t]*", string.Empty);
// Remove additional spaces
res = Regex.Replace(res, " ?([=\\+\\{\\},\\(\\)!\\?:\\>\\<\\|&\\]\\[-]) ?", "$1");
return res;
}
在 GetMinimizedBinderClientScript
方法中,我们删除注释和不必要的空格,并将一些变量名更改为更短的名称。
在 VariablesReplacementValues
字典中,我们为每个应该替换的变量名保存替换字符串。因此,为了更改默认替换,我们可以更改该字典。
添加 HtmlHelper 扩展
有了客户端脚本后,我们可以将其注入到渲染的页面中。为此,我们向 HtmlHelper
类型添加了一个扩展方法。
public static class BinderHtmlHelperExtensions
{
private static uint _pageIdCounter = 1;
private static string GeneratePageId(HtmlHelper helper)
{
string idPrefix;
try
{
string sessionId = helper.ViewContext != null && helper.ViewContext.HttpContext != null && helper.ViewContext.HttpContext.Session != null
? helper.ViewContext.HttpContext.Session.SessionID
: "<null>";
string conrollerName = helper.ViewContext != null && helper.ViewContext.RouteData.Values["Controller"] != null
? helper.ViewContext.RouteData.Values["Controller"].ToString()
: string.Empty;
string actionName = helper.ViewContext != null && helper.ViewContext.RouteData.Values["Action"] != null
? helper.ViewContext.RouteData.Values["Action"].ToString()
: string.Empty;
idPrefix = string.Format("{0}/{1}@{2}", conrollerName, actionName, sessionId);
}
catch (Exception)
{
idPrefix = Guid.NewGuid().ToString();
}
string id = string.Format("{0}#{1}", idPrefix, _pageIdCounter);
_pageIdCounter++;
return id;
}
public static IHtmlString WebBinder(this HtmlHelper helper, BinderDefinitions bd)
{
bd.PageId = GeneratePageId(helper);
bd.ApplyServerBindings();
string binderScript = bd.GetBinderScript();
return new HtmlString(string.Format("<script type=\"text/javascript\">{0}</script>", binderScript));
}
}
在 GeneratePageId
方法中,我们根据 ViewContext
和计数数字为页面生成一个唯一的标识符。
在 WebBinder
方法中,我们注册服务器绑定并生成客户端脚本。此方法返回一个 HtmlString
对象,其中包含用 script
标签包装的客户端脚本。
使用属性生成绑定映射
为了简化绑定映射的创建,我们添加了一个属性来标记绑定属性。
[AttributeUsage(AttributeTargets.Property)]
public class WebBoundAttribute : Attribute
{
public WebBoundAttribute()
{
IndicateCollectionTypeAutomatically = true;
MappingMode = BindingMappingMode.TwoWay;
IndicateMappingModeAutomatically = true;
RecursionLevel = 5;
}
public string ClientPropertyName { get; set; }
public bool IsCollection { get; set; }
public bool IndicateCollectionTypeAutomatically { get; set; }
public BindingMappingMode MappingMode { get; set; }
public bool IndicateMappingModeAutomatically { get; set; }
public uint RecursionLevel { get; set; }
}
使用该属性,我们可以为给定类型生成绑定映射。
public static BindingMapping FromType(Type serverObjectType, bool discardInvalidMappings = false)
{
if (null == serverObjectType)
{
throw new ArgumentNullException("serverObjectType");
}
BindingMapping bm = new BindingMapping();
Dictionary<PropertyInfo, uint> propertiesRecursionLevel = new Dictionary<PropertyInfo, uint>();
try
{
AddSubPropertiesMapping(serverObjectType, bm.SubPropertiesMapping, propertiesRecursionLevel, discardInvalidMappings);
}
catch (InvalidOperationException ioe)
{
string msg = string.Format("Binding cannot fully work for type '{0}'. See inner exception for more details.",
serverObjectType.FullName);
throw new InvalidOperationException(msg, ioe);
}
return bm;
}
private static bool IsPropertyWebBound(PropertyInfo pi, Dictionary<PropertyInfo, uint> propertiesRecursionLevel)
{
if (!propertiesRecursionLevel.ContainsKey(pi))
{
WebBoundAttribute wba =
pi.GetCustomAttributes(typeof(WebBoundAttribute), true).FirstOrDefault() as WebBoundAttribute;
propertiesRecursionLevel[pi] = wba != null ? wba.RecursionLevel : 0;
}
if (propertiesRecursionLevel[pi] > 0)
{
propertiesRecursionLevel[pi]--;
return true;
}
return false;
}
private static void AddSubPropertiesMapping(Type t, List<BindingMapping> dstMappingList,
Dictionary<PropertyInfo, uint> basePropertiesRecursionLevel, bool discardInvalidMappings)
{
if (t == null || dstMappingList == null || basePropertiesRecursionLevel == null)
{
return;
}
Dictionary<PropertyInfo, uint> propertiesRecursionLevel =
new Dictionary<PropertyInfo, uint>(basePropertiesRecursionLevel);
IEnumerable<PropertyInfo> webBindedProperties =
t.GetProperties().Where(p => IsPropertyWebBound(p, propertiesRecursionLevel));
foreach (PropertyInfo pi in webBindedProperties)
{
BindingMapping bm;
try
{
bm = new BindingMapping(PropertyMapping.FromProperty(pi));
}
catch (InvalidOperationException)
{
if (discardInvalidMappings)
{
continue;
}
throw;
}
if (bm.RootMapping.IsCollection)
{
Type elemType = GetElementType(pi.PropertyType);
AddCollectionElementMapping(elemType, bm.CollectionElementMapping, propertiesRecursionLevel, discardInvalidMappings);
}
else
{
AddSubPropertiesMapping(pi.PropertyType, bm.SubPropertiesMapping, propertiesRecursionLevel, discardInvalidMappings);
}
dstMappingList.Add(bm);
}
}
private static void AddCollectionElementMapping(Type elemType, List<BindingMapping> dstMappingList,
Dictionary<PropertyInfo, uint> propertiesRecursionLevel, bool discardInvalidMappings)
{
if (IsCollection(elemType))
{
BindingMapping bm = new BindingMapping();
bm.RootMapping.IsCollection = true;
Type subElemType = GetElementType(elemType);
AddCollectionElementMapping(subElemType, bm.CollectionElementMapping, propertiesRecursionLevel, discardInvalidMappings);
dstMappingList.Add(bm);
}
else
{
AddSubPropertiesMapping(elemType, dstMappingList, propertiesRecursionLevel, discardInvalidMappings);
}
}
private static bool IsCollection(Type t)
{
if (t == null)
{
return false;
}
if (t == typeof(string))
{
return false;
}
Type[] typeIntefaces = t.GetInterfaces();
if (typeIntefaces.Contains(typeof(IEnumerable)))
{
return true;
}
return false;
}
private static Type GetElementType(Type containerType)
{
Type res = null;
if (containerType.HasElementType)
{
res = containerType.GetElementType();
}
if (containerType.IsGenericType)
{
Type[] genericArguments = containerType.GetGenericArguments();
if (genericArguments.Length == 1)
{
res = genericArguments[0];
}
}
return res;
}
在 FromType
方法中,我们创建一个新的 BindingMapping
对象,并根据绑定的属性(用 WebBound
属性标记的属性)填充其 SubPropertiesMapping
。
在 AddSubPropertiesMapping
方法中,我们遍历所有绑定属性,并为它们创建适当的 BindingMapping
对象。如果属性是集合,我们根据集合元素类型的绑定属性填充 CollectionElementMapping
。否则,我们根据属性类型的绑定属性填充 SubPropertiesMapping
。
如果属性的类型拥有其自身类型的后代(嵌套属性),由于我们根据类型信息生成绑定映射,我们很容易陷入无限递归(通常会因 StackOverflowException
异常而中断)。为了防止这种情况,每个 WebBound
属性都有 RecursionLevel
属性。使用该属性,我们可以忽略递归级别过深的属性。这通过 IsPropertyWebBound
方法完成。
为了为每个属性创建属性映射,我们使用 PropertyMapping.FromProperty
方法。在此方法中,我们根据属性的 WebBound
属性创建 PropertyMapping
对象,并在存在无效映射时抛出相应的异常。
垃圾回收
为了在页面不再使用时收到通知,我们存储每个页面的最后操作时间。
// Dictionary of (page-id, last-action-time).
private readonly Dictionary<string, DateTime> _pagesLastActionTime;
public BindingsHandler GetBindingsHandler(string pageId, string bindingId)
{
// ...
_pagesLastActionTime[pageId] = DateTime.Now;
// ...
}
public List<BindingsHandler> GetBindingsHandlers(string pageId)
{
// ...
_pagesLastActionTime[pageId] = DateTime.Now;
// ...
}
利用存储的最后操作时间,我们可以确定哪些页面不再使用(已关闭)。为了清理不需要的页面数据,我们添加了一个计时器,用于运行“垃圾回收”。
private System.Timers.Timer _gcTimer;
private void StartGarbageCollectionTimer()
{
_gcTimer = new System.Timers.Timer();
_gcTimer.AutoReset = true;
_gcTimer.Interval = GarbageCollectionInterval;
_gcTimer.Elapsed += OnGcTimerElapsed;
_gcTimer.Start();
}
private void OnGcTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
CollectNotInUsePagesData();
}
private void StopGarbageCollectionTimer()
{
if (_gcTimer != null)
{
_gcTimer.Stop();
_gcTimer = null;
}
}
private void CollectNotInUsePagesData()
{
lock (Locker)
{
string[] unusedPagesIds = _pagesLastActionTime.
Where(p => (DateTime.Now - p.Value).TotalMilliseconds > MaxInactivationMilliseconds).
Select(p => p.Key).ToArray();
foreach (string pageId in unusedPagesIds)
{
_pagesLastActionTime.Remove(pageId);
if (_pagesBindings.ContainsKey(pageId))
{
Dictionary<string, BindingsHandler> currPagesData = _pagesBindings[pageId];
foreach (var pd in currPagesData)
{
BindingsHandler bh = pd.Value;
BeginInvoke(() => bh.Dispose());
}
_pagesBindings.Remove(pageId);
}
}
}
}
在 CollectNotInUsePagesData
方法中,我们遍历所有未使用的页面,并清理它们的数据。
为了在页面被移除时启用额外的清理,我们添加了一个事件来指示页面被移除。
public class PageEventArgs : EventArgs
{
public string PageId { get; set; }
}
public class BinderContext : IDisposable
{
// ...
public event EventHandler<PageEventArgs> PageRemoved;
private void CollectlNotInUsePagesData()
{
// ...
// Raise PageRemoved event.
EventHandler<PageEventArgs> handler = PageRemoved;
if (handler != null)
{
PageEventArgs arg = new PageEventArgs {PageId = pageId};
handler(this, arg);
}
// ...
}
// ...
}
如何使用它
示例 1:共享视图模型与唯一视图模型
为了演示 WebBinding
库的使用,我们创建了一个示例视图模型和一个 ASP.NET MVC4(可从 http://www.asp.net/mvc/mvc4 下载)网页,该网页展示了该视图模型。
在第一个示例中,我们创建了视图模型的两个实例,其中包含一个 web 绑定 Text
属性。其中一个实例与所有页面共享,另一个实例是该特定页面独有的。
对于我们的视图模型,我们创建了以下类:
public class BaseViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}
public class ExampleViewModel : BaseViewModel
{
#region Text
private string _text;
[WebBound(ClientPropertyName = "text")]
public string Text
{
get { return _text; }
set
{
if (_text != value)
{
_text = value;
NotifyPropertyChanged("Text");
}
}
}
#endregion
}
为了创建我们视图模型的共享实例,我们创建了一个持有我们视图模型的单例。
public class ExampleContext
{
#region Singleton implementation
private ExampleContext()
{
CommonBindPropertiesExampleViewModel = new ExampleViewModel();
}
private static readonly ExampleContext _instance = new ExampleContext();
public static ExampleContext Instance
{
get { return _instance; }
}
#endregion
#region Properties
public ExampleViewModel CommonBindPropertiesExampleViewModel { get; private set; }
#endregion
}
现在,让我们添加一个操作,以展示一个以我们视图模型的新实例作为其 Model
的视图。
public class HomeController : Controller
{
public ActionResult Index()
{
ExampleViewModel vm = new ExampleViewModel();
return View(vm);
}
}
为了在我们的视图上应用 WebBinding
(将服务器端的视图模型绑定到客户端的相应视图模型),我们
- 添加 JavaScript 对象以在客户端保存相应的视图模型。
<script type="text/javascript"> var uniqueVm = {}; var sharedVm = {}; </script>
- 创建一个
BinderDefinitions
对象,该对象包含两个视图模型的绑定映射。@{ BinderDefinitions bd = new KnockoutBinderDefinitions(); bd.AddBinding("uniqueVm", ViewData.Model); bd.AddBinding("sharedVm", ExampleContext.Instance.CommonBindPropertiesExampleViewModel); }
- 在页面上应用
WebBinding
@Html.WebBinder(bd)
由于我们使用 *Knockout.js* 库在 JavaScript 视图模型和 HTML 元素之间进行绑定,因此我们也必须应用 Knockout 绑定。
<script type="text/javascript" src="~/Scripts/knockout-3.0.0.js"></script>
<script type="text/javascript">
// ...
function ExampleViewModel() {
var self = this;
this.uniqueVm = uniqueVm;
this.sharedVm = sharedVm;
}
var viewModel = new ExampleViewModel();
</script>
<script type="text/javascript">
ko.applyBindings(viewModel);
</script>
为了展示我们的示例,我们添加了 2 个 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" data-bind="value: sharedVm.text"/> -
Entered value: <span style="color :blue" data-bind="text: sharedVm.text"></span>
</p>
<h4>Unique view-model</h4>
<p>
Text: <input type="text" data-bind="value: uniqueVm.text"/> -
Entered value: <span style="color :blue" data-bind="text: uniqueVm.text"></span>
</p>
</section>
结果是:
示例 2:二维集合
在第二个示例中,我们展示了一个 web 绑定的二维整数集合。集合的值由服务器随机更新(并反映到所有客户端)。
为了保存我们的集合,我们在视图模型中添加了一个适当的属性。
private ObservableCollection<ObservableCollection<int>> _numbersBoard;
[WebBound(ClientPropertyName = "numbersBoard")]
public ObservableCollection<ObservableCollection<int>> NumbersBoard
{
get { return _numbersBoard; }
set
{
if (_numbersBoard != value)
{
_numbersBoard = value;
NotifyPropertyChanged("NumbersBoard");
}
}
}
为了从客户端更改集合的维度,我们添加了另外两个属性。
#region NumbersBoardRowsCount
private int _numbersBoardRowsCount;
[WebBound(ClientPropertyName = "numbersBoardRowsCount")]
public int NumbersBoardRowsCount
{
get { return _numbersBoardRowsCount; }
set
{
if (_numbersBoardRowsCount != value)
{
lock (_numbersBoard)
{
while (_numbersBoard.Count > value)
{
_numbersBoard.RemoveAt(value);
}
while (_numbersBoard.Count < value)
{
ObservableCollection<int> newRow = new ObservableCollection<int>();
for (int colInx = 0; colInx < NumbersBoardColumnsCount; colInx++)
{
newRow.Add(colInx);
}
_numbersBoard.Add(newRow);
}
}
_numbersBoardRowsCount = value;
NotifyPropertyChanged("NumbersBoardRowsCount");
}
}
}
#endregion
#region NumbersBoardColumnsCount
private int _numbersBoardColumnsCount;
[WebBound(ClientPropertyName = "numbersBoardColumnsCount")]
public int NumbersBoardColumnsCount
{
get { return _numbersBoardColumnsCount; }
set
{
if (_numbersBoardColumnsCount != value)
{
if (_numbersBoardColumnsCount > value)
{
lock (_numbersBoard)
{
foreach (var row in NumbersBoard)
{
while (row.Count > value)
{
row.RemoveAt(value);
}
}
}
}
if (_numbersBoardColumnsCount < value)
{
lock (_numbersBoard)
{
foreach (var row in NumbersBoard)
{
while (row.Count < value)
{
row.Add(row.Count);
}
}
}
}
_numbersBoardColumnsCount = value;
NotifyPropertyChanged("NumbersBoardColumnsCount");
}
}
}
#endregion
当这些属性的值发生更改时,我们相应地更新集合的维度。
为了演示服务器的更改如何反映到客户端,我们添加了一个计时器,该计时器会在经过一定间隔后随机更改集合的值。
private System.Timers.Timer _nbTimer;
private void StartNumbersBoardTimer()
{
_nbTimer = new System.Timers.Timer();
_nbTimer.AutoReset = true;
_nbTimer.Interval = 1000;
_nbTimer.Elapsed += OnNbTimerElapsed;
_nbTimer.Start();
}
private void OnNbTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_numbersBoard)
{
Random r = new Random((int)DateTime.Now.Ticks);
for (int rowInx = 0; rowInx < _numbersBoard.Count; rowInx++)
{
ObservableCollection<int> currRow = _numbersBoard[rowInx];
for (int colInx = 0; colInx < currRow.Count; colInx++)
{
currRow[colInx] = r.Next(0, 100);
}
}
}
}
private void StopNumbersBoardTimer()
{
if (_nbTimer != null)
{
_nbTimer.Stop();
_nbTimer = null;
}
}
为了展示我们的示例,我们添加了 2 个 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" data-bind="value: sharedVm.numbersBoardRowsCount"/> -
Entered value: <span style="color :blue" data-bind="text: sharedVm.numbersBoardRowsCount"></span>
<br />
Columns count: <input type="text" data-bind="value: sharedVm.numbersBoardColumnsCount"/> -
Entered value: <span style="color :blue" data-bind="text: sharedVm.numbersBoardColumnsCount"></span>
<br />
</p>
<table style="background:lightgray;border:gray 1px solid;width:100%">
<tbody data-bind="foreach: sharedVm.numbersBoard">
<tr data-bind="foreach: $data">
<td style="background:lightyellow;border:goldenrod 1px solid">
<span style="color :blue" data-bind="text: $data"></span>
</td>
</tr>
</tbody>
</table>
</section>
结果是:
示例 3:字符串作为集合
在第三个示例中,我们以两种方式呈现一个 web 绑定的 string
:作为 string
和作为字符集合。
为了保存我们的 string
,我们添加了 2 个 web 绑定属性。
#region StringEntry
private string _stringEntry;
[WebBound]
public string StringEntry
{
get { return _stringEntry; }
set
{
if (_stringEntry != value)
{
_stringEntry = value;
NotifyPropertyChanged("StringEntry");
NotifyPropertyChanged("StringEntryCharacters");
}
}
}
#endregion
#region StringEntryCharacters
[WebBound(IndicateCollectionTypeAutomatically = false, IsCollection = true)]
public string StringEntryCharacters
{
get { return _stringEntry; }
}
#endregion
StringEntry
属性封装了 _stringEntry
字段的值。 StringEntryCharacters
属性返回相同的值,但作为集合公开(使用 WebBound
属性的相应属性)。
请注意,在其他示例中,我们使用 ClientPropertyName
属性设置客户端属性的名称。为了演示默认行为(为客户端属性指定与服务器端属性相同的名称),在此示例中,我们省略了 ClientPropertyName
属性的使用。
为了展示我们的示例,我们添加了一个 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" data-bind="value: sharedVm.StringEntry"/> -
Entered value: <span style="color :blue" data-bind="text: sharedVm.StringEntry"></span>
</p>
<h4>The string's characters</h4>
<table style="background:lightgray;border:gray 1px solid;width:100%">
<tbody>
<tr data-bind="foreach: sharedVm.StringEntryCharacters">
<td style="background:lightyellow;border:goldenrod 1px solid">
<span style="color :blue" data-bind="text: $data"></span>
</td>
</tr>
</tbody>
</table>
</section>
结果是:
示例 4:从客户端更改集合
在第四个示例中,我们展示了一个更复杂(比 string
、int
等简单类型更复杂)类型的 Web 绑定集合。我们可以从客户端添加或删除这些集合中的项(并查看更改如何反映到其他客户端)。
对于集合的元素类型,我们创建了一个表示人物的视图模型。
public class PersonViewModel : BaseViewModel
{
public PersonViewModel()
{
Children = new ObservableCollection<PersonViewModel>();
}
#region Name
private FullNameViewModel _name;
[WebBound(ClientPropertyName = "name")]
public FullNameViewModel Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
NotifyPropertyChanged("Name");
}
}
}
#endregion
#region Age
private int _age;
[WebBound(ClientPropertyName = "age")]
public int Age
{
get { return _age; }
set
{
if (value != _age)
{
_age = value;
NotifyPropertyChanged("Age");
}
}
}
#endregion
#region Children
private ObservableCollection<PersonViewModel> _children;
[WebBound(ClientPropertyName = "children")]
public ObservableCollection<PersonViewModel> Children
{
get { return _children; }
set
{
if (_children != value)
{
_children = value;
NotifyPropertyChanged("Children");
}
}
}
#endregion
}
public class FullNameViewModel : BaseViewModel
{
#region FirstName
private string _firstName;
[WebBound(ClientPropertyName = "firstName")]
public string FirstName
{
get { return _firstName; }
set
{
if (value != _firstName)
{
_firstName = value;
NotifyPropertyChanged("FirstName");
}
}
}
#endregion
#region LastName
private string _lastName;
[WebBound(ClientPropertyName = "lastName")]
public string LastName
{
get { return _lastName; }
set
{
if (value != _lastName)
{
_lastName = value;
NotifyPropertyChanged("LastName");
}
}
}
#endregion
}
为了保存我们的集合,我们在视图模型中添加了一个 web 绑定属性。
private ObservableCollection<PersonViewModel> _people;
[WebBound(ClientPropertyName = "people")]
public ObservableCollection<PersonViewModel> People
{
get { return _people; }
set
{
if (_people != value)
{
_people = value;
NotifyPropertyChanged("People");
}
}
}
为了在客户端创建新的集合元素,并使其更改反映到服务器,我们必须创建具有属性更改注册的对象。为此,我们公开了一个创建 web 绑定对象的函数。
@{
BinderDefinitions bd = new KnockoutBinderDefinitions();
bd.CreateBoundObjectFunctionName = "createWebBoundObject";
// ...
}
为了在客户端添加或删除集合元素,我们添加了执行这些操作的适当函数。
function ExampleViewModel() {
// ...
// Actions for example 4.
this.removePerson = function (person) {
var peopleArr = self.sharedVm.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);
}
self.sharedVm.people(peopleArr);
};
this.removeChild = function (child) {
var peopleArr = self.sharedVm.people();
var foundIndex = -1;
for (var personInx = 0; personInx < peopleArr.length && foundIndex < 0; personInx++) {
var childrenHolder = peopleArr[personInx]().children;
var childrenArr = childrenHolder();
for (var childInx = 0; childInx < childrenArr.length && foundIndex < 0; childInx++) {
if (childrenArr[childInx]() == child) {
foundIndex = childInx;
}
}
if (foundIndex >= 0) {
childrenArr.splice(foundIndex, 1);
childrenHolder(childrenArr);
}
}
};
this.addPerson = function () {
var peopleArr = self.sharedVm.people();
var newIndex = peopleArr.length;
var propPath = "people[" + newIndex + "]";
var person = createWebBoundObject(self.sharedVm, propPath);
person().name().firstName("Added_First" + (newIndex + 1));
person().name().lastName("Added_Last" + (newIndex + 1));
person().age(40 + newIndex);
peopleArr.push(person);
self.sharedVm.people(peopleArr);
};
this.addChild = function (person) {
// Find person's index.
var peopleArr = self.sharedVm.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 childrenHolder = peopleArr[foundIndex]().children;
var childrenArr = childrenHolder();
var newIndex = childrenArr.length;
var propPath = "people[" + foundIndex + "].children[" + newIndex + "]";
var child = createWebBoundObject(self.sharedVm, propPath);
child().name().firstName("Added_First" + (foundIndex + 1) + "_" + (newIndex + 1));
child().name().lastName("Added_Last" + (foundIndex + 1) + "_" + (newIndex + 1));
child().age(20 + newIndex);
childrenArr.push(child);
childrenHolder(childrenArr);
}
};
}
为了展示我们的示例,我们添加了一个列表用于展示我们的集合,以及用于应用适当操作的按钮。
<section>
<h3>Example 4: Change collections from the client side</h3>
<p class="exampleDescription">In this example, we add and remove collection's elements (from the client side).
We can see how the changes are reflected to the other pages.</p>
<h4>People collection</h4>
<ol data-bind="foreach: sharedVm.people">
<li>Name: <span style="color :blue" data-bind="text: name().firstName"></span>
<span style="color :brown">, </span>
<span style="color :blue" data-bind="text: name().lastName"></span>
Age: <span style="color :blue" data-bind="text: age"></span>
<button data-bind="click: $root.removePerson">Remove</button>
<br />
Children:
<ol data-bind="foreach: children">
<li>
Name: <span style="color :blue" data-bind="text: name().firstName"></span>
<span style="color :brown">, </span>
<span style="color :blue" data-bind="text: name().lastName"></span>
Age: <span style="color :blue" data-bind="text: age"></span>
<button data-bind="click: $root.removeChild">Remove</button>
</li>
</ol>
<button data-bind="click: $root.addChild">Add child</button>
</li>
</ol>
<button data-bind="click: $root.addPerson">Add person</button>
</section>
结果是: