title: ASP.NET MVC 2 Model with array/list
body: <p>I am in the process of creating my first site in ASP.NET MVC, it is a kind of learn as you go. But I have hit a problem that I just can't find a solution for.</p>
  
  <p>I want my user to be able to create an album with songs and tags attached.
  That can be an unspecified number of songs and tags. But there must be a minimum of 5 songs and 2 tags.</p>
  
  <p>But I can not figure out how to make this possible through the model, here is how far I have been able to get.</p>
  
  <pre><code>public class AlbumCreateModel
  {
      [Required]
      [DisplayName("Title")]
      public string Title { get; set; }
  
      [DisplayName("Description")]
      public string Description { get; set; }
  
      [DisplayName("Publish")]
      public bool Public { get; set; }
  
      [DisplayName("Tags")]
      // Min 2 tags no max
      public List&lt;AlbumTagModel&gt; Tags { get; set; }
  
      [DisplayName("Songs")]
      // Min 5 songs no max
      public List&lt;AlbumSongModel&gt; Songs { get; set; }
  }
  
  public class AlbumTagModel
  {
      [Required]
      [DisplayName("Tag")]
      // Regex to test no spaces
      // min 2 characters
      // maximum 15 characters
      public string Tag { get; set; }
  }
  
  public class AlbumSongModel
  {
      [Required]
      [DisplayName("Title")]
      public string Title { get; set; }
  
      [Required]
      [DisplayName("Artist")]
      public string Artist { get; set; }
  
      [DisplayName("Description")]
      public string Description { get; set; }
  
      [DisplayName("Song Length")]
      public double Length { get; set; }
  
      [DisplayName("Year")]
      public int Description { get; set; }
  }
  </code></pre>
  
  <h3>View:</h3>
  
  <pre><code>&lt;%@ Page Title="" Language="C#" MasterPageFile="~/App/Views/Shared/MasterPage.Master" Inherits="System.Web.Mvc.ViewPage&lt;album.App.Models.AlbumCreateModel&gt;" %&gt;
  
  &lt;asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"&gt;
      Create
  &lt;/asp:Content&gt;
  
  &lt;asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"&gt;
      &lt;% using (Html.BeginForm()) { %&gt;
          &lt;%: Html.ValidationSummary(true, "Committing the album was unsuccessful. Please correct the errors and try again.")%&gt;
          &lt;div&gt;
              &lt;fieldset&gt;
                  &lt;legend&gt;Album Information&lt;/legend&gt;
  
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Title) %&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.TextBoxFor(m =&gt; m.Title)%&gt;
                      &lt;%: Html.ValidationMessageFor(m =&gt; m.Title)%&gt;
                  &lt;/div&gt;
  
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Description) %&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.TextAreaFor(m =&gt; m.Description)%&gt;
                      &lt;%: Html.ValidationMessageFor(m =&gt; m.Description)%&gt;
                  &lt;/div&gt;
  
                  &lt;!-- Tags here --&gt;
  
                  &lt;!-- Songs here --&gt;
  
                  &lt;p&gt;
                      &lt;input type="submit" value="Commit" /&gt;
                  &lt;/p&gt;
              &lt;/fieldset&gt;
          &lt;/div&gt;
      &lt;% } %&gt;
  &lt;/asp:Content&gt;
  
  &lt;asp:Content ID="Content3" ContentPlaceHolderID="MetaData" runat="server"&gt;
  &lt;/asp:Content&gt;
  </code></pre>
  
  <p>A possible solution:</p>
  
  <h3>Model:</h3>
  
  <pre><code>public class PlaylistModel
      {
          [Required]
          [DisplayName("Title")]
          public string Title { get; set; }
  
          [DisplayName("Description")]
          public string Description { get; set; }
  
          [DisplayName("Publish")]
          public bool Public { get; set; }
  
          [DisplayName("Tags")]
          [ListCount(Min = 2)]
          // Min 2 tags no max
          public List&lt;PlaylistTagModel&gt; Tags { get; set; }
  
          [DisplayName("Songs")]
          [ListCount(Min = 5)]
          public List&lt;PlaylistSongModel&gt; Songs { get; set; }
      }
  
      public class PlaylistTagModel
      {
          [Required]
          [DisplayName("Tag")]
          // Regex to test no spaces
          // min 2 characters
          // maximum 15 characters
          public string Tag { get; set; }
      }
  
      public class PlaylistSongModel
      {
          [Required]
          [DisplayName("Title")]
          public string Title { get; set; }
  
          [Required]
          [DisplayName("Artist")]
          public string Artist { get; set; }
  
          [DisplayName("Description")]
          public string Description { get; set; }
  
          [DisplayName("Song Length")]
          public int Length { get; set; }
  
          [DisplayName("Year")]
          public int Year { get; set; }
      }
  </code></pre>
  
  <h3>View:</h3>
  
  <pre><code>&lt;%@ Page Title="" Language="C#" MasterPageFile="~/App/Views/Shared/MasterPage.Master" Inherits="System.Web.Mvc.ViewPage&lt;playlist.App.Models.PlaylistModel&gt;" %&gt;
  
  &lt;asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"&gt;
      Create
  &lt;/asp:Content&gt;
  
  &lt;asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"&gt;
      &lt;% using (Html.BeginForm()) { %&gt;
          &lt;%: Html.ValidationSummary(true, "Committing the playlist was unsuccessful. Please correct the errors and try again.")%&gt;
          &lt;div&gt;
              &lt;fieldset&gt;
                  &lt;legend&gt;Playlist Information&lt;/legend&gt;
  
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Title) %&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.TextBoxFor(m =&gt; m.Title)%&gt;
                      &lt;%: Html.ValidationMessageFor(m =&gt; m.Title)%&gt;
                  &lt;/div&gt;
  
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Description) %&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.TextAreaFor(m =&gt; m.Description)%&gt;
                      &lt;%: Html.ValidationMessageFor(m =&gt; m.Description)%&gt;
                  &lt;/div&gt;
  
                  &lt;br /&gt;
                  &lt;%: Html.ValidationMessageFor(m =&gt; m.Tags)%&gt;
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Tags)%&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.EditorFor(m =&gt; m.Tags) %&gt;
                      &lt;%: Html.Editor("Tags[" + (Model == null ? 0 : Model.Tags.Count) + "]", "PlaylistTagModel")%&gt;
                  &lt;/div&gt;
  
                  &lt;br /&gt;
                  &lt;%: Html.ValidationMessageFor(m =&gt; m.Songs)%&gt;
                  &lt;div class="editor-label"&gt;
                      &lt;%: Html.LabelFor(m =&gt; m.Songs)%&gt;
                  &lt;/div&gt;
                  &lt;div class="editor-field"&gt;
                      &lt;%: Html.EditorFor(m =&gt; m.Songs)%&gt;
                      &lt;%: Html.Editor("Songs[" + (Model == null ? 0 : Model.Songs.Count) + "]", "PlaylistSongModel")%&gt;
                  &lt;/div&gt;
  
                  &lt;p&gt;
                      &lt;input type="submit" value="Commit" /&gt;
                  &lt;/p&gt;
              &lt;/fieldset&gt;
          &lt;/div&gt;
      &lt;% } %&gt;
  &lt;/asp:Content&gt;
  
  &lt;asp:Content ID="Content3" ContentPlaceHolderID="MetaData" runat="server"&gt;
  &lt;/asp:Content&gt;
  </code></pre>
  
  <p>The two templates:</p>
  
  <pre><code>&lt;%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl&lt;playlist.App.Models.PlaylistSongModel&gt;" %&gt;
  
  &lt;fieldset&gt;
      &lt;legend&gt;Song Information&lt;/legend&gt;
      &lt;%: Html.ValidationSummary(true, "Committing this song was unsuccessful. Please correct the errors and try again.")%&gt;
  
      &lt;div class="editor-label"&gt;
          &lt;%: Html.LabelFor(m =&gt; m.Title) %&gt;
      &lt;/div&gt;
      &lt;div class="editor-field"&gt;
          &lt;%: Html.TextBoxFor(m =&gt; m.Title)%&gt;
          &lt;%: Html.ValidationMessageFor(m =&gt; m.Title)%&gt;
      &lt;/div&gt;
  
      &lt;div class="editor-label"&gt;
          &lt;%: Html.LabelFor(m =&gt; m.Artist)%&gt;
      &lt;/div&gt;
      &lt;div class="editor-field"&gt;
          &lt;%: Html.TextBoxFor(m =&gt; m.Artist)%&gt;
          &lt;%: Html.ValidationMessageFor(m =&gt; m.Artist)%&gt;
      &lt;/div&gt;
  
      &lt;div class="editor-label"&gt;
          &lt;%: Html.LabelFor(m =&gt; m.Description)%&gt;
      &lt;/div&gt;
      &lt;div class="editor-field"&gt;
          &lt;%: Html.TextAreaFor(m =&gt; m.Description)%&gt;
          &lt;%: Html.ValidationMessageFor(m =&gt; m.Description)%&gt;
      &lt;/div&gt;
  
      &lt;div class="editor-label"&gt;
          &lt;%: Html.LabelFor(m =&gt; m.Length)%&gt;
      &lt;/div&gt;
      &lt;div class="editor-field"&gt;
          &lt;%: Html.TextBoxFor(m =&gt; m.Length)%&gt;
          &lt;%: Html.ValidationMessageFor(m =&gt; m.Length)%&gt;
      &lt;/div&gt;
  
      &lt;div class="editor-label"&gt;
          &lt;%: Html.LabelFor(m =&gt; m.Year)%&gt;
      &lt;/div&gt;
      &lt;div class="editor-field"&gt;
          &lt;%: Html.TextBoxFor(m =&gt; m.Year)%&gt;
          &lt;%: Html.ValidationMessageFor(m =&gt; m.Year)%&gt;
      &lt;/div&gt;
  &lt;/fieldset&gt;
  </code></pre>
  
  <h3>Tag:</h3>
  
  <pre><code>&lt;%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl&lt;playlist.App.Models.PlaylistTagModel&gt;" %&gt;
  
  &lt;span class="tag"&gt;&lt;%: Html.TextBoxFor(m =&gt; m.Tag)%&gt;&lt;/span&gt; &lt;%: Html.ValidationMessageFor(m =&gt; m.Tag)%&gt;
  </code></pre>
  
  <p>And finally my custom validator to lists:</p>
  
  <pre><code>public class ListCountAttribute : ValidationAttribute
      {
          public int Min { get; set; }
          public int Max { get; set; }
  
          public override bool IsValid(object value)
          {
              if (Min == 0 &amp;&amp; Max == 0)
                  return true;
  
              if (value == null)
                  return false;
  
              if (!(value is ICollection))
                  throw new InvalidOperationException("ListCountAttribute requires underlying property to implement ICollection");
  
              ICollection countable = value as ICollection;
              if (Min == 0 &amp;&amp; Max != 0)
                  return countable.Count &lt;= Max;
              else if (Max == 0 &amp;&amp; Min != 0)
                  return countable.Count &gt;= Min;
              return (countable.Count &gt;= Min) &amp;&amp; (countable.Count &lt;= Max);
          }
  
          public override string FormatErrorMessage(string name)
          {
              if (Min == 0 &amp;&amp; Max != 0)
                  return "The field set " + name + " can not be larger then " + Max;
              else if (Max == 0 &amp;&amp; Min != 0)
                  return "The field set " + name + " need to have atleast a count of " + Min;
              return "The field set " + name + " need to between or equal to " + Min + " and " + Max;
          }
      }
  </code></pre>