2

This is a newbie question from a person learning ASP.NET MVC 5. I'd like to create a web app screen where user can edit a tree-like data structure. I'm not sure how do it properly, while following the best practices.

For simplicity, let's say it's a two-level data structure, a list of drivers where each driver can drive multiple cars:

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

I need to be able to add/remove/edit drivers (and their cars) on the same screen. Does it mean every single change user makes (e.g., adding a new car, then changing its Code) should require a form submission (i.e., a hit to my controller) and sending back a new view to the client?

If so, do I need to use an MVVM framework (like Angular or Knockout) if I want to allow multiple model changes on the client side, before submitting all the updates back to my controller? Or is something that can be done with bare ASP.NET MVC?

Note I don't want a single-page web applications, I just want to cache user updates on the client side, before doing an HTTP post operation.

UPDATE, so this web app currently works in classic MVC way: first a view with the list of drivers, then a view with the list of cars, then a single car. There's a separate HTTP request to render each view.

What I want is to let user edit the whole structure inside the same view, then hit Update button. At that moment, I expect an updated POCO model for the whole data structure to be submitted and made available inside my MVC controller. I'd like a client-side JavaScript framework (Knockout, Angular, Aurelia, etc) to handle generating and updating the DefaultModelBinder indexers for me, so I wouldn't have to manage the indexers manually (i.e., <input name="Drivers[2].Cars[1].Name" ... /> etc, more details in this q/a).

The bounty will be rewarded to an answer illustrating how to do just this, with relevant sample code.

Community
  • 1
  • 1
