【问题标题】:Editing tree-like structures with ASP.MVC [closed]使用 ASP.MVC 编辑树状结构 [关闭]
【发布时间】:2016-02-09 02:54:27
【问题描述】:

这是一个学习 ASP.NET MVC 5 的人提出的新手问题。我想创建一个 Web 应用程序屏幕,用户可以在其中编辑树状数据结构。在遵循最佳实践的同时,我不确定如何正确执行。

为简单起见,假设它是一个两级数据结构,一个司机列表,每个司机可以驾驶多辆汽车:

public class Car
{
  public string Name; { get; set; }
  public string Code; { get; set; }
}

public class Driver
{
  public string FullName { get; set; }
  public List<Cars> { get; set; }
}

public class Model
{
  public List<Driver> Drivers { get; set; }
}

我需要能够在同一屏幕上添加/删除/编辑司机(及其汽车)。这是否意味着每一次用户所做的更改(例如,添加一辆新车,然后更改其Code)都需要提交表单(即,点击我的控制器)并发回一个新视图给客户?

如果是这样,如果我想在客户端允许多个模型更改,然后将所有更新提交回我的控制器,我是否需要使用 MVVM 框架(如 Angular 或 Knockout)?还是可以用裸 ASP.NET MVC 来完成?

请注意,我不想要单页 Web 应用程序,我只想在执行 HTTP 发布操作之前在客户端缓存用户更新。

更新,所以这个网络应用程序目前以经典的 MVC 方式工作:首先是一个包含司机列表的视图,然后是一个包含汽车列表的视图,然后是一辆车。呈现每个视图都有一个单独的 HTTP 请求。

我想要的是让用户在同一个视图中编辑整个结构,然后点击更新按钮。那时,我希望整个数据结构的更新 POCO 模型能够提交并在我的 MVC 控制器中可用。 我想要一个客户端 JavaScript 框架(Knockout、Angular、Aurelia 等)来为我处理生成和更新 DefaultModelBinder 索引器,这样我就不必手动管理索引器了 (即&lt;input name="Drivers[2].Cars[1].Name" ... /&gt; 等,this q/a 中的更多详细信息)。

赏金将奖励给说明如何做到这一点的答案,带有相关示例代码

