如何在ASP.NET MVC视图模型中使用knockout.js?

130

悬赏

已经有一段时间了,我仍然有几个未解决的问题。我希望通过添加悬赏来解答这些问题。

  1. 如何在knockout.js中使用html helpers
  2. 为什么需要document ready才能使它工作(有关更多信息,请参见第一次编辑)

  3. 如果我正在使用knockout映射与我的视图模型,我该如何做类似于这样的事情?因为映射中没有函数。

  4. 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
    
    };
    
  5. 我希望使用插件,例如,如果用户取消请求,我想能够回滚 observables 并返回最后一个值。经过我的研究,人们通过制作插件(如editables)来实现这一点。

    如果我正在使用 mapping,我该如何使用类似的东西?我真的不想采用手动映射的方法,在我的视图中映射每个 MVC viewMode 字段到 KO 模型字段,因为我希望内联 JavaScript 尽可能少,这似乎是双倍的工作,这就是我喜欢 mapping 的原因。

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

  7.  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; }
    
    }
    

    我会创建一个Vm,其中包括一些基本属性,如CourseName,并在其上设置一些简单的验证。如果需要,Vm模型中也可能包含其他视图模型。

    然后,我会将此Vm传递给View,在那里我会使用html helpers来帮助我向用户显示它。

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

    我可能需要使用一些 foreach 循环之类的东西来从 Student View Models 集合中获取数据。

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

    使用 knockout.js 完全不同,因为现在你有了它的 viewmodels,而且从我看到的所有示例中,它们都不使用html helpers。

    如何在 MVC 中使用这 2 个功能与 knockout.js?

    我发现了这个视频,它简要地(视频的最后几分钟 @ 18:48)介绍了一种使用viewmodels的方法,基本上是在具有 knockout.js viewmodel 的内联脚本中分配获取 ViewModel 的值。

    这是唯一的方法吗? 在我的例子中,如果有一个包含 viewmodels 集合的对象怎么做? 我必须使用 foreach 循环或其他方式提取所有值并将其分配给 knockout 吗?

    至于 html helpers,视频中没有提到。

    这些是令我困惑不解的 2 个领域,因为很少有人谈论它,而每个示例仅仅是一些硬编码值的示例,让我困惑不解。


    编辑

    我正在尝试 Darin Dimitrov 建议的方法,这似乎有效(我不过必须对他的代码进行一些更改)。 不知道为什么必须使用 document ready,但某种程度上没有它,所有东西都还没有准备好。

    @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);
        }
    

2
我刚刚写了一篇博客文章来回答另一个类似的问题:http://roysvork.wordpress.com/2012/12/09/dynamic-repeating-field-groups-in-asp-net-mvc-with-just-a-dash-of-knockout-js/ 它可能不能完全回答你的问题,但它可以给你一个很好的想法,如何让事情工作。我希望在不久的将来能够跟进这个问题,并发表更多的文章。如果您需要更多信息,请在文章评论区或此处随时向我提问。 - beyond-code
3个回答

182

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

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

下载完整代码

如何在knockout.js中使用html helpers

这很简单:

@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" />

为什么需要使用document ready(请参见第一个编辑获取更多信息)

我还不明白为什么需要使用ready事件来串行化模型,但似乎它是必需的(不必担心)

如果我正在使用Knockout Mapping和我的视图模型,我该如何做类似这样的事情?因为映射不需要函数。

如果我理解正确,您需要将新方法附加到KO模型,很容易实现合并模型。

有关更多信息,请参阅-从不同源映射-部分。

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 循环或其他操作,以从 Student View Models 集合中获取数据。

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

您可以使用 KO 来完成同样的操作,在下面的示例中,我将创建以下输出:

enter image description here

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

Nuggets:

External 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" })

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


1
我刚刚添加了与 ko.editables 插件的兼容性,您可以检查更新后的响应,或者如果您愿意,可以下载整个项目在本地运行。 - Jupaol
1
非常感谢你,我从你的回复中学到了几种新的策略。赞! - sky-dev
@Jupaol,有没有办法不在视图中编写所有这些JS代码,但仍然可以获取所有服务器端编写的视图模型?对于简单的内容,我使用data-属性,但是对于完整的vm呢? - Siddharth Pandey
为了正确更新ViewModel的属性并且不会失去对ViewModel方法中属性的引用,可以使用以下代码:var g = ko.mapping.fromJS(mvcModel, null, myViewModel); 这个签名就能解决问题。我花了一些时间才明白为什么无法从ViewModel的方法内部访问属性。 - Yoo Matsuo
建议使用kMVC,这是一个Razor HTML助手库,它让你感觉所有操作都在服务器端完成。个人而言,我没有尝试过它,因为发现对于Knockout的razor助手有点不知所措。我更倾向于将客户端代码与服务器端代码分开处理,或者可以重复使用在项目中的自定义绑定处理程序。 - valerysntx
显示剩余11条评论

