如何将 knockout.js 与 ASP.NET MVC ViewModels 一起使用?

Posted

技术标签:

【中文标题】如何将 knockout.js 与 ASP.NET MVC ViewModels 一起使用?【英文标题】:How to use knockout.js with ASP.NET MVC ViewModels? 【发布时间】:2012-06-18 19:10:21 【问题描述】:

赏金

已经有一段时间了,我还有几个悬而未决的问题。我希望通过增加赏金也许这些问题会得到解答。

    如何在 knockout.js 中使用 html 助手

    为什么需要准备好文档才能使其正常工作(有关更多信息,请参阅第一次编辑)

    如果我在视图模型中使用剔除映射,我该怎么做?由于映射,我没有功能。

    function AppViewModel() 
    
        // ... leave firstName, lastName, and fullName unchanged here ...
    
        this.capitalizeLastName = function() 
    
        var currentVal = this.lastName();        // Read the current value
    
        this.lastName(currentVal.toUpperCase()); // Write back a modified value
    
    ;
    

    我想使用插件,例如,我希望能够像用户取消请求一样回滚可观察对象,我希望能够返回到最后一个值。根据我的研究,这似乎是通过制作像editables 这样的插件的人来实现的

    如果我使用映射,我该如何使用类似的东西?我真的不想使用在我的视图中手动映射的方法,即我将每个 MVC viewMode 字段映射到一个 KO 模型字段,因为我想要尽可能少的内联 javascript,这似乎是工作的两倍,那就是为什么我喜欢那个映射。

    我担心为了使这项工作变得容易(通过使用映射)我会失去很多 KO 能力,但另一方面我担心手动映射会做很多工作并且会使我的视图包含太多信息,将来可能会变得更难维护(例如,如果我在 MVC 模型中删除了一个属性,我也必须在 KO 视图模型中移动它)


原帖

我正在使用 asp.net mvc 3,我正在研究淘汰赛,因为它看起来很酷,但我很难弄清楚它如何与 asp.net mvc 一起工作,尤其是视图模型。

对我来说,我现在正在做这样的事情

 public class CourseVM
    
        public int CourseId  get; set; 
        [Required(ErrorMessage = "Course name is required")]
        [StringLength(40, ErrorMessage = "Course name cannot be this long.")]
        public string CourseName get; set; 


        public List<StudentVm> StudentViewModels  get; set; 


我将拥有一个具有一些基本属性(如 CourseName)的虚拟机,并且在它之上会进行一些简单的验证。如果需要,Vm 模型中可能还包含其他视图模型。

如果我会使用 html 助手帮助我将它显示给用户,我会将这个 Vm 传递给视图。

@Html.TextBoxFor(x => x.CourseName)

我可能有一些 foreach 循环或其他东西来从学生视图模型的集合中获取数据。

然后,当我提交表单时,我会使用 jquery 和 serialize array 并将其发送到控制器操作方法,该方法会将其绑定回视图模型。

有了 knockout.js,一切都不同了,因为您现在有了它的视图模型,而且从我看到的所有示例中,它们都没有使用 html 帮助器。

你如何将 MVC 的这 2 个特性与 knockout.js 一起使用?

我找到了 this video,它简要地(视频的最后几分钟 @ 18:48)通过基本上有一个内联脚本来使用视图模型,该脚本具有被分配的值的 knockout.js 视图模型视图模型。

这是唯一的方法吗?在我的示例中,其中包含一组视图模型怎么样?我是否必须有一个 foreach 循环或其他东西来提取所有值并将其分配给淘汰赛?

至于 html 助手,视频没有说明它们。

这两个领域让我很困惑,因为似乎没有多少人谈论它,而且当示例只是一些硬编码时,这让我对初始值和所有内容如何进入视图感到困惑值示例。


编辑

我正在尝试 Darin Dimitrov 的建议,这似乎可行(不过我不得不对他的代码进行一些更改)。不知道为什么我必须使用准备好的文档,但不知何故,如果没有它,一切都没有准备好。

@model MvcApplication1.Models.Test

@
    Layout = null;


<!DOCTYPE html>

