4

I am working on an administrative panel that will allow a user to add one or many other users. There is a text area where the admin can input one or many user IDs to be added to the application, which corresponds to a user's ID assigned during the employment process. On submission, the application pulls name, email, etc from a database containing all employees and displays it on the screen for verification. Also included on the screen are some checkboxes to assign some permissions, such as CanWrite and IsAdmin.

View

using (Html.BeginForm())
        {
            <table>
                <tr>
                    <th>
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.User.First().ID)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.User.First().Name)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.User.First().Email)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.User.First().CanWrite)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.User.First().IsAdmin)
                    </th>
                </tr>

                @foreach (var item in Model.User)
                {
                    <tr>
                        <td>
                            <input type="checkbox" name="id" value="@item.ID" checked=checked/>
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.ID)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Name)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Email)
                        </td>
                        <td>
                            @Html.CheckBoxFor(modelItem => item.CanWrite)
                        </td>
                        <td>
                            @Html.CheckBoxFor(modelItem => item.IsAdmin)
                        </td>
                    </tr>
                }

            </table>
            <input type="submit" />
        }

NOTE: The reason for the checkbox with name ID is to give the ability to not add a user once the name and other information has been fetched, for example, a user you didn't mean to add got in the list of IDs by accident.

Model

public class User
{
    public int ID { set; get; }
    public string Name { set; get; }
    public bool IsAdmin { set; get; }
    public bool CanWrite { set; get; }
    public string Email{ set; get; }

}

Controller

[HttpPost]
public ActionResult Create(IEnumerable<User> model)
{
    //With this code, model shows up as null
}

In the case of a single user, I know I can use User model as the parameter in my controller action. How can I adjust this code to work with multiple users being added at once? Is this even possible?

Jeff
  • 2,283
  • 9
  • 32
  • 50

3 Answers3

1

I am only beginning ASP.NET MVC, but I guess you only need to create a view model class that has a property of type IEnumerable<User> where you store your user collection. And then pass that to your view and back to your POST handler.

meilke
  • 3,280
  • 1
  • 15
  • 31
1

Try something like this:

Model

public class Employee
{
    public IEnumerable<User> Employees { set; get; }
}

public class User
{
    public int ID { set; get; }
    public string Name { set; get; }
    public bool IsAdmin { set; get; }
    public bool CanWrite { set; get; }
    public string Email{ set; get; }
}

View

@model Employee

@using( Html.BeginForm()
{
    // build HTML table, not much different from yours
}

Controller

[HttpPost]
public ActionResult Create(Employee model)
{
    // model will have an IEnumerable property with the User info
}
Alex Costa
  • 306
  • 1
  • 10
1

You're on the right track, Jeff. You just need to expand on what you have a little.

Firstly, the reason why your model is null in your action is because of the way the input fields are named on your form. Your fields would render along the lines of this:

<input id="item_CanWrite" name="item.CanWrite" type="checkbox" value="true" />
<input name="item.CanWrite" type="hidden" value="false" />

Notice that name is item.CanWrite. The problem here is that because you're looping over all of the users in your model, you're essentially giving the same name to all of the input fields. That prevents MVC from being able to model bind to the POSTed data because it cannot distinguish between each User.

In order to fix this, you need to change your calls to Html.DisplayFor in order to allow MVC to process each User as a separate entity, thus allowing each User's set of fields to be named sequentially. This allows the model binder to properly bind to those fields. Like so:

@for (int i = 0; i < Model.Count(); i++)
{
    <tr>
        <td>
            <input type="checkbox" name="id" value="@Model[i].ID" checked=checked/>
        </td>
        <td>
            @Html.DisplayFor(m => m[i].ID)
        </td>
        <td>
            @Html.DisplayFor(m => m[i].Name)
        </td>
        <td>
            @Html.DisplayFor(m => m[i].Email)
        </td>
        <td>
            @Html.CheckBoxFor(m => m[i].CanWrite)
        </td>
        <td>
            @Html.CheckBoxFor(m => m[i].IsAdmin)
        </td>
    </tr>
}

If you check how those fields are rendered as HTML now, you'll see they look something like this:

<input checked="checked" data-val="true" data-val-required="The CanWrite field is required." name="[0].CanWrite" type="checkbox" value="true" />
<input name="[0].CanWrite" type="hidden" value="false" />
<input checked="checked" data-val="true" data-val-required="The CanWrite field is required." name="[1].CanWrite" type="checkbox" value="true" />
<input name="[1].CanWrite" type="hidden" value="false" />

As you can see, each User has its own separate index. I used a simple test setup with the following controller method, which picked up the correct number of records in my test model:

[HttpPost]
public ActionResult Index(IEnumerable<User> model)
{
    // Process your data.

    return View(model);
}

However, I'd like to suggest one further improvement here, in order to remove the logic that's in your view. To do that, we can use DisplayTemplates instead of loops:

  1. Create a DisplayTemplates folder inside your view's current folder (e.g. if your view is Home\Index.cshtml, create the folder Home\DisplayTemplates).
  2. Create a strongly-typed view in that directory with the name that matches your model (i.e. in this case the view would be called User.cshtml).
  3. Place the repeated logic in the view.

Your template should now look something like this (barring the namespace for wherever your User model resides):

@model User

<tr>
    <td>
        <input type="checkbox" name="id" value="@Model.ID" checked=checked/>
    </td>
    <td>
        @Html.DisplayFor(m => m.ID)
    </td>
    <td>
        @Html.DisplayFor(m => m.Name)
    </td>
    <td>
        @Html.DisplayFor(m => m.Email)
    </td>
    <td>
        @Html.CheckBoxFor(m => m.CanWrite)
    </td>
    <td>
        @Html.CheckBoxFor(m => m.IsAdmin)
    </td>
</tr>

Now, in your original view, you can replace all of the loop with this:

@using (Html.BeginForm())
{
    <table>
        @Html.DisplayForModel()
    </table>
    <input type="submit" value="Submit" />
}

If you'd like to know more about binding to collections, please see an answer I provided to another question.

Community
  • 1
  • 1
John H
  • 14,422
  • 4
  • 41
  • 74