Monday, January 6, 2014

MVC.NET - Editing a master-detail relationship on the same view

Introduction

Lately I’ve been playing around with MVC.NET, and as often happens, I found myself exploring fringe scenarios. One case in particular turned out to be rather aggravating – editing a master-detail relationship without using a plethora of different views, controllers and actions, and giving the user a nice integrated interface for the manipulation of said entity relationship all from the same browser window.

I thought I’d share its particulars and the final hack solutions I came up with.

Pre-requisites

The reader should have some familiarity with MVC.NET. I used version 4, but it should prove straightforward to generalize to newer versions. Rudimentary knowledge of javascript and jQuery is necessary. Some experience with MS Entity Framework and SQL server is also needed.

Defining the problem


For simplicity sake, let us consider the following master-detail model between the entity “user” and the entity “address”, as depicted above. Zero or more addresses may be assigned to a user. Each user and address entities are simplified to the maximum, each comprising of only a primary key and a text field with the name/description; additionally, an address has a non-null foreign key pointing to a user, as well as an “order” column establishing a priority within the user’s addresses.
Note: adding entropy, I opted to make the PKs type as integer autonumbers (identity) fields. Normally I’d always use GUIDs as primary keys (even though they introduce an indexing penalty as clustered indexes, it's usually worth it), but one doesn’t always have the luxury to design the database; instead it’s frequent having to write new user interfaces on top of pre-existing databases (this is also one of the reasons why I tend to favour the database-first paradigm for my EF model). Autonumbers for PK’s do have one advantage, though: we close the door to absurd solutions such as storing the address list on the database using a pre-generated GUID for the user’s PK even though the user is yet to be created/modified on the db (yes, I’ve seen it done again and again).
The pattern encouraged by MVC for editing such entities would be to handle each separately:
- Views for user list/details/edit/delete, and similarly for addresses.
- One controller for users and another for addresses.
- Each controller with as many actions as views.
All this results in a rather cumbersome experience for the end user. In this day and age, everyone is used to an ergonomic browsing experience, so the scenario above is obviously not the best choice.
It seems obvious that both entities should be handled simultaneously. While the listing of users, the detail of a user with its associated list of addresses, and the deleting of a user pose no significant technical problems, the editing/creating of a user and its associated addresses is an entirely different matter for which MVC.NET isn’t quite prepared for (at least not without significant amounts of hacking, as we’ll soon find out).
Let’s compound our difficulties by requiring that all the unobtrusive validation mechanisms (both client and server-side) from MVC.NET should be available and seamlessly integrate with our solution. In other words, if a property of an entity is decorated with a validation attribute ([Required], for instance), then it should just work – the proper validation will happen both on the server and the client, without writing any additional code.
From here on we’ll focus solely on the “user edit” action, since it contains all the major difficulties described above.

Designing the solution

I see two possible ways to solve our problem:

1 - Each step is processed server-side

Every operation (save user, add address, delete address, reorder address) on the “user edit” view will result in a trip to the server, each handled by a different action method. The user model “under construction” would be temporarily stored between HTTP requests on form fields, constantly re-bound to the master-detail model at the server, until it may finally be committed to the database.
Pros:
- Easier to implement in MVC, since the original order by which address rows are displayed will be preserved between client round-trips. The server will encounter on the POST collection exactly the same elements that it originally created and therefore will be able to rebind the POST’ed data into the model. This is essential when using lists of entities on our model. Therefore this approach won’t require as many ugly javascripthacks as the next solution.
Cons:
- Laxer adherence to MVC tenets.
- A lot of round-trips to the webserver and (for any serious commercial application) a lot of extra operations such as user authentication, session management, logging and BI reporting, all weighting on the SQL server.
- Posting to the server when adding/deleting/reordering addresses may result in validation errors being triggered, which isn’t appropriate since the user hasn’t yet saved the “user”. Client and server work-arounds are required to bypass this unwanted behaviour (and, if done incorrectly at the server-side, has the potential for abuse).

2 – Every step happens only client-side.

If instead we chose to do everything client-side, refusing to post to the server until we are 100% finished with editing our user, then we’ll save a lot of server time, particularly our SQL server, which is the least scalable element on our distributed solution. We’ll make extensive use of javascript on the client in order to add, reorder and remove addresses. Particular care must be taken in order not to break the client-side unobtrusive validation mechanism.
Pros:
- No needless trips to the server, particularly the SQL server. I find this to be of extreme importance.
Cons:
Javascript skulduggery.
- Laxer adherence to MVC tenets.

Implementation

Let’s start by marking both the UserName and AddressDesc properties as [Required]. This way we’ll be able to test whether the unobtrusive validation is working as expected.
Since the code for our model classes under EF will be regenerated whenever the model changes, we will instead decorate a corresponding metadata class which is associated with the model class using the following technique:

[MetadataType(typeof(AddressMetaData))]
public partial class Address
{
}

public class AddressMetaData
{
    [Required]
    public global::System.String AddressDesc { get; set; }
}


This way, we’ve just indirectly marked the property “AddressDesc” as “Required”. We can do the same for the “UserName” on the “User” class.

Configuration

Make sure to enable client-side validation on your application’s web.config:
<configuration>
  <appSettings>
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    ...

View Model

However, having the “User” entity class as generated by EF directly as our model class won’t do. We’ll need additional properties to handle the minutia of deleting and reordering addresses.
Specifically, we’ll need:
- A list of all the “address” PK’s to be deleted from the current “user”;
- A pair of the PK id and the new desired index position for reordering an “address”;
- And, strangely enough, a copy of the user’s EntityCollection<Address>, transformed into an IList<Address> implementation. Indeed, it turns out that MVC’s default model binder needs to have an indexer when mapping properties that are collections.
public class UserViewModel
{
    // a nice improvement here would be using a custom model property binder, turning a string of
    // comma-separated values into a List<int>. It would bring validation out-of-the-box.
    public virtual string addressesIdsToDelete { getset; }
    public virtual IEnumerable<intaddressesIdsToDeleteList
    {
        get
        {
            if (string.IsNullOrEmpty(addressesIdsToDelete))
                yield break;
            foreach (int i in
                addressesIdsToDelete.Split(',')
                .Where(f => f != "")
                .Select(f => Int32.Parse(f))
                .Distinct())
                yield return i;
        }
    }
    public virtual intMovedItemID getset; }
    public virtual intMovedItemNewIndex getset; }

    public virtual User user { getset; }
    public virtual IList<Address> addresses { getset; }
}


