8

I have an entity called Domain.Models.BlogPost which contains the following properties:

  • PostID
  • Title
  • Author
  • PostedDate
  • Body

I also have an entity called Domain.Models.PostComment which contains the following properties:

  • CommentID
  • PostID
  • Author
  • Email
  • Website
  • Body

BlogPost contains many PostComments. A one to many relationship.

Now I have a view like this (separated comment form from blog post code via html comment):

@model Domain.Models.BlogPost
@using Domain.Models;
@{
    ViewBag.Title = "Post";
}
<div class="postTitle">@Model.Title</div>
<div class="subInfo">
    Posted by @Model.Author on @Model.PostedDate.ToString("D")
</div>
<div class="postBody">
    @Html.Markdown(Model.Body)
</div>
<br />
@Model.PostComments.Count Comment(s).
<div class="comments">
@foreach (PostComment postComment in Model.PostComments)
{
    Html.RenderPartial("PostComment", postComment);
}
</div>

<!-- BELOW IS THE ADD COMMENT FORM -->

<div id="addComment">
@using (Html.BeginForm("AddComment", "Blog"))
{
    <text>
    @Html.Hidden("PostID", Model.PostID)<br />
    Name: @Html.TextBox("Author")<br />
    Email: @Html.TextBox("Email")<br />
    Website: @Html.TextBox("Website")<br />
    Body: @Html.TextArea("Body")<br />
    <input type="submit" value = "Add Comment" />
    </text>
}
</div>
@Html.ActionLink("Add Comment", "AddComment")

The problem is that because the comment form uses @Html.TextBox("Author") and @Html.TextBox("Body"), they are populated with data from the model, which also contains properties Author and Body. Any suggestions on how to fix this so these fields don't get values put in them when the page loads?

I also tried creating a BlogPostViewModel and setting that as the model of the view and assigning the BlogPost property with my actual model:

public class BlogPostViewModel
{
    public BlogPost BlogPost { get; set; }
    public PostComment NewComment { get; set; }
}

Then I did @Html.TextBoxFor(x => x.NewComment.Author) but when the form posted to this action method:

public ActionResult AddComment(PostComment postComment)
{
    // ...
}

postComment did not bind to the form values :/

CatDadCode
  • 58,507
  • 61
  • 212
  • 318
  • 3
    This is an ugly fact of the default model binder. I've hit this a few times. My best solution has been to add [Bind(Exclude="Author, Body")] to the action and then fix up the snafu manually. I'd love to hear a better approach. – Matt Greer Mar 17 '11 at 16:21
  • I would love to hear a better approach also. – CatDadCode Mar 17 '11 at 16:24
  • Saw your update, try binding based on the NewComment prefix. i.e. put [Bind(Prefix = "NewComment")] before the parameter PostComment postComment – Adam Price Mar 17 '11 at 16:39
  • I will try this and let you know how it goes. – CatDadCode Mar 17 '11 at 16:45
  • Attribute 'Bind' is not valid on this declaration type. It is only valid on 'class, param' declarations. <-- that happened after I put your attribute over my action method `public ActionResult AddComment(PostComment postComment)` – CatDadCode Mar 17 '11 at 16:48
  • Bingo. With the help of this question http://stackoverflow.com/questions/1317523/how-to-use-bind-prefix I was able to use `Bind` correctly. Thank you. @optus will you post that as an answer or update your answer and I will accept it :) – CatDadCode Mar 17 '11 at 16:51

3 Answers3

6

You could either rename the fields in the AddComment section to something that does not collide with the properties named in the Model, or you could override the value in the view using a different overload of Html.TextBox This overload of TextBox takes a value:

value (Type: System.Object)
The value of the text input element. If this value is null, the value of the element is retrieved from the ViewDataDictionary object. If no value exists there, the value is retrieved from the ModelStateDictionary object.