【问题讨论】:

  • 在答案herehere 中动态添加(和删除)集合项的一些选项
  • 是的,没错。集合索引器必须从零开始并且是连续的,除非您为 Index 属性添加隐藏字段,这允许 DefaultModelBinder 匹配非连续的索引器。
  • 这样想 - 你如何在你的模型的第三个 Driver 中获得第二个 CarName 属性` - 它会是 string name = model.Drivers[2].Cars[1].Name - 现在去掉 @ 987654334@ 前缀,这就是 name 属性必须是为了绑定 - 即 &lt;input name="Drivers[2].Cars[1].Name" ... /&gt;
  • Bear Bibeault 的 jQuery In Action 有一个关于构建客户端 DVD 收藏的精彩章节。或者 Angular 的 todo 应用程序演示,它们确实不是一个完整的 SPA,只是使用 Angular 来管理与您的 POCO 集合的绑定。您仍然可以使用表单帖子发布您的 POCO。
  • @avo,淘汰赛。只是装订部分。学习曲线低。网络上有大量使用 MVC 的教程。

标签: c# .net asp.net-mvc asp.net-mvc-5


【解决方案1】:

因此,您希望我们帮助您完成的事情需要相当多的时间来构建,但实际上非常简单(公平地说,大多数基本 KO 教程中都有足够的信息来完成所有这些工作。)

所以我用 3 种方法构建了一个页面和一个 MVC 控制器:一个用于页面本身,两个用于 GETting 或 POSTing 数据。

这是控制器的代码:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    public JsonResult PostDriversModel(DriversModel model)
    {
        return Json(new { Success = true }, JsonRequestBehavior.AllowGet);
    }

    [HttpGet]
    public JsonResult GetDriversModel()
    {
        var model = new DriversModel
        {
            Drivers = new List<Driver>
            {
                new Driver
                {
                    FullName = "John Doe",
                    Cars = new List<Car>
                    {
                        new Car {Code = "car0", Name = "Amazing car"},
                        new Car {Code = "car1", Name = "Cool car"}
                    },
                },
                new Driver
                {                        
                    FullName = "Johnny Dough",
                    Cars = new List<Car>
                    {
                        new Car {Code = "car2", Name = "Another Amazing car"},                      new Car {Code = "car3", Name = "Another Cool car"}
                    }
                },
            }
        };

        return Json(model, JsonRequestBehavior.AllowGet);
    }
}

如您所见,控制器非常简单,其中最大的方法是 GetDriversModel(),它为页面提供可使用的示例数据。

在这里,您可能会执行一些操作,例如查询您的长期存储以获取要在客户端呈现的树。它很可能会标有某种 Id,但由于这些细节不在你的问题中,我已经省略了它们。通过这个例子你可以很容易地弄清楚。

最有趣的部分实际上是在我使用敲除为 DriversModel 数据结构构建渲染器的页面上。首先,让我们检查一下 JavaScript:

KO 视图的核心是 ViewModel:

    function DriversViewModel() {
        var self = this;

        self.Drivers = ko.observableArray([]);

        self.addDriver = function() {
            self.Drivers.push(new DriverModel({ FullName: 'Mr. Noname', Cars: [] }));
        };

        self.removeDriver = function(driver) {
            self.Drivers.remove(driver);
        };

        self.update = function() {
            $.ajax("/Home/PostDriversModel", {
                data: ko.toJSON({ Drivers: self.Drivers }),
                type: "post", contentType: "application/json",
                success: function () { alert('Success!'); }
            });
        }

        $.getJSON('/Home/GetDriversModel', function (data) {
            var drivers = data.Drivers.map(function (driver) { return new DriverModel(driver); });
            drivers.push(new DriverModel({ Cars: [], FullName: 'Mr Nocars' }));
            self.Drivers(drivers);
        });
    }

在其中,我们定义了几个用于从树中添加/删除驱动程序的方法,以及将内容发布回服务器的方法。 ViewModel 非常简单(与本示例中的几乎所有内容一样)。请注意,在 JS 完成向服务器查询数据之后,我已将另一个随机驱动程序添加到列表中。这样做只是为了好玩(我也在做实验)。

以下是其余实体的 ViewModel:

    function CarModel(data) {
        var self = this;

        self.Code = ko.observable(data.Code);
        self.Name = ko.observable(data.Name);
    }

    function DriverModel(data) {
        var self = this;

        self.addCar = function () {
            self.Cars.push(new CarModel({ Name: 'Tank', Code: '__' }));
        };

        self.removeCar = function (car) {
            self.Cars.remove(car);
        };

        self.Cars = ko.observableArray(data.Cars.map(function(car) { return new CarModel(car); }));
        self.FullName = ko.observable(data.FullName);
    }

正如您所见,所有这些都包含一些初始化逻辑,我们将从服务器获取的 JSON 对象映射到客户端抽象。您还可以注意到它们不遵循 JavaScript 对象的命名约定。我是故意这样做的,这样当我们在客户端完成对它们的处理时,MVC 在将它们映射到 C# 对象时就没有问题了。 从长远来看,这可能不是一件好事,但为了简单起见,它会这样做。

所以基本上发生的情况是,当我们的 DriversViewModel 从服务器请求项目时,它会将所有数据映射到对 Knockout 友好的抽象,这些抽象会在它们发生变化时被 Knockout 跟踪。剩下的就是告诉 Knockout 使用这个 ViewModel:

    ko.applyBindings(new DriversViewModel());

现在 knout 已经准备好处理这些对象了,是时候构建 UI 部分了。

使用这些 KO 绑定的页面如下所示:

<div>
    <a href="#" data-bind="click: $root.addDriver">Add Driver</a>
    <a href="#" data-bind="click: $root.update">Update</a>
</div>

<ul data-bind="foreach: Drivers, visible: Drivers().length > 0">
    <ul>
        <div>
            <input data-bind="value: FullName"/>
            <a href="#" data-bind="click: $parent.removeDriver">Delete</a>
            <a href="#" data-bind="click: addCar">Add Car</a>
        </div>
        <ul class="no-cars" data-bind="visible: Cars().length == 0">No cars D:</ul>
        <ul data-bind="foreach: Cars, visible: Cars().length > 0">
            <li>
                <div>
                    <a href="#" data-bind="click: $parent.removeCar">Delete</a>
                    <label>Car Name:</label> <input data-bind="value: Name"/>
                    <label>Car Code:</label> <input data-bind="value: Code"/>
                </div>
            </li>
        </ul>
    </ul>
</ul>

如您所见,它没有什么棘手的。设置这一切最棘手的部分是知道要使用哪些数据绑定指令,并跟踪您在哪个上下文中实际使用的 ViewModel。 (当您使用在不同 ViewModel 上定义的不同添加/删除功能时,这一点很重要。)

就是这样。这是一个很好的测量结果解决方案的屏幕截图:

虽然它看起来很乱,但它完成了工作。 当您单击相应的添加按钮时,会添加汽车或司机。 单击 更新 按钮将组装整个树并将其发送回服务器,并在服务器上借助 ASP.NET MVC 的魔力将其转换为 POCO。

这里是代码的粘贴箱,因此您可以复制粘贴并亲自查看。您将不得不稍微摆弄一下 MVC 项目,但我相信您可以处理。

页面: http://pastebin.com/2aGkEHEN

控制器: http://pastebin.com/nZaufcpw

一个重要提示:

如果您真的想做这样的事情,您可能想要跟踪用户如何更改数据,而不是只获取整个更改的结构并覆盖旧数据。 我将尝试跟踪用户对树所做的更改,然后将某种 changeset 发送到服务器而不是整个结构,而不是我在示例中向您展示的幼稚方法。通过这种方式,您可以在事务中应用变更集并获得相同的结果,但带宽更少,一致性更高。

【讨论】:

  • 谢谢,这当然是完成它的一种方法。看起来您的控制器在这里非常用作 Web API。我有点希望可以使用 Razor 以 @Model 呈现初始视图并让 Knockout 管理隐藏的 MVC 索引器(如 @StephenMuecke 在他的 cmets 中提到的 &lt;input name="Drivers[2].Cars[1].Name" ... /&gt; 问题)。淘汰赛不是一个选项吗?也许还有其他框架?
  • @avo,你在说什么。此解决方案不是 WebAPI,它是 ASP.NET MVC,并且 Index 操作方法呈现初始视图,如果您想使用 Razor 而不是 KnockoutJS 来进行模型的初始渲染,而不是 KnockoutJS,您可以并且仍然拥有您的绑定到 KnockoutJS。此外,在此解决方案中,Knockout 正在管理隐藏的 MVC 索引器。
  • @BrianOgden,我承认我错过了这一点。我现在在调试器/F12 模式下玩它,我明白你的意思。我将尝试使用带有 KnockoutJS 绑定的 Razor 来渲染它,这确实是我想要的。
  • 我想我终于找到了我要找的东西:knockoutmvc.com/ContactsEditor。我相信@AndreyAkinshin 是作者之一。
  • 我使用的控制器有点像 WebAPI,因为它更容易在客户端构建树扩展逻辑,而不是使用 razor 在服务器端复制它,然后在客户端使用昏死。 knockoutmvc 是摆脱客户端步骤的一种方法。我更喜欢摆脱服务器端步骤,因为这是构建单页应用程序的方式。 (不过,你会使用 WebAPI,我只是将控制器用作演示)。
猜你喜欢
  • 2012-04-30
  • 1970-01-01
  • 2021-08-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多