JS库Knockout在Asp.net MVC 4.0中的简单应用

简介

这篇文章将向大家介绍一下如何在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程序开发的新手,可以跳过基础部分,直接阅读进阶部分。

基础部分: 

  1. 创建ASP.NET MVC 4应用程序:文件 -> 新建 -> 项目-> 模板 -> Visual C# -> Web -> ASP.NET MVC 4 Web Application -> 给项目起一个友好的名字,单击确定.

  2. 在选择模板菜单中选择Basic。选择Razor作为视图引擎,单击确定.

  3. 手动下载knockout mapping library并添加到项目中。或者运行NuGet命令Install-Package Knockout.Mapping 实现该类库的添加。

  4. 右键Controllers文件夹-> 添加 -> Controller。命名为PersonController,单击添加按钮.

  5. 右键项目->添加 -> 新建文件夹。重命名为:ViewModel.

  6. 右键ViewModel文件夹-> 添加 -> 类。把新添加的类命名为:PersonViewModel。并添加如下代码:

    1
    2
    3
    4
    5
    6
    publicclass PersonViewModel
    {
         publicint Id { get ; set ; }
         publicstring Name { get ; set ; }
         public DateTime DateOfBirth { get ; set ; }
    }
  7. 回到PersonController,粘贴如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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的引用。

  8. 把光标定位到index方法中。右键鼠标,单击Add View VS将弹出一个窗口。保留默认选项。单击添加按钮。这样会在Views文件夹下的Person 文件夹下创建Index.cshtml文件。

  9. 打开App_Start 文件夹下的RouteConfig .cs文件。在这里设置controller为Person。如果完成了以上操作,RoutConfig文件会自动更新为下列代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    publicclass 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 }
             );
         }
    }
  10. 右键Content文件夹-> 添加 -> 新建项 -> 选择 样式表,并命名为Person.css. 

  11. 在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:

  1. 既然我们需要为每一个UI元素添加数据绑定属性,我们最好为自己建立一个html helper方法。为了实现上述操作,请在项目中添加一个文件夹,并重命名为Helper。在Helper文件夹下添加一个类文件HtmlExtensions.cs并将下面的代码复制到该类。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    public 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并复制下列代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public 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语句。如果不太理解上述方法体内的代码,不用担心,随着下面的学习,慢慢就懂了。

  2. 打开Views/Person文件夹下的Index.cshtml文件,并粘贴如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @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 >

    部分人可能遇到如下错误:

    1
    json does not exist in the current context

    你可以按照**连接**中的步骤解决这个问题。或者你需要用你自己的命名空间替换掉Mvc4withKnockoutJsWalkThrough。正如你在代码中看到的,我们正在使用在第12步中创建的html helper。同时你能注意到在script 模块(section)编写的脚本

    1
    2
    Person.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))); 的作用。   
     

  3. 你可能注意到我们在Index.cshtml 页中使用了样式。 因此,必须在相关的页中定义以上内容。在本实例中,是_Layout.cshtml。因此打开views文件夹下Shared folder文件夹中的_Layout.cshtml文件。在头文件结束前,添加下面的代码:   

    1
    @RenderSection( "styles" , required: false)

    最后,图层展示页面会是这个样子。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!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 >

  4. 现在,开始编写等待已久的javascript代码。打开Person.js文件(文件在Scripts文件夹下Application文件夹中)并粘贴下面代码:  

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    var 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值