2

I am fairly new to MVC5 and C# and I am trying to achieve something that I don't fully understand.

I have a Team Model such as this:

public class Team
{
    [Key]
    public Guid ID { get; set; }
    public string TeamName { get; set; }
    public string Coach { get; set; }
    public string Conference { get; set; }
}

I also have a Player Model such as this:

public class Player
{
    [Key]
    public Guid Id { get; set; }
    [ForeignKey("Teams")]
    public Guid TeamId { get; set; }
    public string Name { get; set; }
    public virtual Team Teams { get; set; }
}

View Model is

public class TeamViewModel
{
    public string TeamName { get; set; }
    public string Coach { get; set; }
    public string Conference { get; set; }
    public List<Player> Players { get; set; }
}

With this structure, you are suppose to be able to add and infinite number of players to each team. As such I have a Teams table with few properties and a Player table that contains the player name as well as the player TeamId so that we know to what team they belong.

My problem comes when I am creating a team. I have Create Controller such as this:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(TeamViewModel model)
{
    if (ModelState.IsValid)
    {
        var team = new Team { TeamName = model.TeamName, Coach = model.Coach, Conference = model.Conference, Player = model.Player };
        db.Teams.Add(team);
        var result = await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View();
}

And my View is as follows:

@model SoccerTeams.Models.ViewModels.TeamViewModel
@{
    ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">
        <h4>Team</h4>
        <hr />
        @Html.ValidationSummary(true, "", new { @class = "text-danger" })
        <div class="form-group">
            @Html.LabelFor(model => model.TeamName, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.TeamName, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.TeamName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Coach, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Coach, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Coach, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Conference, htmlAttributes: new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Conference, new { htmlAttributes = new { @class = "form-control" } })
                @Html.ValidationMessageFor(model => model.Conference, "", new { @class = "text-danger" })
            </div>
        </div>
        @if (@Model != null)
        {
            foreach (var p in Model.Player)
            {
                <div class="form-group">
                    @Html.Raw("<label class=\"control-label col-md-2\">" + p.ToString() + "</label>")
                    <div class="col-md-10">
                        @Html.Raw("<input class=\"form-control text-box single-line\" name=\"Player\" type-\"text\"")
                    </div>
                </div>
            }
        }
        else
        {
            <div class="form-group">
                @Html.Raw("<label class=\"control-label col-md-2\">Player</label>")
                <div class="col-md-10">
                    @Html.Raw("<input class=\"form-control text-box single-line\" name=\"Player\" type-\"text\"")
                </div>
            </div>
        }
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

From my understanding, the View is suppose to be able to convert the input element to a list and pass it on to my ViewModel. However, my ViewModel is always coming up as null.

What am I missing and how would I make this work?

P.S. I understand that I can user Html.EditorFor, but I was not able to get it working, so I just printed it out as Html as I need to solve my other problem first.

Edit

I have altered my View to have the following code

<div class="form-group">
                @Html.Raw("<label class=\"control-label col-md-2\">Player</label>")
                <div class="col-md-10">
                    @Html.Raw("<input class=\"form-control text-box single-line\" name=\"model.Players[0].Name\" type-\"text\"")
                </div>
            </div>

As a result, the model now properly populates the Players Array, however all other values have now become null. If I remove the input element, the values are populated but players array is null again as there are no form fields for it. Do you know what could be the culprit?

In the TeamViewModel I have also renamed Player to Players.

halfer
  • 19,824
  • 17
  • 99
  • 186
L1ghtk3ira
  • 3,021
  • 6
  • 31
  • 70
  • Try editor templates. It will work http://stackoverflow.com/questions/34364990/calling-httppost-actionresult-from-inside-a-view-using-a-button/34366111#34366111 – Shyju Dec 27 '15 at 21:06

2 Answers2

5

In order for MVC to bind your form data to the Action method's parameters their names should match.

Supposing your ViewModel has property for List<Player> Players your code should be:

In your case:

foreach (var p in Model.Player)
            {
                <div class="form-group">
                    @Html.Raw("<label class=\"control-label col-md-2\">" + p.ToString() + "</label>")
                    <div class="col-md-10">
                        @Html.Raw("<input class=\"form-control text-box single-line\" name=\"Player\" type-\"text\"")
                    </div>
                </div>
            }

Should be:

for (int i = 0; i < Model.Player.Length; i++)
            {
                <div class="form-group">
                    @Html.Raw("<label class=\"control-label col-md-2\">" + p.ToString() + "</label>")
                    <div class="col-md-10">
                        @Html.Raw("<input class=\"form-control text-box single-line\" name=\"model.Player[" + i + "].Name\" type-\"text\"")
                    </div>
                </div>
            }

Because this is the name of the parameter that you have provided:

Create(TeamViewModel model)

Also be careful because the indexes should not be broken, which means that they should be 0, 1, 2.. etc. without skipping a number.

The way that we read in the properties is by looking for parameterName[index].PropertyName. The index must be zero-based and unbroken.

NOTE You can read more about binding collections in Scott Hanselman's post - here


And last I suggest if you have a property that is list of something - in your case list of Player to use the plural form for the property name - Players.

EDIT

Try removing the "model." in front in the name. Make it like this "Players[0].Name". Since you only have one parameter in your Create Action method it should be fine.

gyosifov
  • 3,193
  • 4
  • 25
  • 41
  • Thank you for your answer, do I then still need my else clause for the Player field when the model is null? Will it know to output 1 blank field if there are no values in the Model for me to enter a new value or I need to add that the way I am adding it now? Is there a better way achieving this? – L1ghtk3ira Dec 27 '15 at 21:45
  • Since you want to add (probably remove) players dynamically, this has to be managed with javascript. To avoid recalculating the indexes of the array in case of removing a player in the middle of the list (as mentioned before they have to be 0, 1 ,2, 3..) you can use a hidden with a name Player.Index. Please read more here http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/ – gyosifov Dec 27 '15 at 21:51
  • I have altered my question, with your approach a new problem arose that I can't figure out why it happens, would you happen to know? – L1ghtk3ira Dec 27 '15 at 22:32
  • Yes. Try removing the "model." in front in the name. Make it like this "Players[0].Name". Since you only have one parameter in your Create Action method it should be fine. – gyosifov Dec 27 '15 at 22:35
  • 1
    Thank you, the information you provided was much appreciated and I hope to return the favor in the future. – L1ghtk3ira Dec 29 '15 at 05:59
0

I suggest you to use the helper @Html.EditorFor, so to do this you will create a partial view that will be used as template to inputs of the nested property. see the example:

Shared/EditorTemplates/Player.cshtml

@model Player

<div class="form-group">
    @Html.HiddenFor(e => e.Id)
    @Html.HiddenFor(e => e.TeamId)
    <label class="control-label col-md-2" for="player">Player</label>
    <div class="col-md-10">
          @Html.TextBoxFor(e => e.Name, new { @class = "form-control text-box single-line", id = "player", name = "Player"})
    </div>
</div>    

Players form on Team view:

 @Html.EditorFor(e => e.Player)

Instead of:

 foreach (var p in Model.Player)
            {
                <div class="form-group">
                    @Html.Raw("<label class=\"control-label col-md-2\">" + p.ToString() + "</label>")
                    <div class="col-md-10">
                        @Html.Raw("<input class=\"form-control text-box single-line\" name=\"Player\" type-\"text\"")
                    </div>
                </div>
            }

See this article for more information about editor templates: Editor and display templates

Joel R Michaliszen
  • 4,164
  • 1
  • 21
  • 28