tags:

views:

661

answers:

3

This seems to be a pretty trivial problem that I have been stuck on for about an hour, and I don't understand what I'm doing wrong.

I have a ViewModel:

public class SongFormViewModel
    {
        public Song Song { get; set; }
        public SelectList AlbumList { get; set; }

        public SongFormViewModel(Song song, IQueryable<Album> albumList)
        {
            Song = song;
            AlbumList = new SelectList(albumList, "AlbumId", "Title", song.AlbumId);  
        }
    }

I have a Create view:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<NightSpot.Models.SongFormViewModel>" %>

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

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Create</h2>

    <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>

    <% using (Html.BeginForm()) {%>

        <fieldset>
            <legend>Fields</legend>
            <p>
                <label for="AlbumId">AlbumId:</label>
                <%= Html.DropDownList("AlbumId", Model.AlbumList) %>
                <%= Html.ValidationMessage("AlbumId", "*") %>
            </p>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="TrackNumber">TrackNumber:</label>
                <%= Html.TextBox("TrackNumber") %>
                <%= Html.ValidationMessage("TrackNumber", "*") %>
            </p>
            <p>
                <input type="submit" value="Create" />
            </p>
        </fieldset>

    <% } %>

    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>

</asp:Content>

And I have a SongsController Create method:

        //
        // POST: /Songs/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(Song song)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    repository.Add(song);
                    repository.Save();

                    return RedirectToAction("Details", new { id = song.SongId });
                }
                catch
                {
                    ModelState.AddRuleViolations(song.GetRuleViolations());
                }
            }

            return View(new SongFormViewModel(song, repository.FindAllAlbums()));
        }

When I navigate to the /Songs/Create URL I see the expected UI. My drop down list contains a list of all valid Albums in my database (from the Albums table), and I have validated that each AlbumId is the value, and each Title is the Text. Great.

So now when I go to fill out my form and hit "Save", I get an error telling me that I need to select an Album. When I step through the debugger in Visual Web Developer, I see that the Title & Track Number are being populated correctly, but my Album object is NULL, and AlbumId is still 0. Any ideas?

UPDATE

Taking Matt's advice, I updated my SongsController to use a SongFormViewModel instead of a Song. Here's the new controller:

        //
        // POST: /Songs/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create(SongFormViewModel song)
        {
            Song newSong = song.Song;
            newSong.AlbumId = (int) song.AlbumList.SelectedValue;

            if (ModelState.IsValid)
            {
                try
                {
                    repository.Add(newSong);
                    repository.Save();

                    return RedirectToAction("Details", new { id = song.Song.SongId });
                }
                catch
                {
                    ModelState.AddRuleViolations(song.Song.GetRuleViolations());
                }
            }

            return View(new SongFormViewModel(newSong, repository.FindAllAlbums()));
        }

I fired up my /Songs/Create URL and got a message saying SongFormViewModel required a parameterless constructor.

So I made one.

When I re-ran the /Songs/Create URL I got an "Object reference not set to an instance of an object" exception on this line in my SongsController:

Line 93:             newSong.AlbumId = (int) song.AlbumList.SelectedValue;

Ideas?

UPDATE

OK, so I updated my view per Matt's suggestion of prefixing the fields with "Song":

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<NightSpot.Models.SongFormViewModel>" %>

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

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Create</h2>

    <%= Html.ValidationSummary("Create was unsuccessful. Please correct the errors and try again.") %>

    <% using (Html.BeginForm()) {%>

        <fieldset>
            <legend>Fields</legend>
            <p>
                <label for="Song.AlbumId">AlbumId:</label>
                <%= Html.DropDownList("Song.AlbumId", Model.AlbumList) %>
                <%= Html.ValidationMessage("Song.AlbumId", "*") %>
            </p>
            <p>
                <label for="Song.Title">Title:</label>
                <%= Html.TextBox("Song.Title") %>
                <%= Html.ValidationMessage("Song.Title", "*") %>
            </p>
            <p>
                <label for="Song.TrackNumber">TrackNumber:</label>
                <%= Html.TextBox("Song.TrackNumber") %>
                <%= Html.ValidationMessage("Song.TrackNumber", "*") %>
            </p>
            <p>
                <input type="submit" value="Create" />
            </p>
        </fieldset>

    <% } %>

    <div>
        <%=Html.ActionLink("Back to List", "Index") %>
    </div>

</asp:Content>

The behavior is, unfortunately, the same. Null SelectedValue and an empty Song object.

+1  A: 

I'm just now learning this stuff myself, but from what I understand, passing song.AlbumId into the constructor of SelectList only sets the initial selected value of the list. It doesn't "bind" the list to your song object's Album property.

In your Create method, you will have to manually assign the album represented by the selected item in the SongFormViewModel.Albums list. You can use the SelectedValue property to retrieve the Id of the album that the user selected in this case. You'll need to cast it to an int (or Guid - whatever type AlbumId is).

Of course, this means you'll need access to the ViewModel class from your Create method, rather than just the Song that was created. I'm fairly certain you can just change the parameter type from Song to SongFormViewModel and let the default binder take care of that for you.

Let me know how you get on - I'm keen to learn more about this stuff so I'd love to see the final solution!

Matt Hamilton
Thanks, Matt! I will give that a try!
KG
See my updates...getting an exception...
KG
A: 

EDIT

If you like to keep your existing model. Change your SongFormViewModel to this

public class SongFormViewModel
{
        public Song Song { get; set; }
        public SelectList AlbumList { get; set; }
}

and then change

return View(new SongFormViewModel(song, repository.FindAllAlbums()));

to

var songFormViewModel = new SongFormViewModel();
songFormViewModel.AlbumList = repository.FindAllAlbums();
songFormViewModel.Song = song;
return View(songFormViewModel);

--

Your view should be a strongly typed of type Song not SongFormViewModel. So change

Inherits="System.Web.Mvc.ViewPage<NightSpot.Models.SongFormViewModel>"

to

Inherits="System.Web.Mvc.ViewPage<NightSpot.Models.Song>"

Now get rid of the prefixing fields change. That should work.

iaimtomisbehave
Then how would I get access to the values for my drop down list?
KG
You will need to save it in the ViewData in your Get request.ViewData["albumList"] = new List<SelectListItem>{//Your entries here}.
iaimtomisbehave
Your approach of using SongFormViewModel is also possible. The problem you are having is because of your Controller. The constructor with the parameters will never get called because by the ControllerFactory won't know how to pass Song and IQueryable<Album> to it, where to get it from. Hence it was complaining about parameterless constructor. You could write your own ControllerFactory or use dependency injection.
iaimtomisbehave
KG
I edited the answer, if you prefer to use your existing model you should be able to now. Matt was right about your controller it should accept SongFormViewModel if your view is of type SongFormViewModel.
iaimtomisbehave
That's not working either.
KG
+1  A: 

You are trying to use MVC like classic ASP.NET

MVC return the raw HTTP post values, and the default helpers with MVC attempt to convert them to basic models

You can can get the selected list items value e.g. AlbumId, but not complex types such as AlbumList.SelectedValue

Try something like this

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(int albumId, ...)

SongFormViewModel is not helping you here. It does not reflect what you appear to actually want. It should have something like the currently selected Album as a field

TFD
Thank you! That is exactly what did the trick!
KG