avo
  • 10,101
  • 13
  • 53
  • 81
  • 1
    Some options for dynamically adding (and removing) collection items in the answers [here](http://stackoverflow.com/questions/29161481/post-a-form-array-without-successful/29161796#29161796) and [here](http://stackoverflow.com/questions/28019793/submit-same-partial-view-called-multiple-times-data-to-controller/28081308#28081308) –  Feb 09 '16 at 02:56
  • @StephenMuecke, do I understand it right that I need to generate correct hidden indexers in HTML (and update them as user is editing data in the form)? Could you pls point me to some docs explaning how the model POCO data gets assembled back from HTML, before it is sent back to the controller, and how this can be customized? Thank you. – avo Feb 09 '16 at 03:23
  • 1
    Yes that's correct. Collection indexers must start at zero and be consecutive unless you add the hidden field for the `Index` property which allows the `DefaultModelBinder` to match up non-consecutive indexers. –  Feb 09 '16 at 03:27
  • 1
    Think about it this way - how would you get the `Name` property of 2nd `Car` in the 3rd `Driver` of your model` - it would be `string name = model.Drivers[2].Cars[1].Name` - now strip the `model.` prefix and that is what the `name` attribute must be in order to bind - i.e. `` –  Feb 09 '16 at 03:32
  • 1
    jQuery In Action by Bear Bibeault has a nice chapter on building a client side DVD collection. Or angular's todo application demos, they really are not a full SPA, just using angular to manage binding to your POCO collection. You can still POST your POCOs using a form post. – JohnWrensby Feb 09 '16 at 04:14
  • @JohnWrensby, thanks. Is there any ASP.NET friendly client-side framework out there which supports MVC-style bindings like `` from Sephen's comment above? It has to be, this scenario seems to be so common... – avo Feb 09 '16 at 05:19
  • 1
    @avo, knockout. Just the binding parts. Low learning curve. Plenty of tutorials on web of using it with MVC. – JohnWrensby Feb 09 '16 at 06:50
  • @avo if possible to use javascript & jquery, then use jqGrid. It will help you build tree like structure and also it has features for add/edit inbuilt. you just need to pass model(list) to jqGrid. – Hayat Sama Feb 09 '16 at 12:16
  • 1
    @Hayat, I'd love an example of that (and knockout as well), to see how well it plays with MVC controllers. I'm really looking for a client-side framework that automatically maintains correct MVC binding indexers for complex POCO models. I'm happy to award a decent bounty to an answer with a sample code illustrating this. – avo Feb 09 '16 at 12:53
  • Is that code working? Seeing List of Cars but not of Car? – Falco Alexander Feb 10 '16 at 14:48
  • 1
    @FalcoAlexander, it's working in classic MVC way: first a view with the list of drivers, then a view with the list of cars, then a single car. There's an separate HTTP request to render each view. What I want is let user edit the whole structure in the same view, then hit Update and get the whole new updated model in my MVC controller. I want the client-side framework to generate and update indexers for me, so I don't have to do it manually. – avo Feb 10 '16 at 22:03
  • @avo Please refer this. this might help you to achieve your goal. http://www.trirand.com/jqgridwiki/doku.php?id=wiki:subgrid_as_grid – Hayat Sama Feb 12 '16 at 07:21
  • 1
    If your wanting to do this using knockout (as opposed to using a html template or the `BeginCollectionItem` helper as per my previous links), refer [Editing a variable-length list, Knockout-style](http://blog.stevensanderson.com/2010/07/12/editing-a-variable-length-list-knockout-style/) –  Feb 15 '16 at 00:29

1 Answers1

2

So the thing you wanted us to help you with required quite a bit of time to build, but was actually extremely simple (To be fair, there was enough info in most basic KO tutorials to do all this.)

So I've built one page and one MVC controller with 3 methods: One for the page itself and two for GETting or POSTing data.

Here is the controller's code:

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

As you can see, the controller is very barebones and largest method of them all is GetDriversModel() which priovides the page with sample data to work with.

Here you would probably do something like querying your long-term storage for a tree to render on a client side. It would most likely be marked with an Id of some sort, but since those details were not in your question, I've ommitted them. You can easily figure it out with this example.

The most interesting part is actually on the page where I've used knockout to build a renderer for the DriversModel data structure. First, lets check the JavaScript:

In the heart of a KO view is a 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);
        });
    }

In it we define a couple of methods for adding/removing the drivers from the tree as well as the method to post contents back to the server. The ViewModel is pretty straight forward (as pretty much everything in this example). Note that I've added another random driver to the list right after JS ahs done querying the server for data. This is done just for the fun of it (I was experimenting as well).

Here are ViewModels for the remainder of Entities:

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

As you can see all of those have some initialization logic inside them where we map the JSON objects that we got from our server to our client-side abstractions. You can also notice that they don't follow naming conventions for JavaScript objects. I've done this deliberately so that MVC has no problem when mapping them to C# object when we are done working with them on the client side. This might be not a very good thing to do in a long term but it will do for the sake of simplicity.

So what basically happens is when our DriversViewModel has requested items from server it maps all of the data to Knockout-friendly abstractions which are being tracked by the Knockout as they get changed. All that remains is to actually tell Knockout to use this ViewModel:

    ko.applyBindings(new DriversViewModel());

Now knockout is ready to work with those objects and it's time for us to build the UI part.

The page that uses those KO bindings looks like that:

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

As you can see there is nothing tricky about it. The most tricky part about setting this all up is to know which data-binding directives to use and to keep track of which ViewModel you are actually using in which context. (This is important when you are using different adding/removal functions defined on different ViewModels.)

And that's it. Here is a screenshot of the resulting solution for a good measure: Screenshot of the resulting page

And while it looks pretty messy, it gets the job done. When you click respective Add buttons, cars or drivers get added. Clicking an Update button will assemble the entire tree and post it back to server, where it is getting converted to POCO with the power of ASP.NET MVC magic.

Here are pastebins of the code so that you can just copy-paste and see it for yourself. You'll have to fiddle with MVC project a bit but I believe that you can handle that.

Page: http://pastebin.com/2aGkEHEN

Controller: http://pastebin.com/nZaufcpw

One important note:

If you really wanted to do something like this, you might want to track how user changes the data rather then just get the entire changed structure and overwrite the old data. Instead of the naiive approach I've shown you in the example, I would try and track the changes that user does to the tree and then send some kind of changeset to the server instead of the whole structure. This way you can apply the changeset in a transaction and achieve the same result but with less bandwidth and more consistency.

MarengoHue
  • 1,789
  • 13
  • 34
  • Thank you, this is certainly one way of getting it done. It appears though your controller is used here very much as a Web API. I kinda hoped it would be possible to use Razor to render the initial view with `@Model` and **let Knockout manage the hidden MVC indexers** (like ``, mentioned by @StephenMuecke in his comments to the question). Is it not an option with Knockout? Any other framework, perhaps? – avo Feb 11 '16 at 21:27
  • @avo, what are you talking about. This solution is not WebAPI, it is ASP.NET MVC and the Index action method renders the initial view, if you want to use Razor instead of KnockoutJS to do the initial rendering of your Model, instead of KnockoutJS, you can and still have your binding to KnockoutJS. Also, in this solution, Knockout is managing the hidden MVC indexers as you put it. – Brian Ogden Feb 11 '16 at 21:59
  • @BrianOgden, I admit I've missed that. I'm playing with it now in the debugger/F12 mode and I see what you mean. I'm going to try to render it with Razor with KnockoutJS bindings, that's what I want indeed. – avo Feb 11 '16 at 23:34
  • I think I've finally found what I was looking for: http://knockoutmvc.com/ContactsEditor. I believe @AndreyAkinshin is one of the authors. – avo Feb 12 '16 at 00:18
  • I've used the controller kinda like a WebAPI because it is much easier to build the tree expansion logic on the client side rather than duplicating it on the server side with razor and then on the client side with knockout. knockoutmvc is one way to get rid of the client-side step. I preffered to get rid of server-side step because this is how you would build a Single Page App. (You would use WebAPI though, I was using the controller just as a demonstration). – MarengoHue Feb 12 '16 at 05:43
  • I believe that all of the actions performed on a tree with knockout mvc require a postback after something major (like adding or deleting an item) happens. – MarengoHue Feb 12 '16 at 05:49
  • @MokonaModoki, it's a good answer deserving the bounty. Let's just let the question to linger for a little longer. – avo Feb 12 '16 at 10:27