MVVM(Knockout.js)的新尝试:多个Page,一个ViewModel
2013-01-07 21:14
399 查看
[code] function ViewModel(options){
var self = this;
//标题、数据集、弹出对话框和内容(HTML)
self.title= ko.observable(options.title);
self.recordSet= ko.observableArray();
self.dialogContent= ko.observable();
self.dialog = options.dialogId ? $("#" + options.dialogId) : $("#dialog");
//排序
//orderBy,defaultOrderBy & isAsc: 当前排序字段名,默认排序字段名和方向(升序/降序)
//totalPages, pageNumbers & pageIndex:总页数,页码列表和当前页
self.orderBy= ko.observable();
self.isAsc= ko.observable();
self.defaultOrderBy = options.defaultOrderBy;
//分页
//totalPages, pageNumbers & pageIndex:总页数,页码列表和当前页
self.totalPages = ko.observable();
self.pageNumbers= ko.observableArray();
self.pageIndex= ko.observable();
//查询条件:标签和输入值
self.searchCriteria = ko.observableArray(options.searchCriteria);
//作为显示数据的表格的头部:显示文字和对应的字段名(辅助排序)
self.headers = ko.observableArray(options.headers);
//CRUD均通过Ajax调用实现,这里提供用于获取Ajax请求地址的方法
self.dataQueryUrlAccessor = options.dataQueryUrlAccessor;
self.dataAddUrlAccessor = options.dataAddUrlAccessor;
self.dataUpdateAccessor = options.dataUpdateAccessor;
self.dataDeleteAccessor = options.dataDeleteAccessor;
//removeData:删除操作完成后将数据从recordSet中移除
//replaceData:修改操作后更新recordSet中相应记录
self.removeData = options.removeData;
self.replaceData= options.replaceData;
//Search按钮
self.search = function (){
self.orderBy(self.defaultOrderBy);
self.isAsc(true);
self.pageIndex(1);
$.ajax(
{
url: self.dataQueryUrlAccessor(self),
type: "GET",
success: function (result){
self.recordSet(result.Data);
self.totalPages(result.TotalPages);
self.resetPageNumbders();
}
});
};
//Reset按钮
self.reset = function (){
for (var i = 0; i < self.searchCriteria().length; i++){
self.searchCriteria()[i].value("");
}
};
//获取数据之后根据记录数重置页码
self.resetPageNumbders = function (){
self.pageNumbers.removeAll();
for (var i = 1; i <= self.totalPages(); i++){
self.pageNumbers.push(i);
}
};
//点击表格头部进行排序
self.sort = function (header){
if (self.orderBy() == header.value){
self.isAsc(!self.isAsc());
}
self.orderBy(header.value);
self.pageIndex(1);
$.ajax(
{
url: self.dataQueryUrlAccessor(self),
type: "GET",
success: function (result){
self.recordSet(result.Data);
}
});
};
//点击页码获取当前页数据
self.turnPage = function (pageIndex){
self.pageIndex(pageIndex);
$.ajax(
{
url: self.dataQueryUrlAccessor(self),
type: "GET",
success: function (result){
self.recordSet(result.Data);
}
});
};
//点击Add按钮弹出“添加数据”对话框
self.onDataAdding = function (){
$.ajax(
{
url: self.dataAddUrlAccessor(self),
type: "GET",
success: function (result){
self.dialogContent(result);
self.dialog.modal("show");
}
});
};
//点击“添加数据”对话框的Save按钮关闭对话框,并将添加的记录插入recordSet
self.onDataAdded = function (data){
self.dialog.modal("hide");
self.recordSet.unshift(data);
};
//点击Update按钮弹出“修改数据”对话框
self.onDataUpdating = function (data){
$.ajax(
{
url: self.dataUpdateAccessor(data, self),
type: "GET",
success: function (result){
self.dialogContent(result);
self.dialog.modal("show");
}
});
};
//点击“修改数据”对话框的Save按钮关闭对话框,并修改recordSet中的数据
self.onDataUpdated = function (data){
self.dialog.modal("hide");
self.replaceData(data, self);
};
//点击Delete按钮删除当前记录
self.onDataDeleting = function (data){
$.ajax(
{
url: self.dataDeleteAccessor(data,self),
type: "GET",
success: function (result){
self.removeData(result, self);
}
});
};
}
[/code]
[/code]
四、Controller的定义
目前我们公共的View已经定义好了,我们来看看在具体的页面中的绑定如何定义,以及ViewModel如何初始化。我们同样采用一个ASP.NET MVC应用作为例子,模式的场景就是上图中演示的“联系人管理”,如下所示的是表示联系人的Contact类型的定义:[code] [code] public class Contact
{
[Required]
public string Id{ get; set;}
[Required]
public string FirstName{ get; set;}
[Required]
public string LastName{ get; set;}
[Required]
[DataType(DataType.EmailAddress)]
public string EmailAddress{ get; set;}
[Required]
[DataType(DataType.PhoneNumber)]
public string PhoneNo{ get; set;}
}
[/code]
[/code]
如下所示的是Controller的定义,联系人管理页面通过默认的Action方法Index呈现出来,在View中实现CRUD操作的Ajax请求的目标Action方法也定义其中。用于获取数据的GetContacts方法不仅仅在用户点击“Search”按钮时被调用,实际上用户点击页码获取当前页数据,以及点击表格标头针对某个字段进行排序的时候调用的也是这个方法。该方法返回一个JSON对象,其Data属性返回具体的数据(针对指定的页码),而用于客户端重置页码的TotalPages属性表示总页数,在这里每页记录数设置为2。
[code] [code] public class HomeController : Controller
{
public const int PageSize = 2;
private static List<Contact> contacts = new List<Contact>
{
new Contact{Id = "001", FirstName = "San", LastName = "Zhang", EmailAddress = "zhangsan@gmail.com", PhoneNo="123"},
new Contact{Id = "002", FirstName = "Si", LastName = "Li", EmailAddress = "zhangsan@gmail.com", PhoneNo="456"},
new Contact{Id = "003", FirstName = "Wu", LastName = "Wang", EmailAddress = "wangwu@gmail.com", PhoneNo="789"}
};
public ActionResult Index()
{
return View();
}
public ActionResult GetContacts(string firstName, string lastName, string orderBy, int pageIndex=1, bool isAsc = true)
{
IEnumerable<Contact> result = from contactin contacts
where (string.IsNullOrEmpty(firstName) || contact.FirstName.ToLower().Contains(firstName.ToLower()))
&& (string.IsNullOrEmpty(lastName) || contact.LastName.ToLower().Contains(lastName.ToLower()))
select contact;
int count = result.Count();
int totalPages = count / PageSize + (count % PageSize > 0 ? 1 : 0);
result = result.Sort(orderBy, isAsc).Skip((pageIndex - 1) * PageSize).Take(PageSize);
return Json(new{ Data = result.ToArray(), TotalPages = totalPages}, JsonRequestBehavior.AllowGet);
}
public ActionResult Add()
{
ViewBag.Action = "Add";
ViewBag.OnSuccess = "viewModel.onDataAdded";
return View("ContactPartial", new Contact{ Id = Guid.NewGuid().ToString()});
}
[HttpPost]
public ActionResult Add(Contact contact)
{
contacts.Add(contact);
return Json(contact);
}
public ActionResult Update(string id)
{
ViewBag.Action = "Update";
ViewBag.OnSuccess = "viewModel.onDataUpdated";
return View("ContactPartial", contacts.First(c=>c.Id == id));
}
[HttpPost]
public ActionResult Update(Contact contact)
{
Contact existing = contacts.First(c=>c.Id == contact.Id);
existing.FirstName = contact.FirstName;
existing.LastName = contact.LastName;
existing.PhoneNo = contact.PhoneNo;
existing.EmailAddress = contact.EmailAddress;
return Json(contact);
}
public ActionResult Delete(string id)
{
Contact existing = contacts.First(c=>c.Id == id);
contacts.Remove(existing);
return Json(existing,JsonRequestBehavior.AllowGet);
}
}
[/code]
[/code]
针对HTTP-GET请求的Add和Update方法返回的是一个ViewResult,换句话说客户端通过Ajax请求最终得到的结果是相应的HTML。客户端最终将HTML作为对话框的内容显示出来,就是我们看到的“联系人编辑”对话框。两个方法呈现的都是一个名为ContactPartial的分部View,从如下定义可以看出这是一个Model类型为Contact的强类型View,Contact对象以编辑模式呈现在一个以Ajax方式提交的表单中。由于数据添加和数据更新操作针对不同的目标Action,而且提交之后回调的JavaScript函数也不一样,两者以ViewBag的形式(ViewBag.Action和ViewBag.OnSuccess)来动态设置。
[code] [code] @model Contact
@{
Layout = null;
}
@using (Ajax.BeginForm((string)ViewBag.Action , "Home", null,
new AjaxOptions{ HttpMethod = "Post", OnSuccess = (string)ViewBag.OnSuccess},
new{ @class = "form-horizontal"}))
{
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3>Detail</h3>
</div>
<div class="modal-body">
<div class="control-group">
@Html.HiddenFor(model=>model.Id)
@Html.LabelFor(model=>model.FirstName,new{@class="control-label"})
<div class="controls">
@Html.EditorFor(model => model.FirstName)
@Html.ValidationMessageFor(model => model.FirstName)
</div>
</div>
<div class="control-group">
@Html.LabelFor(model=>model.LastName,new{@class="control-label"})
<div class="controls">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
</div>
<div class="control-group">
@Html.LabelFor(model=>model.EmailAddress,new{@class="control-label"})
<div class="controls">
@Html.EditorFor(model => model.EmailAddress)
@Html.ValidationMessageFor(model => model.EmailAddress)
</div>
</div>
<div class="control-group">
@Html.LabelFor(model=>model.PhoneNo,new{@class="control-label"})
<div class="controls">
@Html.EditorFor(model => model.PhoneNo)
@Html.ValidationMessageFor(model => model.PhoneNo)
</div>
</div>
</div>
<div class="modal-footer">
<a href="#" class="btn" data-dismiss="modal">Close</a>
<input type="submit" class="btn btn-primary" value="Save" />
</div>
}
[/code]
[/code]
五、View的定义
我们最终来看看作为“联系人管理”页面的Index.cshtml的定义,由于大部分内容都可以与ViewModel的成员进行绑定,所以我们可以将它们通通定义在Layout之中,所以Index.cshtml的定义是非常少的。如下面的代码片断所示,HTML部分只包含针对Contact对象4个属性的绑定而已,因为ViewModel不包括具体数据类型相关的属性定义。对于JS部分,我们指定相应的options创建了一个具体的ViewModel对象并调用ko的applyBindings方法应用到当前页中。options指定的内容包括具体的title、searchCriteria、headers、defaultOrderBy和四个用于获取CRUD操作地址的函数。[code] [code] <td data-bind="text: FirstName"></td>
<td data-bind="text: LastName"></td>
<td data-bind="text: EmailAddress"></td>
<td data-bind="text: PhoneNo"></td>
@section Script
{
<script type="text/javascript">
var options ={
title: "Maintain Contacts",
searchCriteria: [
{ displayText: "First Name", value: ko.observable()},
{ displayText: "Last Name", value: ko.observable()}
],
headers: [
{ displayText: "First Name", value: "FirstName", width: "auto"},
{ displayText: "Last Name", value: "LastName", width: "auto"},
{ displayText: "Email Address", value: "EmailAddress", width: "auto"},
{ displayText: "Phone No.", value: "PhoneNo", width: "auto"},
{ displayText: "", value: "", width: "auto"}
],
defaultOrderBy: "FirstName",
dataQueryUrlAccessor: function (viewModel){
return appendQueryString('@Url.Action("GetContacts")',{
firstName : viewModel.searchCriteria()[0].value(),
lastName: viewModel.searchCriteria()[1].value(),
pageIndex : viewModel.pageIndex(),
orderBy : viewModel.orderBy(),
isAsc : viewModel.isAsc()
});
},
dataAddUrlAccessor: function (){ return '@Url.Action("Add")';},
dataUpdateAccessor: function (data){ return appendQueryString('@Url.Action("Update")',{ id: data.Id});},
dataDeleteAccessor: function (data){ return appendQueryString('@Url.Action("Delete")',{ id: data.Id});},
replaceData: function (data, viewModel){
for (var i = 0; i < viewModel.recordSet().length; i++){
var existing = viewModel.recordSet()[i];
if (existing.Id == data.Id){
viewModel.recordSet.replace(existing, data);
break;
}
}
},
removeData: function (data, viewModel){
viewModel.recordSet.remove(function (c){
return c.Id == data.Id;
});
}
};
var viewModel = new ViewModel(options);
ko.applyBindings(viewModel);
</script>
}
[/code]
[/code]
六、_Layout.cshtml定义
所有能够共享的内容都被定义在如下所示的布局文件中,我们简单地分析一下每个部分具体和ViewModel的哪些成员绑定:作为查询条件的标签和文本框(简单起见,这里只考虑了这一种输入元素类型)与ViewModel的searchCriteria进行绑定,集合元素包含标签(displayText)和对应的值(value)。
Search、Reset和Add按钮的Click事件则和ViewModel的search、reset和onDataAdding方法进行绑定。
与表格头部链接绑定的是ViewModel的headers,headers集合的元素包含显示文字(displayText)、对应的排序字段名(value)和宽度(width)。
对于表格头部的每一列,我们还通过KO的visible绑定设置了表示当前排序列和排序方向的图标(<i class="icon-circle-arrow-up" >和<i class="icon-circle-arrow-down" >)。
表示获取数据的表格主体部分与ViewModel的recordSet绑定。
每个记录后的Update和Delete链接的Click事件与ViewModel的onDataUpdating和onDataDeleting方法绑定。
页码列表和ViewModel的pageNumbers绑定,当前页的CSS(.selected)利用ViewModel的pageIndex来设置。
表示弹出对话框<div>的内容和ViewModel的dialogContent绑定。
[code] [code] <!DOCTYPE html>
<html>
...
<body>
<div class="form-search">
<fieldset>
<legend data-bind="text: title"></legend>
<span class="pull-left">
<!--ko foreach: searchCriteria-->
<label class="control-label" data-bind="text: displayText"></label>
<input class="search-query input-medium" data-bind="value: value" />
<!--/ko-->
<a href="#" data-bind = "click: search" class="btn btn-primary">Search</a>
<a href="#" data-bind = "click: reset" class="btn">reset</a>
</span>
<span class="pull-right">
<a href="#" data-bind = "click: onDataAdding" class="btn btn-primary">Add</a>
</span>
</fieldset>
</div>
<table class="table table-striped table-bordered table-condensed">
<thead>
<tr data-bind="foreach: headers">
<th data-bind="style:{width: width}" >
<a href="#" data-bind="text: displayText, click: $root.sort"></a>
<i class="icon-circle-arrow-up" data-bind="visible: value == $root.orderBy() && $root.isAsc()"> </i>
<i class="icon-circle-arrow-down" data-bind="visible: value == $root.orderBy() && !$root.isAsc()" ></i>
</th>
</tr>
</thead>
<tbody data-bind="foreach: recordSet">
<tr>
@RenderBody()
<td>
<a href="#" data-bind="click: $root.onDataUpdating">Update</a>
<a href="#" data-bind="click: $root.onDataDeleting">Delete</a>
</td>
</tr>
</tbody>
</table>
<div class="pagination pagination-centered">
<ul data-bind="foreach: pageNumbers">
<li data-bind="css:{selected: $index() == $root.pageIndex() - 1}">
<a href="#" data-bind="text: $data, click: $root.turnPage" ></a>
</li>
</ul>
</div>
<div class="modal fade hide" id="dialog" data-backdrop ="static" data-bind="html:dialogContent"></div>
@RenderSection("Script")
</body>
</html>
[/code]
[/code]
相关文章推荐
- MVVM(Knockout.js)的新尝试:多个Page,一个ViewModel
- 一个View显示多个Model
- JS组件系列——BootstrapTable+KnockoutJS实现增删改查解决方案(三):两个Viewmodel搞定增删改查
- ViewModel在MVC3中的应用:一个view显示多个model
- js架构设计模式——MVVM模式下,ViewModel和View,Model有什么区别
- JS组件系列——BootstrapTable+KnockoutJS实现增删改查解决方案(三):两个Viewmodel搞定增删改查
- BootstrapTable+KnockoutJS相结合实现增删改查解决方案(三)两个Viewmodel搞定增删改查
- ViewModel在MVC3中的应用:一个view显示多个model
- JS组件系列——BootstrapTable+KnockoutJS实现增删改查解决方案(三):两个Viewmodel搞定增删改查
- KnockoutJS---一个极其优秀的MVVM模型的js框架
- Multiple-View ViewPager-如何实现Viewpager控件的一个页面展示多个page以及回弹效果
- ViewModel在MVC3中的应用:一个view显示多个model
- 迷你MVVM框架 avalonjs 学习教程2、模块化、ViewModel、作用域
- .net mvc4 一个 view 显示多个 model
- .net mvc4 一个 view 显示多个 model
- knockout里面一个页面需要绑定多个ViewModel如何处理
- Cpage.js,一款轻盈的前端MVVM框架
- 一个共通的viewModel搞定所有的编辑页面-经典ERP录入页面(easyui + knockoutjs + mvc4.0)
- Javascript MVC/MVVM 框架对比, AngularJS vs Backbone vs Knockout
- QTableview QSqlTableModel如何最恰当地只显示一个表中的某几个字段的数据