23
你可以将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文档中的许多示例。


2
是的,我已经完成了他们网站上的交互式教程,但我真的没有看到任何与asp.net mvc有关的内容。我看到他们还有一些映射插件,但不确定它如何适用。在您的示例中,您将如何将其绑定到knockout模型(在另一个脚本中)。我真的希望尽可能少地使用内联JavaScript(最好不使用,但我想这是不可能的)。 - chobo2
2
你想解决什么问题?如果你想要MVC视图并且对如何使用它们感到满意,那么你可以继续使用它们。如果你想要客户端数据绑定和操作,那么KO是一个很好的选择。你可以像这个答案所示一样从你的MVC代码生成你的KO视图模型。它将vm序列化为json。然后在客户端,你可以将结果映射到JavaScript视图模型。然后将视图模型绑定到视图中,就完成了。关键是,除非你想要它们耦合在一起,否则MVC和KO不必以任何方式耦合在一起。这完全取决于你想要解决的问题。 - John Papa
1
很正常你没有看到任何与asp.net mvc有关的内容。Knockout是一个客户端框架,它不知道也不关心你使用的服务器端语言是什么。这两个框架应该完全解耦。 - Darin Dimitrov
@JohnPapa - 我目前喜欢自己的工作方式,但我也喜欢学习新事物(我发现 KO 在某些情况下非常有用)。我知道 KO 是客户端脚本,但在我看来它们可以共同工作。我目前使用视图模型和 HTML 帮助程序生成视图。所以在我的想法中,KO 需要与此协作。例如,假设你拥有一个编辑对话框。你会如何设计并将数据库中的值填充到这些字段中?如果我使用我的方法,它将是一个带有 viewModel 的 html helpers 视图。将填充视图模型并通过 Action 方法发送使用它。 - chobo2
1
@chobo2,Knockout是一个客户端框架。它在客户端使用视图模型来实现MVC模式。服务器是解耦的。你也可以在服务器上使用视图模型。这只是两个不同的地方。如果你想要在客户端使用JavaScript实现一些复杂的逻辑,那么Knockout可以简化这个过程。否则,老实说,你不需要它。 - Darin Dimitrov
显示剩余7条评论

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);

这将对您的模型执行增量数据更新,仅涉及已映射的属性。您可以在映射文档中阅读更多有关信息。
在Darin的答案评论中,您提到了FluentJSON包。我是它的作者,但它的使用场景比ko.mapping更为特定。通常只有在您的视图模型是单向的(即服务器->客户端),然后以某种不同的格式(或根本不)返回数据时,才会使用它。或者如果您的JavaScript视图模型需要与服务器模型大不相同的格式。

嗯,那我猜FluentJSON可能不适合我,因为我的视图模型大多数情况下是双向的(虽然我通常通过json将其发送回来,然后将其绑定到操作方法参数中的视图模型)。你知道我怎么能使用我提到的那些插件,比如editable吗?最后,如果我使用映射并尝试使用我的视图模型而不是不使用它,是否会失去任何功能? - chobo2
我没有使用过任何插件,所以不确定。我过去所做的只是订阅每一个更改,并保留一堆串行化的视图模型状态,在更改时将其推入堆栈并在撤销时弹出 (参见此问题). - Paul Tyng
映射不会影响任何功能,您只需要确保并遵守其处理从JS到映射的约定,以使它们协同工作。 - Paul Tyng
你发布的问题的被接受答案基本上就是插件。这就是让我感到困惑的地方,因为你可以看到他们制作了一个视图模型,然后使用了他们制作的函数(ko.observableArrayWithUndo([]))。如果我正在进行映射,我不知道该怎么做。唯一想到的是编写自己的映射(我怀疑我此时能否编写),具有可撤销的观察属性或将每个属性映射出来,但那样我基本上就有了重复的视图模型,一个用于服务器端,一个用于客户端,我担心这将变得难以维护。 - chobo2
啊,对不起,我在谈论我的回答那个问题,抱歉应该直接链接。 - Paul Tyng
我看了你的帖子,但是接受的答案引起了我的注意。因为它正是我正在努力解决的问题。那个人基本上做了一个插件/包装器来完成我想要做的事情,但如果我使用KO映射,我不知道该怎么使用它。我必须将所有服务端视图模型属性映射到客户端视图模型。我担心我会创建太多重复代码,这将变得难以维护,或者如果我仅使用映射,我将失去功能。所以这就是我目前正在努力解决的问题。 - chobo2

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接