views:

158

answers:

3

Hello i'm 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 thoug the model, here is how far i have been able to get.

Thank you for your help.

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; }
}

The view file

<%@ 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>

After alot of playing around and thaks to the 2 anwser did i come to a sulution. It still need some work thoug :D

The 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; }
    }

The 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 to 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 finaly 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;
        }
    }

Thanks alot for your help!

+1  A: 

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.

Rob
+1  A: 

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.

jwsample
One more thing, make sure the .ascx files are strongly typed to a single object, NOT a list of objects.
jwsample
Thank you after playing some with it have i gotten it to work! I will add my final solutio to my first message when i'm done.
DoomStone
A: 

I can't seem to make it work when trying to use template editors for editing a collection of objects. Here's what I have:

public class Candidate
    {
        public Candidate()
        {
            this.References = new List<Reference>();
        }
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime? DateOfBirth { get; set; }
        [UIHint("Reference")]  
        public List<Reference> References { get; set; }

    }

public class Reference
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Institution { get; set; }
        public string Email { get; set; }
        public string Phone { get; set; }
        public string Position { get; set; }
    }

I created a template in Views/Shared/TemplateEditors, and created a strongly typed partial view Register.ascx.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Models.Reference>" %>
<%:Html.TextBoxFor(model=>model.Name) %>
<%:Html.TextBoxFor(model=>model.Email) %>
<%:Html.TextBoxFor(model=>model.Institution) %>
<%:Html.TextBoxFor(model=>model.Position) %>
<%:Html.TextBoxFor(model=>model.Phone) %>

In the view that I have the candidate I use the code below to render the list of references.

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

When I run the project I get

 InvalidOperationException: The model item passed into the dictionary is of type 'System.Collections.Generic.List`1[Models.Reference]', but this dictionary requires a model item of type 'Models.Reference'.

Do you guys see anything wrong with the implementation?

Thanks.

Blerim J