Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make an Edit Form for an object with a List property in ASP.NET MVC 4 with Razor

Tags:

asp.net-mvc

I have an Edit page for my MVC application, using Razor.

I have a Model like:

public class MyModelObject
{
    public int Id { get; set; }

    public string Name { get; set; }

    public string Description { get; set; }

    public List<MyOtherModelObject> OtherModelObjects { get; set; }
}

And MyOtherModelObject looks like:

public class MyOtherModelObject
{
    public string Name { get; set; }

    public string Description { get; set; }
}

I am making Edit page for MyModelObject. I need a way to add space to the form on the Edit page for MyModelObject for the user to create/add as many MyOtherModelObject instances as the user wishes to the List of OtherModelObjects.

I'm thinking the user can hit a button, that will do ajax to another action which returns a PartialView of form elements (with no form tag since this is intended to part of the form on my edit page). When the user has added all the MyOtherModelObjects they want and filled out the data, they should be able to save their edits to the existing MyModelObject, that will HttpPost to the Edit action and hopefully all the MyOtherModelObjects will be in the correct list.

I also need the user to be able to re-order the items once they've added them.

Does anyone know how to make this work? Have sample project, or online sample walkthrough with this solution implemented?

like image 898
David Hollowell - MSFT Avatar asked Sep 11 '12 20:09

David Hollowell - MSFT


1 Answers

This blog post contains a step by step guide illustrating how to achieve that.


UPDATE:

As requested in the comments section I am illustrating step by step how to adapt the aforementioned article to your scenario.

Model:

public class MyOtherModelObject
{
    public string Name { get; set; }
    public string Description { get; set; }
}

public class MyModelObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public List<MyOtherModelObject> OtherModelObjects { get; set; }
}

Controller:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyModelObject
        {
            Id = 1,
            Name = "the model",
            Description = "some desc",
            OtherModelObjects = new[]
            {
                new MyOtherModelObject { Name = "foo", Description = "foo desc" },
                new MyOtherModelObject { Name = "bar", Description = "bar desc" },
            }.ToList()
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyModelObject model)
    {
        return Content("Thank you for submitting the form");
    }

    public ActionResult BlankEditorRow()
    {
        return PartialView("EditorRow", new MyOtherModelObject());
    }
}

View (~/Views/Home/Index.cshtml):

@model MyModelObject

@using(Html.BeginForm())
{
    @Html.HiddenFor(x => x.Id)
    <div>
        @Html.LabelFor(x => x.Name)
        @Html.EditorFor(x => x.Name)
    </div>
    <div>
        @Html.LabelFor(x => x.Description)
        @Html.TextBoxFor(x => x.Description)
    </div>
    <hr/>
    <div id="editorRows">
        @foreach (var item in Model.OtherModelObjects)
        {
            @Html.Partial("EditorRow", item);
        }
    </div>
    @Html.ActionLink("Add another...", "BlankEditorRow", null, new { id = "addItem" })

    <input type="submit" value="Finished" />
}

Partial (~/Views/Home/EditorRow.cshtml):

@model MyOtherModelObject

<div class="editorRow">
    @using (Html.BeginCollectionItem("OtherModelObjects"))
    {
        <div>
            @Html.LabelFor(x => x.Name)
            @Html.EditorFor(x => x.Name)
        </div>
        <div>
            @Html.LabelFor(x => x.Description)
            @Html.EditorFor(x => x.Description)
        </div>
        <a href="#" class="deleteRow">delete</a>
    }
</div>

Script:

$('#addItem').click(function () {
    $.ajax({
        url: this.href,
        cache: false,
        success: function (html) {
            $('#editorRows').append(html);
        }
    });
    return false;
});

$('a.deleteRow').live('click', function () {
    $(this).parents('div.editorRow:first').remove();
    return false;
});

Remark: the BeginCollectionItem custom helper is taken from the same article I've linked to, but I am providing it here for completeness sake of the answer:

public static class HtmlPrefixScopeExtensions
{
    private const string idsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";

    public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
    {
        var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
        string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

        // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
        html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

        return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
    }

    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
    {
        return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
    {
        // We need to use the same sequence of IDs following a server-side validation failure,  
        // otherwise the framework won't render the validation error messages next to each item.
        string key = idsToReuseKey + collectionName;
        var queue = (Queue<string>)httpContext.Items[key];
        if (queue == null)
        {
            httpContext.Items[key] = queue = new Queue<string>();
            var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
            if (!string.IsNullOrEmpty(previouslyUsedIds))
                foreach (string previouslyUsedId in previouslyUsedIds.Split(','))
                    queue.Enqueue(previouslyUsedId);
        }
        return queue;
    }

    private class HtmlFieldPrefixScope : IDisposable
    {
        private readonly TemplateInfo templateInfo;
        private readonly string previousHtmlFieldPrefix;

        public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
        {
            this.templateInfo = templateInfo;

            previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
            templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
        }

        public void Dispose()
        {
            templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
        }
    }
}
like image 104
Darin Dimitrov Avatar answered Oct 18 '22 05:10

Darin Dimitrov