UPDATE: Since you added "NewComment" as a property and resolved the property naming collision that way, all that you need to do to bind a PostComment rather than the whole view model on POST to the action, is to instruct the model binder that a prefix will be used. This is done using the BindAttribute.

public ActionResult AddComment([Bind(Prefix = "NewComment")] PostComment postComment)
Adam Price
  • 10,027
  • 1
  • 20
  • 16
  • Hmmm, alright well that's two people telling me to override the value to `string.Empty` so I guess that's what I'll do. I just thought there would be a better way to structure the models/view so that these collisions don't even occur. – CatDadCode Mar 17 '11 at 16:23
  • You could structure it a different way to avoid the collision. @Jakub Konecki is on the right track. Add a PostComment property named "NewComment" to the BlogPost model class. Then your view code would look like @Html.TextBoxFor(x => x.NewComment.Body); – Adam Price Mar 17 '11 at 16:26
  • right except then I have to accept the view model as a parameter in the `AddComment` action method, instead of accepting a simple `PostComment` object. As mentioned in the question, I tried this once already. – CatDadCode Mar 17 '11 at 16:30
  • Love the update. Solved all my problems without any cheesy value overriding. Thank you! – CatDadCode Mar 17 '11 at 17:10
1

Use ASP NET MVC templates so you have full control over what gets populated and it is type-safe.

So you would create an .ascx template which takes a strongly typed Comment. In your model, you leave an empty one in there.

Aliostad
  • 80,612
  • 21
  • 160
  • 208
  • Wouldn't this mean that the `AddComment` action method would have to accept `BlogPostViewModel` as a parameter instead of just a `PostComment`? That seems odd posting back a view model object with all empty properties except the one I'm after. Maybe I'm wrong and that's totally normal. – CatDadCode Mar 17 '11 at 16:26
  • No. You would pass `Model.EditorFor(m => m.Comment)` – Aliostad Mar 17 '11 at 16:29
  • In the view yes, but the action method would require accepting a view model object, not a `PostComment` object right? I'm testing it now to verify. – CatDadCode Mar 17 '11 at 16:34
  • Yep, just checked. Because `NewComment` is a property of `BlogPostViewModel` I would need to accept `BlogPostViewModel` as the argument of the `AddComment` action method instead of the more succinct parameter of simple `PostComment`. – CatDadCode Mar 17 '11 at 16:38
  • Check this screenshot http://www.codetunnel.com/POST_ScreenShot.jpg and you'll notice that the form values are prepended with `NewComment.Property` whereas without being part of a view model object, they are simply `Property`. `NewComment.Property` causes it not to bind unless I accept the view model object itself as the argument. – CatDadCode Mar 17 '11 at 16:41
  • I did use the editor template and this will save me some code as I have 2 or 3 places where this editor will be. So thank you and +1. However this did not solve the issue of form data being prepended with the view model property. Optus helped me with that through the use of the `Bind` attribute. Thanks for the help though! – CatDadCode Mar 17 '11 at 16:53
0

Isn't there an overload on TextBox that takes a value? You could just pass string.Empty...

Jakub Konecki
  • 45,581
  • 7
  • 87
  • 126
  • Yes, but this seems hackish. It feels like there should be a better way. – CatDadCode Mar 17 '11 at 16:21
  • But I believe he wants the value the user put in. The user's value is getting clobbered by the default model binder after the POST occurs. – Matt Greer Mar 17 '11 at 16:21
  • I think this is a proper way - you want to render an empty form. You could bind to an empty instance of PostComment.... – Jakub Konecki Mar 17 '11 at 16:22
  • Nevermind, I read the question completely wrong. Ignore me :) – Matt Greer Mar 17 '11 at 16:25
  • @Matt no, the POST is fine. If the user erases the existing values dropped in by the model binder and posts the form, the values correctly submit. It's just annoying that the form fields are pre-loaded with values from the `BlogPost` model even though they are meant to be empty `PostComment` fields. – CatDadCode Mar 17 '11 at 16:27