Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC 2 Model with array/list

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.

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.

But I can not figure out how to make this possible through the model, here is how far I have been able to get.

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<AlbumTagModel> Tags { get; set; }

    [DisplayName("Songs")]
    // Min 5 songs no max
    public List<AlbumSongModel> 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; }
}

View:

<%@ Page Title="" Language="C#" MasterPageFile="~/App/Views/Shared/MasterPage.Master" Inherits="System.Web.Mvc.ViewPage<album.App.Models.AlbumCreateModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <% using (Html.BeginForm()) { %>
        <%: Html.ValidationSummary(true, "Committing the album was unsuccessful. Please correct the errors and try again.")%>
        <div>
            <fieldset>
                <legend>Album Information</legend>

                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Title) %>
                </div>
                <div class="editor-field">
                    <%: Html.TextBoxFor(m => m.Title)%>
                    <%: Html.ValidationMessageFor(m => m.Title)%>
                </div>

                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Description) %>
                </div>
                <div class="editor-field">
                    <%: Html.TextAreaFor(m => m.Description)%>
                    <%: Html.ValidationMessageFor(m => m.Description)%>
                </div>

                <!-- Tags here -->

                <!-- Songs here -->

                <p>
                    <input type="submit" value="Commit" />
                </p>
            </fieldset>
        </div>
    <% } %>
</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="MetaData" runat="server">
</asp:Content>

A possible solution:

Model:

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<PlaylistTagModel> Tags { get; set; }

        [DisplayName("Songs")]
        [ListCount(Min = 5)]
        public List<PlaylistSongModel> 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; }
    }

View:

<%@ Page Title="" Language="C#" MasterPageFile="~/App/Views/Shared/MasterPage.Master" Inherits="System.Web.Mvc.ViewPage<playlist.App.Models.PlaylistModel>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Create
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <% using (Html.BeginForm()) { %>
        <%: Html.ValidationSummary(true, "Committing the playlist was unsuccessful. Please correct the errors and try again.")%>
        <div>
            <fieldset>
                <legend>Playlist Information</legend>

                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Title) %>
                </div>
                <div class="editor-field">
                    <%: Html.TextBoxFor(m => m.Title)%>
                    <%: Html.ValidationMessageFor(m => m.Title)%>
                </div>

                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Description) %>
                </div>
                <div class="editor-field">
                    <%: Html.TextAreaFor(m => m.Description)%>
                    <%: Html.ValidationMessageFor(m => m.Description)%>
                </div>

                <br />
                <%: Html.ValidationMessageFor(m => m.Tags)%>
                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Tags)%>
                </div>
                <div class="editor-field">
                    <%: Html.EditorFor(m => m.Tags) %>
                    <%: Html.Editor("Tags[" + (Model == null ? 0 : Model.Tags.Count) + "]", "PlaylistTagModel")%>
                </div>

                <br />
                <%: Html.ValidationMessageFor(m => m.Songs)%>
                <div class="editor-label">
                    <%: Html.LabelFor(m => m.Songs)%>
                </div>
                <div class="editor-field">
                    <%: Html.EditorFor(m => m.Songs)%>
                    <%: Html.Editor("Songs[" + (Model == null ? 0 : Model.Songs.Count) + "]", "PlaylistSongModel")%>
                </div>

                <p>
                    <input type="submit" value="Commit" />
                </p>
            </fieldset>
        </div>
    <% } %>
</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="MetaData" runat="server">
</asp:Content>

The two templates:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<playlist.App.Models.PlaylistSongModel>" %>