Parenthesis

At this point we must open a parenthesis to further explain the need for the IList property and how the default model binder deals with lists:
When using the “For” helper methods to format collection properties, the proper syntax is something like:
@for (int ix = 0; ix < Model.addresses.Count; ix++)
{
    <li>
    @Html.EditorFor(k => Model.addresses[ix].AddressDesc)
    </li>
}

By passing an expression as an argument, the helper method is able to not only evaluate the expression itself, but also to generate metadata identifiers for the underlying Html attributes, such as differentiated “name” and “id” attributes. Here’s a sample of the html generated for the code above:
<input id="addresses_2__AddressDesc" name="addresses[2].AddressDesc" type="text" value="d" />

This way, whenever the default binder needs to parse the POST data and fill up our model class, it will know exactly where to place each value in the list.
If our Model was a “User” class, straight from the EF code generator, we’d have a syntax error with “addresses[ix]”, since “EntityCollection<Address>” isn’t indexable.
We close our parenthesis and move on.

The view

Now we’re ready to write our editor’s view. As usual, we’ll divide it into two sections – the default body section, containing all our html content, and a scripts section which will contain exclusively all the javascript to be executed after the document is parsed and the javascript libraries have been loaded and initialized.
@model MvcApplication1.Models.UserViewModel

@{ ViewBag.Title = "Edit"}

<h2>Edit</h2>