<html>
<head>
    <title>Index</title>
    <script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
    <script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
    <script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
   <script type="text/javascript">

   $(function()
   
      var model = @Html.Raw(Json.Encode(Model));


// Activates knockout.js
ko.applyBindings(model);
   );

</script>

</head>
<body>
    <div>
        <p>First name: <strong data-bind="text: FirstName"></strong></p>
        <p>Last name: <strong data-bind="text: LastName"></strong></p>
        @Model.FirstName , @Model.LastName
    </div>
</body>
</html>

我必须将它包裹在一个 jquery 文档中以使其工作。

我也收到此警告。不知道是怎么回事。

Warning 1   Conditional compilation is turned off   -> @Html.Raw

所以我有一个起点,我想至少会在我进行更多的尝试以及它是如何工作的时候更新。

我正在尝试阅读交互式教程,但改用 ViewModel。

还不知道如何处理这些部分

function AppViewModel() 
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");

function AppViewModel() 
    // ... leave firstName, lastName, and fullName unchanged here ...

    this.capitalizeLastName = function() 
        var currentVal = this.lastName();        // Read the current value
        this.lastName(currentVal.toUpperCase()); // Write back a modified value
    ;


编辑 2

我能够找出第一个问题。对第二个问题毫无头绪。然而虽然。有人有什么想法吗?

 @model MvcApplication1.Models.Test

    @
        Layout = null;
    

    <!DOCTYPE html>

    <html>
    <head>
        <title>Index</title>
        <script src="../../Scripts/jquery-1.5.1.js" type="text/javascript"></script>
        <script src="../../Scripts/knockout-2.1.0.js" type="text/javascript"></script>
        <script src="../../Scripts/knockout.mapping-latest.js" type="text/javascript"></script>
       <script type="text/javascript">

       $(function()
       
        var model = @Html.Raw(Json.Encode(Model));
        var viewModel = ko.mapping.fromJS(model);
        ko.applyBindings(viewModel);

       );

    </script>

    </head>
    <body>
        <div>
            @*grab values from the view model directly*@
            <p>First name: <strong data-bind="text: FirstName"></strong></p>
            <p>Last name: <strong data-bind="text: LastName"></strong></p>

            @*grab values from my second view model that I made*@
            <p>SomeOtherValue <strong data-bind="text: Test2.SomeOtherValue"></strong></p>
            <p>Another <strong data-bind="text: Test2.Another"></strong></p>

            @*allow changes to all the values that should be then sync the above values.*@
            <p>First name: <input data-bind="value: FirstName" /></p>
            <p>Last name: <input data-bind="value: LastName" /></p>
            <p>SomeOtherValue <input data-bind="value: Test2.SomeOtherValue" /></p>
            <p>Another <input data-bind="value: Test2.Another" /></p>

           @* seeing if I can do it with p tags and see if they all update.*@
            <p data-bind="foreach: Test3">
                <strong data-bind="text: Test3Value"></strong> 
            </p>

     @*took my 3rd view model that is in a collection and output all values as a textbox*@       
    <table>
        <thead><tr>
            <th>Test3</th>
        </tr></thead>
          <tbody data-bind="foreach: Test3">
            <tr>
                <td>    
                    <strong data-bind="text: Test3Value"></strong> 
<input type="text" data-bind="value: Test3Value"/>
                </td>
            </tr>    
        </tbody>
    </table>

控制器

  public ActionResult Index()
    
              Test2 test2 = new Test2
        
            Another = "test",
            SomeOtherValue = "test2"
        ;

        Test vm = new Test
        
            FirstName = "Bob",
            LastName = "N/A",
             Test2 = test2,

        ;
        for (int i = 0; i < 10; i++)
        
            Test3 test3 = new Test3
            
                Test3Value = i.ToString()
            ;

             vm.Test3.Add(test3);
        

        return View(vm);
    

【问题讨论】:

我刚刚写了一篇博文来回答另一个类似的问题:roysvork.wordpress.com/2012/12/09/… 它可能无法完全回答您的问题,但它可以让您很好地了解事情的运作方式。我希望在不久的将来再发一篇文章来跟进这一点。如果您需要更多信息,请随时在帖子上的 cmets 或此处向我提问。 【参考方案1】:

我想我已经总结了你所有的问题,如果我遗漏了什么,请告诉我(如果你能把你所有的问题总结在一个地方就好了 =))

注意。添加了与ko.editable 插件的兼容性

Download完整代码

如何在 knockout.js 中使用 html 助手

这很简单:

@Html.TextBoxFor(model => model.CourseId, new  data_bind = "value: CourseId" )

地点:

value: CourseId 表示您正在将input 控件的value 属性与您的模型和脚本模型中的CourseId 属性绑定

结果是:

<input data-bind="value: CourseId" data-val="true" data-val-number="The field CourseId must be a number." data-val-required="The CourseId field is required." id="CourseId" name="CourseId" type="text" value="12" />

为什么需要准备好文档才能使其工作(有关更多信息,请参阅第一次编辑)

我还不明白为什么你需要使用ready 事件来序列化模型,但似乎它只是必需(不过不用担心)

如果我在视图模型中使用剔除映射,我该怎么做?由于映射,我没有功能。

如果我理解正确,您需要在 KO 模型中添加一个新方法,这很容易合并模型

For more info, in the section -Mapping from different sources-

function viewModel() 
    this.addStudent = function () 
        alert("de");
    ;
;

$(function () 
    var jsonModel = '@Html.Raw(JsonConvert.SerializeObject(this.Model))';
    var mvcModel = ko.mapping.fromJSON(jsonModel);

    var myViewModel = new viewModel();
    var g = ko.mapping.fromJS(myViewModel, mvcModel);

    ko.applyBindings(g);
);

关于您收到的警告

警告 1 条件编译已关闭 -> @Html.Raw

你需要使用引号

与 ko.editable 插件的兼容性

我以为它会更复杂,但事实证明集成真的很容易,为了使您的模型可编辑,只需添加以下行:(请记住,在这种情况下,我使用的是混合模型,来自服务器并在客户端中添加扩展名,并且可编辑的内容很简单......这很棒):

    ko.editable(g);
    ko.applyBindings(g);

从这里你只需要使用插件添加的扩展来播放你的绑定,例如,我有一个按钮可以像这样开始编辑我的字段,在这个按钮中我开始编辑过程:

    this.editMode = function () 
        this.isInEditMode(!this.isInEditMode());
        this.beginEdit();
    ;

然后我有提交和取消按钮,代码如下:

    this.executeCommit = function () 
        this.commit();
        this.isInEditMode(false);
    ;
    this.executeRollback = function () 
        if (this.hasChanges()) 
            if (confirm("Are you sure you want to discard the changes?")) 
                this.rollback();
                this.isInEditMode(false);
            
        
        else 
            this.rollback();
            this.isInEditMode(false);
        
    ;

最后,我有一个字段来指示字段是否处于编辑模式,这只是绑定启用属性。

this.isInEditMode = ko.observable(false);

关于你的数组问题

我可能有一些 foreach 循环或其他东西来从学生视图模型的集合中获取数据。

然后,当我提交表单时,我会使用 jquery 和序列化数组并将其发送到控制器操作方法,该方法会将其绑定回视图模型。

你可以对 KO 做同样的事情,在下面的例子中,我将创建以下输出:

基本上在这里,您有两个列表,使用Helpers 创建并与KO 绑定,它们绑定了dblClick 事件,当触发时,从当前列表中删除所选项目并将其添加到另一个列表中,当您发布到Controller,每个列表的内容都会作为 JSON 数据发送并重新附加到服务器模型

掘金队:

Newtonsoft jQuery knockoutjs Knockout.Mapping

外部scripts。

