0

I am building a simple web application where users can vote on topics that the admins set. Each topic, can have two or more options, for example a Yes or No style vote topic, will have two options, where as a voting topic of "What is the best restaurant in Kensington?" can have many options.

Here is my current ViewModel:

public class NewTopicVM
{
    [Required]
    [Display(Name = "Topic Title")]
    public string TopicTitle { get; set; }

    [Required]
    [Display(Name = "Topic Description")]
    public string TopicDescription { get; set; }

    [Display(Name = "Topic Image")]
    public byte[] TopicImage { get; set; }
    public List<NewOptionVM> TopicOptions { get; set; }
}

public class NewOptionVM
{
    public string OptTitle { get; set; }
    public string OptDescription { get; set; }
}

Note the list of NewOptionVM in the NewTopicVM. Thanks Iko's answer, I have managed to bind the non-dynamically generated inputs, however my dynamically generated ones are not getting bound for some reason !

Here's the jQuery code that creates the Option input fields:

var index = 1;

function generateOptMarkup() {
    var container = $('<div />');

    var optionInputsBlock = $('<div />');
    optionInputsBlock.addClass('optionInputsBlock');

    var inputTitleBlock = $('<div />');
    inputTitleBlock.addClass('inputBlock');

    var inputDescBlock = $('<div />');
    inputDescBlock.addClass('inputBlock');

    var optTitleLbl = $('<label />');
    optTitleLbl.attr('for', 'TopicOptions_' + ++index + '__OptTitle');
    optTitleLbl.text('Option Title');

    var optDescLbl = $('<label />');
    optDescLbl.attr('for', 'TopicOptions_' + index + '__OptDescription');
    optDescLbl.text('Option Description');

    var optTitleInpt = $('<input type="text" />');
    optTitleInpt.addClass('text-box single-line');
    optTitleInpt.attr('name', 'TopicOptions_' + index + '__OptTitle');
    optTitleInpt.attr('id', 'TopicOptions_' + index + '__OptTitle');
    optTitleInpt.attr('data-val-required', 'The Option Title field is required.');
    optTitleInpt.attr('data-val', 'true');

    var optDescInpt = $('<input type="text" />');
    optDescInpt.addClass('text-box single-line');
    optDescInpt.attr('name', 'TopicOptions_' + index + '__OptDescription');
    optDescInpt.attr('id', 'TopicOptions_' + index + '__OptDescription');
    optDescInpt.attr('data-val-required', 'The Option Description field is required.');
    optDescInpt.attr('data-val', 'true');

    inputTitleBlock.append(optTitleLbl).append(optTitleInpt);
    inputDescBlock.append(optDescLbl).append(optDescInpt);

    optionInputsBlock.append(inputTitleBlock).append(inputDescBlock);

    container.append(optionInputsBlock);

    return container.html();
}


$('div.buttons > a').click(function () {
    $('div.topicOptionsBlock').append(generateOptMarkup());
});

I start at Index = 1 because I already have 2 options displayed. Here is a screenshot:

enter image description here

The first two boxes get bound fine, but the third one which is dynamically added after clicking the [Add Option] button does not!

What am I doing wrong? I have ensured the indexing is correct, yet it isn't working :(

1 Answer 1

2

Loop through the list with a for loop and use the index and editorfor.

// other fields as usual

For(int i = 0; i < Model.TopicOptions.Count; i++)
{
    @Html.EditorFor(m => m.TopicOptions[i].OptTitle)
    @Html.EditorFor(m => m.TopicOptions[i].OptDescription)
}

MVC default binding will catch them now. If you examine the HTML MVC has some format to bind them so it's like TopicOptions__0_OptTitle.

Put a breakpoint on the action, the model should be bound completely, you don't need to manually do anything.

Edit. You can put the inside of the for loop (the 2 editorFors) in a PartialView and then when you call it pass the index via ViewData.

for(...)
    @Html.Partial("_NewOption", new ViewDataDictionary { { "index", i } })

Now, if you add new items dynamically, you can make an ajax call to an action to return a list with the last item being a new object. The action should return the index, and then the binding will be ok as the name and id will be generated automatically. (You can also send everything to the server to delete)

It just depends if you want to handle the id/name fields manually in javascript, or automatically from the server.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks Iko your code worked for non dynamic inputs, but not for the dynamic ones, see updated Q.
never mind, I was making a silly mistake. My name attribute on the inputs needed to be of the following format >> TopicOptions[2].OptTitle
Ok, good you figured it out. Using a list of objects requires the unique index for each item. It gets a little tricky adding more items dynamically because you have to ensure you are adding incrementing indexes. If you delete one item in the middle you also have to adjust the indexes (iirc).
Thanks I hadn't thought about fixing the indexes when one input is removed. Is there an easy way of doing that?
Unfortunately there isn't an easy way to do it, you'll have to process them with JavaScript (parse for indexes in the name and ID and decrement them etc.) or pass the whole list to the server and delete the deleted item from the list. If the objects are complex it's probably easier to do it at the server (remove one item from the list) vs. parse all of the items in the list and their properties in JS.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.