<fieldset>
    <legend>Song Information</legend>
    <%: Html.ValidationSummary(true, "Committing this song was unsuccessful. Please correct the errors and try again.")%>

    <div class="editor-label">
        <%: Html.LabelFor(m => m.Title) %>
    </div>
    <div class="editor-field">
        <%: Html.TextBoxFor(m => m.Title)%>
        <%: Html.ValidationMessageFor(m => m.Title)%>
    </div>

    <div class="editor-label">
        <%: Html.LabelFor(m => m.Artist)%>
    </div>
    <div class="editor-field">
        <%: Html.TextBoxFor(m => m.Artist)%>
        <%: Html.ValidationMessageFor(m => m.Artist)%>
    </div>

    <div class="editor-label">
        <%: Html.LabelFor(m => m.Description)%>
    </div>
    <div class="editor-field">
        <%: Html.TextAreaFor(m => m.Description)%>
        <%: Html.ValidationMessageFor(m => m.Description)%>
    </div>

    <div class="editor-label">
        <%: Html.LabelFor(m => m.Length)%>
    </div>
    <div class="editor-field">
        <%: Html.TextBoxFor(m => m.Length)%>
        <%: Html.ValidationMessageFor(m => m.Length)%>
    </div>

    <div class="editor-label">
        <%: Html.LabelFor(m => m.Year)%>
    </div>
    <div class="editor-field">
        <%: Html.TextBoxFor(m => m.Year)%>
        <%: Html.ValidationMessageFor(m => m.Year)%>
    </div>
</fieldset>

Tag:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<playlist.App.Models.PlaylistTagModel>" %>

<span class="tag"><%: Html.TextBoxFor(m => m.Tag)%></span> <%: Html.ValidationMessageFor(m => m.Tag)%>

And finally my custom validator to lists:

public class ListCountAttribute : ValidationAttribute
    {
        public int Min { get; set; }
        public int Max { get; set; }

        public override bool IsValid(object value)
        {
            if (Min == 0 && 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 && Max != 0)
                return countable.Count <= Max;
            else if (Max == 0 && Min != 0)
                return countable.Count >= Min;
            return (countable.Count >= Min) && (countable.Count <= Max);
        }

        public override string FormatErrorMessage(string name)
        {
            if (Min == 0 && Max != 0)
                return "The field set " + name + " can not be larger then " + Max;
            else if (Max == 0 && 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;
        }
    }
like image 939
Androme Avatar asked Aug 14 '10 20:08

Androme


2 Answers

Create a folder in /views/shared called "EditorTemplates". It must be called this exactly.

In this folder create files called AlbumTagModel.ascx and AlbumSongModel.ascx strongly typed to their respective models.

Add the input fields in these files but do not wrap them in form tags.

Back in your view page put:

<%: Html.EditorFor(m => m.Tags)%>

and

<%: Html.EditorFor(m => m.Songs)%>

Now voila!

When you render the input tags will be subscripted to match your list. EditorFor will loop and render all by itself. When you post your strongly typed AlbumViewModel your lists will be correctly bound back to their original positions.

To add new songs/tags add the following to your AlbumViewModel:

public AlbumTagModel NewTagModel {get;set;}

and add an extra EditorFor() for it.

When your model posts if the NewTagModel is valid add it to the list and reshow the view.

like image 52
jwsample Avatar answered Oct 20 '22 05:10

jwsample


You have to write a custom validation attribute to support that requirement.

Here's an example that isn't exactly what you're looking for, but should get you pointed in the right direction. There are notes after it on how you would adjust it for your environment.

public class AtLeastOneRequiredAttribute : ValidationAttribute
{
  public override bool IsValid(object value)
  {
    if (value == null)
      return false;

    if (!(value is ICountable))
      throw new InvalidOperationException("AtLeastOneRequiredAttribute requires underlying property to implement ICountable");

    ICountable countable = value as ICountable;
    return countable.Count >= 1;
  }
}

We use our own "child models" instead of generic lists and have them implement ICountable, which is a utility interface in our environment. You would just check to make sure that your value implemented IList, then invoke (value as IList).Count.

For a general minimum instead of the "At Least One", define a Min Property.

Hope that gets you headed in the right direction, post if you have other questions.

like image 39
Rob Avatar answered Oct 20 '22 04:10

Rob