控制器代码

    [HttpGet]
    public ActionResult Index()
    
        var m = new CourseVM  CourseId = 12, CourseName = ".Net" ;

        m.StudentViewModels.Add(new StudentVm  ID = 545, Name = "Name from server", Lastname = "last name from server" );

        return View(m);
    

    [HttpPost]
    public ActionResult Index(CourseVM model)
    
        if (!string.IsNullOrWhiteSpace(model.StudentsSerialized))
        
            model.StudentViewModels = JsonConvert.DeserializeObject<List<StudentVm>>(model.StudentsSerialized);
            model.StudentsSerialized = string.Empty;
        

        if (!string.IsNullOrWhiteSpace(model.SelectedStudentsSerialized))
        
            model.SelectedStudents = JsonConvert.DeserializeObject<List<StudentVm>>(model.SelectedStudentsSerialized);
            model.SelectedStudentsSerialized = string.Empty;
        

        return View(model);
    

型号

public class CourseVM

    public CourseVM()
    
        this.StudentViewModels = new List<StudentVm>();
        this.SelectedStudents = new List<StudentVm>();
    

    public int CourseId  get; set; 

    [Required(ErrorMessage = "Course name is required")]
    [StringLength(100, ErrorMessage = "Course name cannot be this long.")]
    public string CourseName  get; set; 

    public List<StudentVm> StudentViewModels  get; set; 
    public List<StudentVm> SelectedStudents  get; set; 

    public string StudentsSerialized  get; set; 
    public string SelectedStudentsSerialized  get; set; 


public class StudentVm

    public int ID  get; set; 
    public string Name  get; set; 
    public string Lastname  get; set; 

CSHTML 页面

@using (Html.BeginForm())

    @Html.ValidationSummary(true)
    <fieldset>
        <legend>CourseVM</legend>

        <div>
            <div class="editor-label">
                @Html.LabelFor(model => model.CourseId)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => model.CourseId, new  data_bind = "enable: isInEditMode, value: CourseId" )
                @Html.ValidationMessageFor(model => model.CourseId)
            </div>

            <div class="editor-label">
                @Html.LabelFor(model => model.CourseName)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => model.CourseName, new  data_bind = "enable: isInEditMode, value: CourseName" )
                @Html.ValidationMessageFor(model => model.CourseName)
            </div>
            <div class="editor-label">
                @Html.LabelFor(model => model.StudentViewModels);
            </div>
            <div class="editor-field">

                @Html.ListBoxFor(
                    model => model.StudentViewModels,
                    new SelectList(this.Model.StudentViewModels, "ID", "Name"),
                    new
                    
                        style = "width: 37%;",
                        data_bind = "enable: isInEditMode, options: StudentViewModels, optionsText: 'Name', value: leftStudentSelected, event:  dblclick: moveFromLeftToRight "
                    
                )
                @Html.ListBoxFor(
                    model => model.SelectedStudents,
                    new SelectList(this.Model.SelectedStudents, "ID", "Name"),
                    new
                    
                        style = "width: 37%;",
                        data_bind = "enable: isInEditMode, options: SelectedStudents, optionsText: 'Name', value: rightStudentSelected, event:  dblclick: moveFromRightToLeft "
                    
                )
            </div>

            @Html.HiddenFor(model => model.CourseId, new  data_bind="value: CourseId" )
            @Html.HiddenFor(model => model.CourseName, new  data_bind="value: CourseName" )
            @Html.HiddenFor(model => model.StudentsSerialized, new  data_bind = "value: StudentsSerialized" )
            @Html.HiddenFor(model => model.SelectedStudentsSerialized, new  data_bind = "value: SelectedStudentsSerialized" )
        </div>

        <p>
            <input type="submit" value="Save" data-bind="enable: !isInEditMode()" /> 
            <button data-bind="enable: !isInEditMode(), click: editMode">Edit mode</button><br />
            <div>
                <button data-bind="enable: isInEditMode, click: addStudent">Add Student</button>
                <button data-bind="enable: hasChanges, click: executeCommit">Commit</button>
                <button data-bind="enable: isInEditMode, click: executeRollback">Cancel</button>
            </div>
        </p>
    </fieldset>

脚本

<script src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout-2.1.0.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout.mapping-latest.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/ko.editables.js")" type="text/javascript"></script>

