简介
这篇文章将向大家介绍一下如何在ASP.NET MVC 4.0下如何使用Knockout JS和如何使用一种可以帮助我们写出更高维护度的JS基本模式。这里我用到的实例,最适用于SAP 的开发(详细信息见:single-page application)。但是knockout JS的使用范围却不仅限于于此。你可以根据你的需求,把它使用到任何asp.net MVC 的程序开发中。
背景
如果我们从事基于asp.net MVC 架构的程序开发。那么,很明显,我们不得不分出相当一部分精力来处理JS(至少我敢说,我们可以在几乎所有的项目中都能看到jQuery)。根据我的个人经验,对一个传统的ASP.NET开发者来说,处理JS部分的工作(开发、调试等)是一件肯头疼的事情。诚然,在MVC应用中,不受服务端控制和也没有viewstate(视图状态),确实会让人惴惴不安。然而,在我刚刚开始从事MVC应用开发的时候,我仔细研究了一下JS和jQUery后,我发现js和jQuery是一种非常简单而且可靠的web开发框架。我最近开发的项目中大多数是基于SAP开发的程序。 尽管刚开始的时候,我尝试了很多种技术和方式(灰常纠结),但是我现在觉得使用该框架真心的舒服,而且非常喜爱这个方法。在这里,非常感谢Douglas Crockford,他写的那本《JavaScript Good Parts》,非常棒,对我更好的理解JavaScript有很大帮助。
代码
我将分基础和进阶两部分,一步一步的向大家介绍该技术。如果你不是ASP.NET MVC程序开发的新手,可以跳过基础部分,直接阅读进阶部分。
基础部分:
-
创建ASP.NET MVC 4应用程序:文件 -> 新建 -> 项目-> 模板 -> Visual C# -> Web -> ASP.NET MVC 4 Web Application -> 给项目起一个友好的名字,单击确定.
-
在选择模板菜单中选择Basic。选择Razor作为视图引擎,单击确定.
-
手动下载knockout mapping library并添加到项目中。或者运行NuGet命令Install-Package Knockout.Mapping 实现该类库的添加。
-
右键Controllers文件夹-> 添加 -> Controller。命名为PersonController,单击添加按钮.
-
右键项目->添加 -> 新建文件夹。重命名为:ViewModel.
-
右键ViewModel文件夹-> 添加 -> 类。把新添加的类命名为:PersonViewModel。并添加如下代码:
123456publicclass PersonViewModel
{
publicint Id {
get
;
set
; }
publicstring Name {
get
;
set
; }
public
DateTime DateOfBirth {
get
;
set
; }
}
-
回到PersonController,粘贴如下代码:
1234567891011121314151617public
ActionResult Index()
{
var viewModel =
new
PersonViewModel()
{
Id = 1,
Name =
"Naveen"
,
DateOfBirth =
new
DateTime(1990, 11, 21)
};
return
View(viewModel);
}
[HttpPost]
public
JsonResult SavePersonDetails(PersonViewModel viewModel)
{
// TODO: Save logic goes here.return Json(new { });
}
同时,确保在该文件中添加了对View-Model的引用。
-
把光标定位到index方法中。右键鼠标,单击Add View VS将弹出一个窗口。保留默认选项。单击添加按钮。这样会在Views文件夹下的Person 文件夹下创建Index.cshtml文件。
-
打开App_Start 文件夹下的RouteConfig .cs文件。在这里设置controller为Person。如果完成了以上操作,RoutConfig文件会自动更新为下列代码。
12345678910111213publicclass RouteConfig
{
publicstaticvoid RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute(
"{resource}.axd/{*pathInfo}"
);
routes.MapRoute(
name:
"Default"
,
url:
"{controller}/{action}/{id}"
,
defaults:
new
{ controller =
"Person"
, action =
"Index"
, id = UrlParameter.Optional }
);
}
}
-
右键Content文件夹-> 添加 -> 新建项 -> 选择 样式表,并命名为Person.css.
-
在Scripts文件加下添加Application文件夹。右键该文件夹添加新项目,选择javascript文件。命名为Person.js。
进阶
以上操作只是做好了准备工作。现在,让我们总结一下到做了哪些操作:
首先,我们创建了一个以Person为实体的项目。这意味着我们创建了PersonViewModel, PersonController,和一个为表现层而创建的Index.cshtml文件。
其次,我们也添加了一个样式文件: Person.css 和Person.js。(框架需要)
三、为了使Person作为根,我们修改了RouteConfig
四、我们在项目中添加了knockout mapping library
到此,准备工作全部完毕,但是在继续学习使用knockout JS之前,先向大家简单介绍一下什么是knockout。
Knockout:
它是是一个用来帮助我们保持视图模型和UI元素同步的javascript类库。然而,这不是knockout的唯一功能。若想对其深入了解,请单机http://knockoutjs.com/了解更多内容。由于这篇文章旨在向大家展示如何入门,因此本文将把重点放在基础的UI绑定上。关于knockout的数据绑定方面,我们只需向html页面元素添加data-bind属性即可。例如:如果ViewModel 对象在Person.ViewModel并且我们想绑定name属性到一个textbox,那么我们需要有下列标记符。
1
|
<
inputdata-bind
=
"value: Person.ViewModel.Name"
type
=
"text"
>
|
同样,我们需要为所有域添加数据绑定属性。现在,如果你改变了那个对象的值,那么它的值会反射到UI和vice-versa。这是knockout的基本功能。随着我们的进一步探索,我们将会学到更多关于knockout的功能。下面我们继续一起讨论学习knockout:
-
既然我们需要为每一个UI元素添加数据绑定属性,我们最好为自己建立一个html helper方法。为了实现上述操作,请在项目中添加一个文件夹,并重命名为Helper。在Helper文件夹下添加一个类文件HtmlExtensions.cs并将下面的代码复制到该类。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116public
static
class
HtmlExtensions
{
/// <summary>
/// To create an observable HTML Control.
/// </summary>
/// <typeparam name="TModel">The model object</typeparam>
/// <typeparam name="TProperty">The property name</typeparam>
/// <param name="htmlHelper">The <see cref="HtmlHelper<T>"/></param>
/// <param name="expression">The property expression</param>
/// <param name="controlType">The <see cref="ControlTypeConstants"/></param>
/// <param name="htmlAttributes">The html attributes</param>
/// <returns>Returns computed HTML string.</returns>
public
static
IHtmlString ObservableControlFor<TModel, TProperty>(
this
HtmlHelper<TModel> htmlHelper, Expression<Func<TModel,
TProperty>> expression,
string
controlType =
ControlTypeConstants.TextBox,
object
htmlAttributes =
null
)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
string
jsObjectName =
null
;
string
generalWidth =
null
;
// This will be useful, if the same extension has
// to share with multiple pages (i.e. each with different view models).
switch
(metaData.ContainerType.Name)
{
case
"PersonViewModel"
:
// Where Person is the Javascript object name (namespace in theory).
jsObjectName =
"Person.ViewModel."
;
generalWidth =
"width: 380px"
;
break
;
default
:
throw
new
Exception(
string
.Format(
"The container type {0} is not supported yet."
,
metaData.ContainerType.Name));
}
var propertyObject = jsObjectName + metaData.PropertyName;
TagBuilder controlBuilder =
null
;
// Various control type creation.
switch
(controlType)
{
case
ControlTypeConstants.TextBox:
controlBuilder =
new
TagBuilder(
"input"
);
controlBuilder.Attributes.Add(
"type"
,
"text"
);
controlBuilder.Attributes.Add(
"style"
, generalWidth);
break
;
case
ControlTypeConstants.Html5NumberInput:
controlBuilder =
new
TagBuilder(
"input"
);
controlBuilder.Attributes.Add(
"type"
,
"number"
);
controlBuilder.Attributes.Add(
"style"
, generalWidth);
break
;
case
ControlTypeConstants.Html5UrlInput:
controlBuilder =
new
TagBuilder(
"input"
);
controlBuilder.Attributes.Add(
"type"
,
"url"
);
controlBuilder.Attributes.Add(
"style"
, generalWidth);
break
;
case
ControlTypeConstants.TextArea:
controlBuilder =
new
TagBuilder(
"textarea"
);
controlBuilder.Attributes.Add(
"rows"
,
"5"
);
break
;
case
ControlTypeConstants.DropDownList:
controlBuilder =
new
TagBuilder(
"div"
);
controlBuilder.Attributes.Add(
"class"
,
"dropDownList"
);
break
;
case
ControlTypeConstants.JqueryUIDateInput:
controlBuilder =
new
TagBuilder(
"input"
);
controlBuilder.Attributes.Add(
"type"
,
"text"
);
controlBuilder.Attributes.Add(
"style"
, generalWidth);
controlBuilder.Attributes.Add(
"class"
,
"dateInput"
);
controlBuilder.Attributes.Add(
"data-bind"
,
"date: "
+ propertyObject);
// date is the customized knockout binding handler. Check PrepareKo method of Person.
break
;
default
:
throw
new
Exception(
string
.Format(
"The control type {0} is not supported yet."
, controlType));
}
controlBuilder.Attributes.Add(
"id"
, metaData.PropertyName);
controlBuilder.Attributes.Add(
"name"
, metaData.PropertyName);
// Check data-bind already exists, add if not.
if
(!controlBuilder.Attributes.ContainsKey(
"data-bind"
))
{
controlBuilder.Attributes.Add(
"data-bind"
,
"value: "
+ propertyObject);
}
// Merge provided custom html attributes. This overrides the previously defined attributes, if any.
if
(htmlAttributes !=
null
)
{
controlBuilder.MergeAttributes(
HtmlExtensions.AnonymousObjectToHtmlAttributes(htmlAttributes),
true
);
}
return
MvcHtmlString.Create(controlBuilder.ToString());
}
/// <summary>
/// To convert '_' into '-'.
/// </summary>
/// <param name="htmlAttributes">The html attributes.</param>
/// <returns>Returns converted <see cref="RouteValueDictionary"/>.</returns>
private
static
RouteValueDictionary AnonymousObjectToHtmlAttributes(
object
htmlAttributes)
{
RouteValueDictionary result =
new
RouteValueDictionary();
if
(htmlAttributes !=
null
)
{
foreach
(System.ComponentModel.PropertyDescriptor property
in
System.ComponentModel.TypeDescriptor.GetProperties(htmlAttributes))
{
result.Add(property.Name.Replace(
'_'
,
'-'
), property.GetValue(htmlAttributes));
}
}
return
result;
}
}
同样添加一个类文件ViewModelConstants.cs并复制下列代码。
1234567891011public
static
class
ControlTypeConstants
{
publicconststring TextBox =
"TextBox"
;
publicconststring TextArea =
"TextArea"
;
publicconststring CheckBox =
"CheckBox"
;
publicconststring DropDownList =
"DropDownList"
;
publicconststring Html5NumberInput =
"Html5NumberInput"
;
publicconststring Html5UrlInput =
"Html5UrlInput"
;
publicconststring Html5DateInput =
"Html5DateInput"
;
publicconststring JqueryUIDateInput =
"JqueryUIDateInput"
;
}
ObservableControlFor是一个实现页面元素和数据属性相关联的一个方法。通过默认机制,它创建了TextBox。但是我们可以自己添加在ControlTypeConstants中定义的各种类型。在添加的时候可以按自己的需求随意添加。在添加的时候只需在ControlTypeConstants中添加另一个constant.并扩展位于ObservableControlFor 中的switch语句。如果不太理解上述方法体内的代码,不用担心,随着下面的学习,慢慢就懂了。
-
打开Views/Person文件夹下的Index.cshtml文件,并粘贴如下代码:
1234567891011121314151617181920212223242526@model Mvc4withKnockoutJsWalkThrough.ViewModel.PersonViewModel
@using Mvc4withKnockoutJsWalkThrough.Helper
@section styles{
@Styles.Render("~/Content/themes/base/css")
<
linkhref
=
"~/Content/Person.css"
rel
=
"stylesheet"
/>
}
@section scripts{
@Scripts.Render("~/bundles/jqueryui")
<
scriptsrc
=
"~/Scripts/knockout-2.1.0.js"
></
script
><
scriptsrc
=
"~/Scripts/knockout.mapping-latest.js"
></
script
><
scriptsrc
=
"~/Scripts/Application/Person.js"
></
script
><
scripttype
=
"text/javascript"
>
Person.SaveUrl = '@Url.Action("SavePersonDetails", "Person")';
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
</
script
>
}
<
form
><
divclass
=
"mainWrapper"
><
table
><
tr
><
td
>Id :
</
td
><
td
>
@Html.ObservableControlFor(model => model.Id, ControlTypeConstants.Html5NumberInput)
</
td
></
tr
><
tr
><
td
>Name :
</
td
><
td
>
@Html.ObservableControlFor(model => model.Name)
</
td
></
tr
><
tr
><
td
>Date Of Birth :
</
td
><
td
>
@Html.ObservableControlFor(model => model.DateOfBirth,
ControlTypeConstants.JqueryUIDateInput)
</
td
></
tr
></
table
></
div
><
br
/><
inputid
=
"Save"
type
=
"submit"
value
=
"Save"
/></
form
>
部分人可能遇到如下错误:
1json does
not
exist
in
the current context
你可以按照**连接**中的步骤解决这个问题。或者你需要用你自己的命名空间替换掉Mvc4withKnockoutJsWalkThrough。正如你在代码中看到的,我们正在使用在第12步中创建的html helper。同时你能注意到在script 模块(section)编写的脚本
12Person.SaveUrl =
'@Url.Action("SavePersonDetails", "Person")'
;
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
这里的Person是一个js对象(我们即将在Person.js文件中创建)。理论上来说,我们可以称之为命名空间(至少在这里非常适用)。在razor引擎中创建的工程中存在不能像扩展JS文件中使用语法。因此我们指定razor估计Person 对象的属性值。下面我将详细解ko.mapping.fromJS(@Html.Raw(Json.Encode(Model))); 的作用。
-
你可能注意到我们在Index.cshtml 页中使用了样式。 因此,必须在相关的页中定义以上内容。在本实例中,是_Layout.cshtml。因此打开views文件夹下Shared folder文件夹中的_Layout.cshtml文件。在头文件结束前,添加下面的代码:
1@RenderSection(
"styles"
, required: false)
最后,图层展示页面会是这个样子。
12345678910<!DOCTYPEhtml><
html
><
head
><
metacharset
=
"utf-8"
/><
metaname
=
"viewport"
content
=
"width=device-width"
/><
title
>@ViewBag.Title</
title
>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@RenderSection("styles", required: false)
</
head
><
body
>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</
body
></
html
>
-
现在,开始编写等待已久的javascript代码。打开Person.js文件(文件在Scripts文件夹下Application文件夹中)并粘贴下面代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879var
Person = {
PrepareKo:
function
() {
ko.bindingHandlers.date = {
init:
function
(element, valueAccessor, allBindingsAccessor, viewModel) {
element.onchange =
function
() {
var
observable = valueAccessor();
observable(
new
Date(element.value));
}
},
update:
function
(element, valueAccessor, allBindingsAccessor, viewModel) {
var
observable = valueAccessor();
var
valueUnwrapped = ko.utils.unwrapObservable(observable);
if
((
typeof
valueUnwrapped ==
'string'
|| valueUnwrapped
instanceof
String) &&
valueUnwrapped.indexOf(
'/Date'
) === 0) {
var
parsedDate = Person.ParseJsonDate(valueUnwrapped);
element.value = parsedDate.getMonth() + 1 +
"/"
+
parsedDate.getDate() +
"/"
+ parsedDate.getFullYear();
observable(parsedDate);
}
}
};
},
ParseJsonDate:
function
(jsonDate) {
return
new
Date(parseInt(jsonDate.substr(6)));
},
BindUIwithViewModel:
function
(viewModel) {
ko.applyBindings(viewModel);
},
EvaluateJqueryUI:
function
() {
$(
'.dateInput'
).datepicker();
},
RegisterUIEventHandlers:
function
() {
$(
'#Save'
).click(
function
(e) {
// Check whether the form is valid. Note: Remove this check, if you are not using HTML5
if
(document.forms[0].checkValidity()) {
e.preventDefault();
$.ajax({
type:
"POST"
,
url: Person.SaveUrl,
data: ko.toJSON(Person.ViewModel),
contentType:
'application/json'
,
async:
true
,
beforeSend:
function
() {
// Display loading image
},
success:
function
(result) {
// Handle the response here.
},
complete:
function
() {
// Hide loading image.
},
error:
function
(jqXHR, textStatus, errorThrown) {
// Handle error.
}
});
}
});
},
};
$(document).ready(
function
() {
Person.PrepareKo();
Person.BindUIwithViewModel(Person.ViewModel);
Person.EvaluateJqueryUI();
Person.RegisterUIEventHandlers();
});
这里的Person是命名空间,或者你可你称它为描述person相关操作的核心对象。为了更好的理解上述代码,在我解释上面方法之前。我想简单介绍一下knockout的一些内容。
关于knockout:
到目前为止,我解释了如何绑定viewmodel和UI元素。但是我没介绍如何创建viewmodel。大体上,你可以像下面这样创建一个viewmodel:
1
2
3
4
5
|
var
myViewModel = {
Name: ko.observable(
'Bob'
),
Age: ko.observable(123),
Report: ko.observableArray([1,5,6,7,8])
};
|
可以这样激活knockout:
1
|
ko.applyBindings(myViewModel);
|
通过阅读上述代码,貌似我们需要在每个属性上调用ko.observable。但是别担心,我们还有其它的方式可以选择。Knockout在knockout.mapping-* 库中提供了引入的功能。 我们在第三步的时候已经加入了该类库。我们也在第十三步中使用了一次:
1
|
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
|
ko.mapping.fromJS通过服务器服务器提供的javascript对象为我们创建了想要的视图模型(viewmodel)。通过这种方式我们在Person.ViewModel 中实现了视图模型(viewmodel)。此后,所有的Person.ViewModel 属性都是可见的,而且我们可以通过方法调用它的属性。例如:我们可以像Person.ViewModel.Name()这样获得person的name属性。也可以像Person.ViewModel.Name('New Name') 设置person的name属性。正如你所注意到的:Person.ViweModel 已经不再适合存储。这意味着,如果你直接向服务器传递Person.ViewModel ,它将不能映射为.NET可识别的对象。因此,我们需要从knockout获得无格式的JS对象。想要实现上述需求,就要调用ko.toJSON 方法。
1
|
ko.toJSON(Person.ViewModel)
|
下面我们开始探讨Person 对象中定义的方法:
PrepareKo: 这个方法是设置knockout,或者扩展它默认的方法。在上面粘贴的代码中。由于.NET对JSON不兼容,我创建了我自己的绑定句柄(handler)来捕获数据。由于关于.NET和JSON的内容,已经超出本文的范围,在此不再赘述。(如果你有兴趣,请回帖,我将向您详细介绍)下面是绑定数据的样例:
1
|
<
inputdata-bind
=
"date: AnyDate"
type
=
"text"
>
|
我们可以在第十二步中的代码中看到使用它的代码:
1
|
controlBuilder.Attributes.Add(
"data-bind"
,
"date: "
+ propertyObject);
|
-
ParseJsonDate
: 这是一个处理JSON格式转换为JS格式的公共方法。 -
BindUIwithViewModel
: 这个方法实现向UI元素绑定viewModel。 -
EvaluateJqueryUI
: 这个方法处理jQuery UI相关的操作。这里已经将datepicker处理完毕。 -
RegisterUIEventHandlers
: 这个方法实现注册UI元素的事件句柄。目前,元素的单击事件已经被注册为save。Save 方法首先校验页面,阻止默认功能并且触发一个URL指向Person.SaveUrl的AJAX请求。由于这个URL是从服务器端用Url.Action 生成的,因此我们不用考虑域名和虚拟目录。
以上就是关于Person内的所有对象。所有材料都准备完毕,可以进行开发了。也就是说,一旦文件建好了,我们可以用合适的顺序一个一个调用相关方法。下面可以运行一下程序看看效果。
兴趣点
我们建立了一个包含上百个基础UI元素和7个处理大量json数据的富元素页面。这个页面运行的非常流畅,而且没有发现任何展示方面的问题。至于代码,那个页面比我在这里介绍的页面复杂得多。但是,使用的JS模式方面,采用了相似的方式。在我的那个项目中,js脚本被分割到了一系列的文件中。最后,传统ASP.NET开发者对zero处理页面元素从而生成服务器端代码的目的会比较好奇。对我来说,我感觉是为了更好地实现客户端的数据分离操作。让我们专注于处理数据。
转自:http://www.codeproject.com/Articles/657981/ASP-NET-MVC-4-with-Knockout-Js