@using (Html.BeginForm(nullnullFormMethod.Postnew { id = "myform" }))
{
    @Html.HiddenFor(m => m.addressesIdsToDelete)
    @Html.HiddenFor(m => m.MovedItemID)
    @Html.HiddenFor(m => m.MovedItemNewIndex)

    @Html.ValidationSummary(true)

    <fieldset>
        <legend>User</legend>
        
        @Html.HiddenFor(m => m.user.ID)

        <div class="editor-label">
            @Html.LabelFor(m => m.user.UserName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(m => m.user.UserName)
            @Html.ValidationMessageFor(m => m.user.UserName)
        </div>

        <fieldset>
            <legend>Addresses</legend>
            <ul id="AddressesList" style="list-style-type: none">
                @{ Html.RenderPartial("AddressEditor"); }
            </ul>
        </fieldset>

        <p>
            <input type="button" value="Add" id="addbtn" />
            <input type="button" value="Save" id="savebtn" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List""Index")
</div>

We’ve encapsulated the code that generates the list of editable addresses, since we’ll need to use it more than once:
@model MvcApplication1.Models.UserViewModel

@for (int ix = 0; ix < Model.addresses.Count; ix++)
{
    <li>
    <span style="cursor: move; background-color:Aqua" >Drag</span>
    @Html.HiddenFor(k => Model.addresses[ix].ID, new { PKStorage = "" })
    @Html.LabelFor(k => Model.addresses[ix].AddressDesc)
    @Html.EditorFor(k => Model.addresses[ix].AddressDesc)
    @Html.ValidationMessageFor(k => Model.addresses[ix].AddressDesc)
    <input type="button" value="Del" onclick="DeleteAddress(this);" />
    </li>
}


Notice how the add, delete and sort functionalities aren’t yet implemented. We’ll later hook them up to events on the scripts section.

The Action methods

Finally we are ready to write our two action methods, one for GET, where our user is loaded from the database and attached to our model object.
[HttpGet]
public ActionResult Edit(int id = 0)
{
    User user = db.Users.Where(u => u.ID == id).Include(j => j.Addresses).FirstOrDefault();
    if (user == null)
    {
        return HttpNotFound();
    }
    UserViewModel vm = new UserViewModel()
    {
        user = user,
        addresses = user.Addresses.OrderBy(f => f.Order).ToArray()
    };
    return View(vm);
}

Notice how:
- We make sure to load the address data together with the user’s data, to avoid needless trips to the SQL Server.
- We copy the address list into our model object, properly sorted.
And one action for POST, where, once the model is validated, we attach all the relevant entities onto the EF context and determine their desired state based on whether their PK values are negative (newly added), positive (modified) or belonging to the list of id’s to remove (deleted).
[HttpPost]
public ActionResult EditServer(UserViewModel vm)
{
    if (ModelState.IsValid)
    {
        db.Users.Attach(vm.user);
        db.ObjectStateManager.ChangeObjectState(vm.userEntityState.Modified);
        int orderIx = 0;
        if (vm.addresses == null)
            vm.addresses = new List<Address>();
        foreach (var add in vm.addresses)
        {
            add.Order = orderIx++;
            vm.user.Addresses.Add(add);
            // we'll use the convention that, when using IDENTITY PK's, they are 1-based
            // and we'll reserve the value PK<=0 to signify that its a newly inserted record.
            if (add.ID < 0)
                add.ID = 0;
            db.ObjectStateManager.ChangeObjectState(add, add.ID <= 0 ?
                EntityState.Added :
                EntityState.Modified);
        }

        foreach (int id in vm.addressesIdsToDeleteList.Where(f => f > 0))
        {
            Address add = new Address() { ID = id };
            db.Addresses.AddObject(add);
            db.ObjectStateManager.ChangeObjectState(add, EntityState.Deleted);
        }

        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(vm);
}

Case 1 - Each step is processed server-side

Additional Action methods

If we are to handle the delete/reorder/add of addresses at the server, the 1st thing we’ll need are the corresponding actions. Fortunately we can reuse the view for the editor, since the rendered output will exactly match.

[HttpPost]
public ActionResult EditServerAdd(UserViewModel vm)
{
    // needed, otherwise mvc's helper methods, seeing a POST request, will assume a view redraw
    // results from a validation error, reusing the modelState's values instead of our model
    // object.
    ModelState.Clear();

    // merely add a new address at the bottom of the list.
    if (vm.addresses == null)
        vm.addresses = new List<Address>();
    // we'll temporarily use a negative number as pk to identify newly inserted items. When we
    // are finally ready to insert them onto the database, we'll set those id's to zero, since
    // pk's are identity columns.
    // alternatively we could have added an auxiliary property to the address entity that would
    // handle these cases.
    vm.addresses.Add(new Address()
    {
        // make sure our negative id is also unique
        ID = (vm.addresses.Count == 0 ? 0 : (Math.Min(0, vm.addresses.Min(f => f.ID) - 1))),
        // inserted at the end of the address list
        Order = (vm.addresses.Count == 0 ? 0 : (vm.addresses.Max(f => f.Order) + 1)),
        AddressDesc = ""
    });

    return View("EditServer"vm); // we can reuse our edit view
}

[HttpPost]
public ActionResult EditServerReorder(UserViewModel vm)
{
    // needed, otherwise mvc's helper methods, seeing a POST request, will assume a view redraw
    // results from a validation error, reusing the modelState's values instead of our model
    // object.
    ModelState.Clear();

    // remove the item from the list, and insert it in its new location
    Address MovedItem = vm.addresses.First(f => f.ID == vm.MovedItemID);
    vm.addresses.Remove(MovedItem);
    vm.addresses.Insert(vm.MovedItemNewIndex.ValueMovedItem);

    // match Order property with the list's new intrinsic order
    int currIx = 0;
    foreach (Address add in vm.addresses)
        add.Order = currIx++;

    return View("EditServer"vm); // we can reuse our edit view
}

[HttpPost]
public ActionResult EditServerDelete(UserViewModel vm)
{
    // needed, otherwise mvc's helper methods, seeing a POST request, will assume a view redraw
    // results from a validation error, reusing the modelState's values instead of our model
    // object.
    ModelState.Clear();

    // remove from the address list all whose pk's match the ones marked for deletion
    vm.addresses = vm
        .addresses
        .Join
        (
            vm.addresses.Select(f => f.ID).Except(vm.addressesIdsToDeleteList),
            f => f.ID,
            g => g,
            (f, g) => f
        )
        .ToList();

    return View("EditServer"vm); // we can reuse our edit view
}


Notice how:
- We don’t check the model’s validation rules, since we aren’t yet trying to change the state of the user on the database.
- We clear the model state. This is necessary due to a feature of the MVC’s design pattern: Whenever a POST action fires and the corresponding view ends up being re-rendered, the “For” helper methods will assume the form is being rendered due to a validation error and, therefore, will first try to use the values stored in the ModelState to generate the values for the html input fields. Only if the state is blank will the helper methods use our model object as a fall-back. Although this makes sense on a typical scenario when a POST request is associated with an attempt to change the model state, it isn’t really what we want in these special circumstances.

The scripts section

Now that we have our additional Action methods, all we’re left with is writing the scripts section and hook up some methods which will trigger our MVC Actions.
We’ll need an array which will hold the list of addresses marked to be deleted:
@section scripts
{
    <script type="text/javascript">

    // this list will hold the PK's of all the addresses that were removed from the UI. Will be used
    // to delete them from the db.
    var deletedItemIDs = '@Model.addressesIdsToDelete'.split(',');

Adding an address (Notice how we’re suppressing the form’s jQuery client validation, since we want to be able to add a new address to the bottom of the list even if some fields aren’t yet properly filled up):
    function AddAddress()
    {
        var form = $('#myform');
        form.attr('action''@Url.Action("EditServerAdd")');
        form.validate().cancelSubmit = true// necessary in order to reset the client-side validator
        form.submit();
    }

Deleting an address (we’re retrieving the deleted address id from the DOM and storing it on our array variable, before submitting the form towards our delete Action):
    function DeleteAddress(item)
    {
        var parent = $(item).parent();
        // read the original PK value from the element that is to be deleted, and store it on a list
        // to be subsequently processed at the server.
        deletedItemIDs.push(parent.find('input[PKStorage]').val());
        // submit the form to the proper action
        var form = $('#myform');
        form.attr('action''@Url.Action("EditServerDelete")');
        form.validate().cancelSubmit = true// necessary in order to reset the client-side validator
        form.submit();
    }


Reordering an address. Of the lot, this method is the most complex, simply because we have to discover both the id and the new index of the address being rearranged. Once we have those two values, we store them on the model’s corresponding placeholder attributes.
    function ChangeAddressesOrder(event, ui)
    {
        var item = ui.item.get(0);
       
        // iterate on every address row and find our item's new location. The ui helper object's
        // other properties won't help in this in a reliable manner
        var MovedItemNewIndex = 0;
        $('#AddressesList').children().each(function (addressIx, address)
        {
            if (address == item)
                MovedItemNewIndex = addressIx;
        });
        // get the moved item's pk
        var MovedItemID = ui.item.find('input[PKStorage]').val();
        // store data on form post
        $('#MovedItemID').val(MovedItemID);
        $('#MovedItemNewIndex').val(MovedItemNewIndex);
        // submit the form to the proper action
        var form = $('#myform');
        form.attr('action''@Url.Action("EditServerReorder")');
        form.validate().cancelSubmit = true// necessary in order to reset the client-side validator
        form.submit();
    }

The “save” button, which completes the process and stores the modified user model onto the database:
    function PostForm()
    {
        // submit the form to the proper action
        var form = $('#myform');
        form.attr('action''@Url.Action("EditServer")');
        form.submit();
    }

Finally, we attach all these handlers to their respective UI elements:
    $(function ()
    {
        $('#AddressesList').sortable(
        {
            // reordering address rows affects their indexes, so we need to update them.
            updatefunction (event, ui) { ChangeAddressesOrder(event, ui); } 
        });
        $('#addbtn').click(function () { AddAddress(); });
        $('#savebtn').click(function () { PostForm(); });
        $('#myform').submit(function (event)
        {
            // pack list of deleted items into a string of comma-separated values.
            $('#addressesIdsToDelete').val(deletedItemIDs.join(','));
        });
    });
</script>}

Case 2 – Every step happens only client-side.

If we’re feeling adventurous, we can try to discard entirely our constant server accesses and do everything client-side, except of course for the final user update on the db.

Under the hood

But first we need to have a good look at the html generated by the view helpers for each address rendered. Here’s an example:
<li>
    <span style="cursor: move; background-color:Aqua" >Drag</span>
    <input PKStorage="" data-val="true" data-val-number="The field ID must be a number." data-val-required="The ID field is required." id="addresses_1__ID" name="addresses[1].ID" type="hidden" value="40" />
    <label for="addresses_1__AddressDesc">AddressDesc</label>
    <input class="text-box single-line" data-val="true" data-val-required="The AddressDesc field is required." id="addresses_1__AddressDesc" name="addresses[1].AddressDesc" type="text" value="my address desc here" />
    <span class="field-validation-valid" data-valmsg-for="addresses[1].AddressDesc" data-valmsg-replace="true"></span>
    <input type="button" value="Del" onclick="DeleteAddress(this);" />
</li>

There’s a lot of “addresses[n]” and “addresses_n__” being generated, indexed by the address’s position within its containing collection. These are needed both by the unobtrusive validator and the default model binder, therefore we should be careful to preserve this syntax.
Then, if we were to change the order of every <li/> element within its containing list, or add, or remove one such block from the list, we’d somehow need to “fix” this numbered syntax so as to match the perceived order of each address within the list. We can do so in javascript relatively easily, as we’ll soon see.
Such technique isn’t without its shortcomings. By second-guessing the syntax generated by the helpers, we not only risk breaking compatibility with future versions of MVC but also raise the spectre of potential unanticipated bugs due to some particularly contrived combination of helper elements and validation rules (one example would be a conditional validation, where the validation rule of one element would depend on the state of another, such as an editBox being validated only if a checkbox is checked). This is the price we have to pay for greater flexibility.

No need for Action methods

The first thing to do is get rid of our spurious Actions “EditServerAdd”, “EditServerDelete” and “EditServerReorder”; they’ve become redundant due to our new all-client approach.

The scripts section

The second and final step is to completely rewrite our scripts section. Everything else remains unchanged.
We begin the section by storing a “template” of an address list item, used whenever we wish to insert a new address into our list. We’ll use a <script/> element to store the html, setting its type to “text/html” in order to avoid having it interpreted by the browser. It’s a perfectly safe technique, as long as we don’t output the closing </script> tag along our html block. We’ll use the partial view we created previously to render a default blank address object model. This way we don’t risk a conflict of repeated “id” attributes within our html document.
@section scripts
{
    <script id="vmTemplate" type="text/html">@{
        // A hiden template for a row of the address editor. Not part of the form, so it won't be
        // posted to the server. it's a good idea to place it inside a custom typed script
        // element, otherwise there might be collisions with the template's id attributes and
        // those from the actual address editor. just make sure the partial view doesn't use a
        // closing script tag.
        MvcApplication1.Models.UserViewModel vmTemplate = new MvcApplication1.Models.UserViewModel()
        {
            user = new MvcApplication1.Models.User(),
            addresses = new MvcApplication1.Models.Address[]
            {
                new MvcApplication1.Models.Address() { AddressDesc = "" }
            }
        };
        Html.RenderPartial("AddressEditor"vmTemplate);
    }</script>

Again we start by declaring an array which will hold the list of addresses to be deleted:
    <script type="text/javascript">

    // this list will hold the PK's of all the addresses that were removed from the UI. Will be used
    // to delete them from the db.
    var deletedItemIDs = new Array();


Now we’ll write the greatest hack of them all: a function that will parse the list of addresses within the DOM and reset their attributes, matching the patterns discussed previously, making sure they respect the perceived display order of the address list.
    // helper function
    function startsWith(str, prefix)
    {
        return (str.substring(0, prefix.length) === prefix);  // same as using "indexOf", but faster
    }

    // Since the operations of deleting, adding or reordering addresses within the address editor
    // will shuffle their respective indexes, we are forced to use client-side scripting to fix it.
    // Every address row in the addresses editor will be iterated and the underlying order will
    // be enforced upon the attributes of every element, regardless of their type.
    // This will ensure that the default property binder will properly populate the collection of
    // addresses on the view's model.
    function ResetAddressesOrder()
    {
        var form = $('#myform');
       
        // necessary in order to reset the client-side validator
        form.removeData('validator');
        form.removeData('unobtrusiveValidation');

        // iterate on every address row
        $('#AddressesList').children().each(function (addressIx, address)
        {
            // for a given address, scan each descendent element
            $(address).find('*').each(function (elemIxelem)
            {
                var item = $(elem);
                $.each(elem.attributesfunction (i, attrib)
                {
                    // we are now scanning each attribute of the given element. Specifically, we're
                    // loking for "id"'s, "name"'s, "for"'s and "data-*" attributes, since these
                    // are the ones that may depend on the address index.
                    // even though we're scanning every attribute of every element within our address
                    // editor, the performance impact is negligible.
                    var name = attrib.name.toLowerCase();
                    if (name == 'id' || name == 'for')
                        item.attr(name, attrib.value.replace(
                            /addresses_\d+__/g,
                            'addresses_' + addressIx + '__'));
                    // alas, working with jquery's "data" functionality won't do here, since values
                    // are stored in an internal object model instead of on the actual dom.
                    else if (name == 'name' || startsWith(name, 'data-'))
                        item.attr(name, attrib.value.replace(
                            /addresses\[\d+\]/g,
                            'addresses[' + addressIx + ']'));
                });
            });
        });

        // necessary in order to reset the client-side validator
        $.validator.unobtrusive.parse(form);
    }

Basically we loop each of the addresses in our list, and for each, we scan their DOM looking for elements whose “id”, “name”, “data-*” or “for” attributes exist and match one of the search patterns discussed previously. Whenever found we replace their value in order to respect the list’s display order. It might seem that it would be slow operation, but that’s actually not the case – it’s linear on the number of elements within the DOM sub-tree, and the regular expressions are applied only once other faster verifications have checked.
Notice how we reset and re-parse the validator on our form, since whichever id references they might have had have now all been rearranged.

Now all that remains is to hook up the methods which will trigger our MVC Actions.
Adding an address (we extract the html of our template and add it to the address list, making sure we reset the order of the address list once we’ve modified the DOM):
    function AddAddress()
    {
        $('#AddressesList').append($('#vmTemplate').html());
        ResetAddressesOrder();
    }

Deleting an address (we simply remove from the list the <li/> containing the address, and reset the address list order):
    function DeleteAddress(item)
    {
        var parent = $(item).parent();
        // read the original PK value from the element that is to be deleted, and store it on a list
        // to be subsequently processed at the server.
        deletedItemIDs.push(parent.find('input[PKStorage]').val());
        parent.remove();
        ResetAddressesOrder();
    }

Reordering an address (this implementation has become the most trivial – since the address list item is already in the correct position, we simply have to reset our list order):

    function ChangeAddressesOrder(event, ui)
    {
        ResetAddressesOrder();
    }

The “save” button, again quite trivial:
    function PostForm()
    {
        $('#myform').submit();
    }

As before, we attach all these handlers to their respective UI elements:
    $(function ()
    {
        $('#AddressesList').sortable(
        {
            // reordering address rows affects their indexes, so we need to update them.
            updatefunction (event, ui) { ChangeAddressesOrder(event, ui);  } 
        });
        $('#addbtn').click(function () { AddAddress(); });
        $('#savebtn').click(function () { PostForm(); });
        $('#myform').submit(function (event)
        {
            // pack list of deleted items into a string of comma-separated values.
            $('#addressesIdsToDelete').val(deletedItemIDs.join(','));
        });
    });
</script>}


Final remarks

Frankly, it’s disappointing. One would thing that such an obvious scenario would have been contemplated by MS while designing its MVC framework. In both solutions I had to take measures that I’d easily qualify as not just “questionable” but outright “hackacrazy”.
Am I missing something terribly obvious here? Leave your suggestions on the remarks section below. Thanks for reading!