<script type="text/javascript">
    var g = null;
    function ViewModel() 
        this.addStudent = function () 
            this.StudentViewModels.push(new Student(25, "my name" + new Date(), "my last name"));
            this.serializeLists();
        ;
        this.serializeLists = function () 
            this.StudentsSerialized(ko.toJSON(this.StudentViewModels));
            this.SelectedStudentsSerialized(ko.toJSON(this.SelectedStudents));
        ;
        this.leftStudentSelected = ko.observable();
        this.rightStudentSelected = ko.observable();
        this.moveFromLeftToRight = function () 
            this.SelectedStudents.push(this.leftStudentSelected());
            this.StudentViewModels.remove(this.leftStudentSelected());
            this.serializeLists();
        ;
        this.moveFromRightToLeft = function () 
            this.StudentViewModels.push(this.rightStudentSelected());
            this.SelectedStudents.remove(this.rightStudentSelected());
            this.serializeLists();
        ;
        this.isInEditMode = ko.observable(false);
        this.executeCommit = function () 
            this.commit();
            this.isInEditMode(false);
        ;
        this.executeRollback = function () 
            if (this.hasChanges()) 
                if (confirm("Are you sure you want to discard the changes?")) 
                    this.rollback();
                    this.isInEditMode(false);
                
            
            else 
                this.rollback();
                this.isInEditMode(false);
            
        ;
        this.editMode = function () 
            this.isInEditMode(!this.isInEditMode());
            this.beginEdit();
        ;
    

    function Student(id, name, lastName) 
        this.ID = id;
        this.Name = name;
        this.LastName = lastName;
    

    $(function () 
        var jsonModel = '@Html.Raw(JsonConvert.SerializeObject(this.Model))';
        var mvcModel = ko.mapping.fromJSON(jsonModel);

        var myViewModel = new ViewModel();
        g = ko.mapping.fromJS(myViewModel, mvcModel);

        g.StudentsSerialized(ko.toJSON(g.StudentViewModels));
        g.SelectedStudentsSerialized(ko.toJSON(g.SelectedStudents));

        ko.editable(g);
        ko.applyBindings(g);
    );
</script>

注意:我刚刚添加了这些行:

        @Html.HiddenFor(model => model.CourseId, new  data_bind="value: CourseId" )
        @Html.HiddenFor(model => model.CourseName, new  data_bind="value: CourseName" )

因为当我提交表单时,我的字段被禁用,所以值没有传输到服务器,这就是为什么我添加了几个隐藏字段来解决问题

【讨论】:

嗯,信息量很大。从您的回答和 Pual 的回答中,我想我几乎得到了所有问题的解答,除了如何使用可编辑的插件。希望有人知道我如何使用它。 我刚刚添加了与ko.editables插件的兼容性,您可以查看更新的响应,或者如果您愿意,您可以下载整个项目以在本地运行它 我会尽可能检查一下。是否需要进行很多更改才能使其正常工作?我想知道是否对于我找到的每个插件,我是否必须对其进行更改,然后必须保留我自己的版本。 不。你会感到惊讶,它几乎是开箱即用的 谢谢大家,我从您的回复中学到了几个新策略。赞一个!【参考方案2】:

您可以将 ASP.NET MVC 视图模型序列化为 javascript 变量:

@model CourseVM
<script type="text/javascript">
    var model = @Html.Raw(Json.Encode(Model));
    // go ahead and use the model javascript variable to bind with ko
</script>

knockout documentation 中有很多例子可以参考。

【讨论】:

是的,我浏览了他们在网站上提供的交互式教程,但我真的从来没有看到与 asp.net mvc 有任何关系。我看到他们也有一些映射插件,但不确定它如何适合。在您的示例中,您将如何将其绑定到淘汰模型(在另一个脚本中)。我真的想要尽可能少的内联 javascript(最好不要,但我想是不可能的) 您要解决什么问题?如果你想要 MVC 视图并且对如何使用它们感到满意,你可以坚持下去。如果您想要客户端数据绑定和操作,那么 KO 是一个不错的选择。如this answer所示,您可以从您的 MVC 代码生成您的 KO 视图模型。它需要 vm 并将其序列化为 json。然后在客户端上,您可以将结果映射到 javascript 视图模型。然后将视图模型绑定到视图,一切就绪。关键是 MVC 和 KO 不必以任何方式耦合,除非您希望它们耦合。这完全取决于您要解决的问题。 看不到与 asp.net mvc 有任何关系是正常的。 Knockout 是一个客户端框架。它不知道也不关心您使用的是什么服务器端语言。这两个框架应该绝对解耦。 @JohnPapa - 我确实喜欢我现在做事的方式,但我也喜欢学习新事物(我发现 KO 在某些情况下非常有用)。我知道 KO 是客户端脚本,但对我来说,我认为它们是一起工作的。我目前使用视图模型和 html 助手生成我的视图。所以在我看来,KO需要与此一起工作。例如说你有编辑对话框。您将如何设计数据库中的值并将其填充到这些字段中。如果我使用我的方式,那将是具有 viewModel 的 html 助手视图。将填充视图模型并通过操作方法发送并使用它。 @chobo2,knockout 是一个客户端框架。它使用客户端上的视图模型在客户端上实现 MVC 模式。服务器是解耦的。您也可以在其上使用视图模型。这只是2个不同的地方。如果您想使用 javascript 在客户端上实现一些复杂的逻辑,那么敲除可以简化这一点。否则,老实说,你不需要它。【参考方案3】:

要在服务器映射后获得额外的计算属性,您需要进一步增强客户端的视图模型。

例如:

var viewModel = ko.mapping.fromJS(model);

viewModel.capitalizedName = ko.computed(function() ..., viewModel);

因此,每次从原始 JSON 映射时,都需要重新应用计算的属性。

此外,映射插件提供了增量更新视图模型的能力,而不是每次来回重新创建它(使用fromJS 中的附加参数):

// Every time data is received from the server:
ko.mapping.fromJS(data, viewModel);

这会在您的模型上执行增量数据更新,仅包含映射的属性。您可以在mapping documentation 中阅读更多相关信息

您在 Darin 的回答中提到了 FluentJSON 包。我是那本书的作者,但它的用例比 ko.mapping 更具体。如果您的视图模型是一种方式(即服务器->客户端),我通常只会使用它,然后数据以某种不同的格式(或根本不)发回。或者,如果您的 javascript 视图模型需要采用与服务器模型完全不同的格式。

【讨论】:

嗯,那么我想也许 FluentJSON 不适合我,因为我的视图模型大部分时间都是双向的(我通常通过 json 将它发回,然后在操作方法参数中将它绑定到视图模型)。你知道我如何使用我提到的那些像可编辑的插件吗?最后,通过使用映射并尝试使用我的视图模型而不是不使用它,我是否会失去任何功能? 我没有使用任何插件,所以不确定。我过去所做的只是订阅每个更改并保留一堆序列化的视图模型状态,我将在更改时推送并在撤消时弹出 (see this question)。 映射不会阻止您使用任何功能,您只需要确保并遵守其处理与 JS 之间的映射的约定,以使它们完美地结合在一起。 嗯,您发布的问题的公认答案基本上就是插件的内容。这就是让我感到困惑的地方,因为您可以看到他们制作了一个视图模型,然后使用他们制作的函数(ko.observableArrayWithUndo([]))。如果我正在做映射,我不知道该怎么做。唯一想到的是编写我自己的映射(我现在怀疑我是否可以正确),它具有可撤消的可观察性或将每个属性映射出来,但是我基本上有重复的视图模型,一个用于服务器端,一个用于客户端,我是害怕会变得无法维护 啊,对不起,我在谈论我对那个问题的回答,抱歉应该直接链接。

以上是关于如何将 knockout.js 与 ASP.NET MVC ViewModels 一起使用?的主要内容,如果未能解决你的问题,请参考以下文章

如何在ASP.NET MVC5中正确包含jQuery与knockout.js

基于Bootstrap和Knockout.js的ASP.NET MVC开发实战

ASP.NET Web API教程2.3.5 用Knockout.js创建动态UI

基于Bootstrap和Knockout.js的ASP.NET MVC开发实战 关于 拦截器的 学习 部分

使用asp.net mvc,boostrap及knockout.js开发微信自定义菜单编辑工具

持久化视图状态数据并中继到 Knockout.JS