BeginCollectionItem partial within partial not behaving correctly - c#

I am trying to bind all my model data at once through a form submission in MVC 5 using an edited version of BeginCollectionItem as discussed in Joe Steven's blog here.
The model, Company, has a List<Pa_Ipv4>, the class Pa_Ipv4 in turn has a List<IpAllocation>, I want to access and save to the database all the properties of the IpAllocation in each Pa_Ipv4.
IE: Model.pa_ipv4s[x].requestedIps[x].subnet
The main page is using model Company, which has a partial accepting Pa_Ipv4, which has a partial accepting IpAllocation.
Question 1: In my controller, I'm setting a string property for the first item in the list (requestedIp), but when I submit and postback, the property (allocationType) is null, this property needs to be hard coded as it's for internal use within the DB - why is this being reset?
Reason: The property isn't in the post method, as such what is initially declared is discarded as it's not within the end post.
Possible Solution: Use a hidden property within the form so that it is present when the form is posted and the user cannot access the property.
Question 2: BeginCollectionItem is naming attributes appropriately, IE: pa_ipv4s[8e075d50-a5fb-436f-9cef-85abfb6910e3].requestedIps[b693b83c-b6b1-4c42-b983-4d058e766d4c].subnet, but only the initial model, it's then ignoring any others created, what have I done wrong?
Reason: The GUID needed for a prefix generated by the Pa_Ipv4 sections BeginCollectionItem is not able to be accessed by the IpAllocation BeginCollectionItem, as such only the initial content has the correct prefixes, anything added hereafter misses the necessary prefix.
Another potential solution is essentially the same concept, but instead of using a div, use html data attribute instead so that it's accessible.
I think both of the issues I'm experiencing are to do with how I've set my controller up, but I've included the Views and Model below as well. The model contains all the properties, lots of these have been removed in my views to save space as these are not causing the issue.
Create
#model Company
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Html.BeginForm())
{
<div class="jumboservice">
<div data-role="page">
<div data-role="header">
<h2>PA IPv4 Request Form</h2>
</div>
<div class="ui-content" data-role="main">
<h3>Company Details</h3>
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<p class="lblStyle">Company Name</p>
<span>
#Html.EditorFor(m => m.name)
#Html.ValidationMessageFor(m => m.name)
</span>
</div>
</div>
</div>
<br />
#foreach (var i in Model.pa_ipv4s)
{
#Html.Partial("Pa_IPv4View", i)
}
<br />
<div data-role="main" class="ui-content">
<div data-role="controlgroup" data-type="horizontal">
<input type="submit" class="ui-btn" value="Create" />
</div>
</div>
</div>
</div>
}
<script type="text/javascript">
$(function () {
$('#addItemRIpM').on('click', function () {
$.ajax({
url: '#Url.Action("RequestedManager")',
cache: false,
success: function (html) { $("#editorRowsRIpM").append(html); }
});
return false;
});
$('#editorRowsRIpM').on('click', '.deleteRow', function () {
$(this).closest('.editorRow').remove();
});
});
</script>
Pa_Ipv4 Partial
#model Pa_Ipv4
#using (HtmlHelpers.BeginCollectionItem.HtmlPrefixScopeExtensions.BeginCollectionItem(Html,"pa_ipv4s"))
{
#Html.AntiForgeryToken()
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<p class="lblStyle">Subnet</p>
</div>
<div class="ui-block-b">
<p class="lblStyle">Size(CIDR)</p>
</div>
<div class="ui-block-c">
<p class="lblStyle">Mask</p>
</div>
<div class="ui-block-d">
</div>
</div>
#*Request IP Address Space List*#
<div id="editorRowsRIpM">
#foreach (var item in Model.requestedIps)
{
#Html.Partial("RequestedIpView", item)
}
</div>
#Html.ActionLink("Add", "RequestedManager", null, new { id = "addItemRIpM", #class = "button" })
}
RequestedIp Partial
#model IpAllocation
<div class="editorRow">
#using (HtmlHelpers.BeginCollectionItem.HtmlPrefixScopeExtensions.BeginCollectionItem(Html, "requestedIps"))
{
<div class="ui-grid-c ui-responsive">
<div class="ui-block-a">
<span>
#Html.TextBoxFor(m => m.subnet)
</span>
</div>
<div class="ui-block-b">
<span>
#Html.TextBoxFor(m => m.cidr)
</span>
</div>
<div class="ui-block-c">
<span>
#Html.TextBoxFor(m => m.mask)
<span class="dltBtn">
Remove
</span>
</span>
</div>
</div>
}
</div>
Controller
public ActionResult Create()
{
var cmp = new Company();
cmp.contacts = new List<Contact>
{
new Contact { email = "", name = "", telephone = "" }
};
cmp.pa_ipv4s = new List<Pa_Ipv4>
{
new Pa_Ipv4
{
ipType = "Pa_IPv4", registedAddress = false, existingNotes = "",
numberOfAddresses = 0, returnedAddressSpace = false, additionalInformation = "",
requestedIps = new List<IpAllocation>
{
new IpAllocation { allocationType = "Requested", cidr = "", mask = "", subnet = "" } // allocationType is null in cmp in the Create[HttpPost]
}
}
};
return View(cmp);
}
public ActionResult Pa_IPv4Manager()
{
return PartialView("Pa_IPv4View", new Pa_Ipv4());
}
public ActionResult RequestedManager()
{
return PartialView("RequestedIpView", new IpAllocation { allocationType = "Requested" }); // allocationType is null in cmp in the Create[HttpPost]
}
// POST: Pa_Ipv4/Create
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Company cmp) //only one requestedIps count regardless of how many add
{
if (ModelState.IsValid)
{
db.companys.Add(cmp);
db.SaveChanges();
return RedirectToAction("Index");
}
Model
[Table("Ipv_Base")]
public class Ipv_Base
{
[Key]
public int ipv_baseId { get; set; }
public int companyId { get; set; }
[ForeignKey("companyId")]
public Company company { get; set; }
public string ipType { get; set; }
[Required]
public bool registedAddress { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string existingNotes { get; set; }
[Required]
public int numberOfAddresses { get; set; }
[Required]
public bool returnedAddressSpace { get; set; }
[DataType(DataType.MultilineText)]
public string additionalInformation { get; set; }
// navigation properties
public virtual IList<IpAllocation> requestedIps { get; set; }
}
[Table("Company")]
public class Company
{
[Key]
public int companyId { get; set; }
[Required]
public string name { get; set; }
[Required]
public string telephone { get; set; }
[Required]
public string regNumber { get; set; }
// navigation properties to keep track of the models that belong to the company
public virtual IList<Pa_Ipv4> pa_ipv4s { get; set; }
}
[Table("IpAllocation")]
public class IpAllocation
{
[Key]
public int ipAllocationId { get; set; }
public int ipv_BaseId { get; set; }
[ForeignKey("ipv_BaseId")]
public Ipv_Base ipv_Base { get; set; }
[Required]
public string allocationType { get; set; }
[Required]
public string subnet { get; set; }
[Required]
public string cidr { get; set; }
[Required]
public string mask { get; set; }
}
public class Pa_Ipv4 : Ipv_Base
{
public Pa_Ipv4()
{
ipType = "pa_ipv4";
}
}

Question 1 Solution:
The issue with Q1 was that the property value I was assigning in the controller wasn't being parsed back from the form post, because the property wasn't there.
Added a hidden field for the property to rectify the pesky null.
<div class="ui-block-a">
<span>
#Html.HiddenFor(m => m.allocationType)
#Html.TextBoxFor(m => m.subnet, new { #class = "checkFiller" })
</span>
</div>
Question 2 Solution:
The issues that I was facing with the GUID of the first model being attached as the prefix to the second model was largely due to how I was sending data using AJAX to the controller action method.
The code snippets shown below fix the issues and display the correctly bound GUIDs.
name="pa_ipv4s[f7d8d024-5bb6-451d-87e3-fd3e3b8c1bba].requestedIps[d5c08a43-f65e-46d1-b224-148225599edc].subnet" is now being shown on the dynamically created model properties, not just the initially created.
When running in debug in visual studio and hovering over the model, digging down into the data shows the correct counts of the model lists.
Controller ActionMethod:
public ActionResult ExistingManager(string containerPrefix)
{
ViewData["ContainerPrefix"] = containerPrefix;
return PartialView("ExistingIpView", new IpAllocation { allocationType = "Existing" });
}
AJAX GET Method calling Controller ActionMethod:
$('#addItemEIpM').on('click', function () {
$.ajax({
url: '#Url.Action("ExistingManager")',
cache: false,
data: 'containerPrefix=' + $('#addItemEIpM').data('containerprefix'),
success: function (html) {
$("#editorRowsEIpM").append(html);
}
});
return false;
});

Related

C# Model Binding to Custom Classes Not Working

I am having difficulty getting MVC to bind to a model I have created. I have done this quite a few times in the past successfully. As such, I am just not sure why it is not working in this project.
For example, I have the following View:
#model StoryWall.ViewModels.ViewPostViewModel
#{
ViewBag.Title = "View Post";
}
<article class="story">
<header>
<h1>#Model.story.Title</h1>
<spann class="text-muted">#Model.story.Store.StoreName</span>
<h2>Posted by #Model.story.PosterName</h2>
</header>
if(#Model.story.StoryImage != null) {
<div class="storyImageWrapper">
<img src="~/img/#Model.story.StoryImage" />
</div>
<p>#Model.story.StoryBody</p>
}
</article>
<div class="commentsSection">
<h2>Comments</h2>
<h3>Add a Comment</h3>
<form method="post" class="form-horizontal" name="CommentForm" action="/View/AddComment">
#Html.AntiForgeryToken()
<input type="hidden" name="newComment.StoryID" value="#Model.story.StoryID" />
<div class="form-group"><label class="control-label col-sm-2">Name </label><div class="col-sm-10">#Html.TextBoxFor(#m => m.newComment.CommenterName, new { #class = "form-control", #required = true, #ng_model = "CommenterName"}) <span class="text-warning" ng-show="CommentForm.newComment.CommenterName.$dirty && CommentForm.newComment.CommenterName.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommenterName) </span></div> </div>
<div class="form-group"><label class="control-label col-sm-2">Email </label><div class="col-sm-10">#Html.TextBoxFor(#m => m.newComment.CommenterEmail, new { #class = "form-control", #required = true, #ng_model = "CommenterEmail" }) <span class="text-warning" ng-show="CommentForm.newComment.CommenterEmail.$dirty && CommentForm.newComment.CommenterEmail.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommenterEmail) </span></div> </div>
<div class="form-group"><label class="control-label col-sm-2">Message </label><div class="col-sm-10">#Html.TextAreaFor(#m => m.newComment.CommentBody, new { #class = "form-control", #required = true, #ng_model = "CommentBody" }) <span class="text-warning" ng-show="CommentForm.newComment.CommentBody.$dirty && CommentForm.newComment.CommentBody.$invalid"> Required </span> <span class="text-warning"> #Html.ValidationMessageFor(#m => m.newComment.CommentBody) </span></div> </div>
<button type="submit" ng-disabled="CommentForm.$invalid">Submit</button>
</form>
<h3>Current Comments</h3>
#foreach(var comment in #Model.story.Comments) {
<blockquote>#comment.CommentBody</blockquote>
<span>Poster: #comment.CommenterName on #comment.DatePosted.ToString("MM-dd-yyyy")</span>
}
</div>
Even though I am specifically using Html.TextBoxFor() for my input boxes, the binding is still not working as expected.
This is my Controller. "comment" in the second Action method is not binding correctly; its properties are null.
public class ViewController : Controller
{
StoryModel dbContext = new StoryModel();
public ActionResult ViewPost(Int32 postID)
{
ViewPostViewModel vm = new ViewPostViewModel();
vm.story = dbContext.Stories.FirstOrDefault(s => s.StoryID == postID);
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddComment(Comment comment)
{
if (ModelState.IsValid)
{
dbContext.Comments.Add(comment);
dbContext.SaveChanges();
return RedirectToAction("ViewPost", new { storyID = comment.StoryID});
}
else
{
ViewPostViewModel vm = new ViewPostViewModel();
vm.story = dbContext.Stories.FirstOrDefault(s => s.StoryID == comment.StoryID);
vm.newComment = comment;
return View("ViewPost", vm);
}
}
}
I know this is not the first time a similar question has been asked, but I could not find a solution that solved my problem. Additionally, as stated, this is something I have done in the past with success.
The only "new" element in this scenaria for me is Angular.js. This is my first time using the framework. Could it be interfering with the binding somehow?
If it helps, the Comment model:
public partial class Comment
{
public int CommentID { get; set; }
public int? StoryID { get; set; }
public int UserID { get; set; }
[Required]
[StringLength(75)]
public string CommenterName { get; set; }
[Required]
[StringLength(75)]
public string CommenterEmail { get; set; }
[Required]
public DateTime DatePosted { get; set; }
[Required]
public string CommentBody { get; set; }
public virtual Story Story { get; set; }
public virtual User User { get; set; }
}
and the ViewPostViewModel
public class ViewPostViewModel
{
public Story story { get; set; }
public Comment newComment { get; set; }
}
}
Thanks much for any help.
One answer would be to use #Html.EditorFor()
#Html.EditorFor(m => m.newComment)
Then on the folder where the view is placed you create a new folder called EditorTemplates with a view that is named exactly as the object type. In this case, Comment.cshtml
The view could be something like this ->
#model StoryWall.ViewModels.Comment
#Html.TextBoxFor(m => m.CommenterName)
#Html.TextBoxFor(m => m.CommenterEmail)
#Html.TextAreaFor(m => m.CommentBody)
This approach is the one I normally use to work with lists (useful in surveys or tests) but it also works with a single item.
Another approach could be to just add everything to the viewmodel since the viewmodel doesn't need to be a one to one mapping of the business objects or the database models. :)
Edit: Forgot to add. I think using this approach the method that receives the post will have to receive the whole ViewModel instead of just the comment. ->
public ActionResult AddComment(ViewPostViewModel vm)

Strongly-typed view for model class with collection

I am trying to do something like personal blog and i encountered a problem. I have strongly typed form for edit of my article. I am able to update simple columns like title, content, ... But I have no idea how to handle collection of tags which is mapped as many-to-many collection. What is the best practice here? Can i use some HTML helper like those for simple columns? Or need I to create a new collection everytime? Honestly I have no idea.
Model class
public class Post : IEntity
{
public virtual int Id{ get; set; }
[Required(ErrorMessage = "Každý článek musí mít titulek")]
[MaxLength(250, ErrorMessage ="Nadpis může mít maximálně 250 znaků")]
public virtual string Title { get; set; }
public virtual string Annotation { get; set; }
[AllowHtml]
public virtual string Content { get; set; }
public virtual User Author { get; set; }
public virtual DateTime CreationDate { get; set; }
public virtual Rating Rating { get; set; }
public virtual string PreviewImageName { get; set; }
public virtual string ContentImageName { get; set; }
public virtual Category Category { get; set; }
public virtual IList<Tag> Tags { get; set; }
public virtual IList<BlogImage>Gallery { get; set; }
}
}
So far i was able to do all CRUDs with html helpers like these.
<div class="form-group">
<div class="col-sm-10">
<label>Anotace</label>
</div>
<div class="col-sm-10">
#Html.TextAreaFor(x => x.Annotation, new { #class = "form-control", #rows = 5 })
#Html.ValidationMessageFor(x => x.Annotation)
</div>
</div>
<div class="form-group">
<div class="col-sm-10">
<label>Obsah</label>
</div>
<div class="col-sm-10">
#Html.TextAreaFor(x => x.Content, new { #class = "form-control formatedText", #rows = 20 })
#Html.ValidationMessageFor(x => x.Content)
</div>
</div>
You basically need to use some client side javascript to make intuitive user interface to handle this collection properties which user adds. It could be one item or 100 items.
Here is a very simple way of doing it which allows user to enter each tag for the post in a textbox. user will be able to dynamically add a textbox to enter a tag name for the post.
Assuming you have a view model like this for your post creation.
public class PostViewModel
{
public int Id { set; get; }
public string Title { get; set; }
public List<string> Tags { set; get; }
}
Your view will be strongly typed to this view model
#model PostViewModel
<h2>Create Post</h2>
#using (Html.BeginForm("Create","Post"))
{
#Html.LabelFor(g=>g.Title)
#Html.TextBoxFor(f=>f.Title)
<button id="addPost">Add Tag</button>
<label>Enter tags</label>
<div id="tags">
<input type="text" class="tagItem" name="Tags" />
</div>
<input type="submit"/>
}
You can see that i have a div with one input element for the tag and a button to add tag. So now we have to listen to the click event on this button and create a copy of the textbox and add it to the dom so user can enter a second tag name. Add this javascript code to your page
#section scripts
{
<script>
$(function() {
$("#addPost").click(function(e) {
e.preventDefault();
$(".tagItem").eq(0).clone().val("").appendTo($("#tags"));
});
});
</script>
}
The code is self explanatory. When the button is clicked, it clone the textbox for tag name entry and add it to our container div.
Now when you submit the form, the Tags property will be filled with the tag names user entered in those textboxes. Now you just need to read the values posted and save that to the database.
[HttpPost]
public ActionResult Create(PostViewModel model)
{
var p = new Post { Title = model.Title };
//Assign other properties as needed (Ex : content etc)
p.Tags = new List<Tag>();
var tags = db.Tags;
foreach (var item in model.Tags)
{
var existingTag = tags.FirstOrDefault(f => f.Name == item);
if (existingTag == null)
{
existingTag = new Tag {Name = item};
}
p.Tags.Add(existingTag);
}
db.Posts.Add(p);
db.SaveChanges();
return RedirectToAction("Index","Post");
}

Check from razor view if derived System.ComponentModel.DataAnnotations.ValidationAttribute exists

I'm implementing Html.EditorForModel() so that it's Bootstrap friendly. I have editor templates for all the data types, and one, Object.cshtml, that wraps each editor template with <div class="form-control"></div> etc.
The issue is that when I have a property on a model that is marked as [ComplexType], I want to then list each property of the child class. This works fine, however, for [DataType(DataType.Upload)], which is an HttpPostedFileBase data type, prop.IsComplexType sees that as a complex data type and ends up listing its properties rather than rendering <input type="file" />
Here's Object.cshtml:
#model dynamic
#{
var modelMetaData = ViewData.ModelMetadata;
var properties = modelMetaData.Properties;
}
#foreach (var prop in properties.Where(p => p.ShowForEdit))
{
string propertyName = prop.PropertyName;
if (prop.TemplateHint == "HiddenInput")
{
#Html.Hidden(propertyName)
}
else
{
if (prop.IsComplexType)
{
<fieldset>
<legend>#propertyName</legend>
<div class="mt-ml-2em">
#foreach (var subProp in prop.Properties)
{
var propertyName1 = subProp.PropertyName;
string fullname = propertyName + "." + propertyName1;
<div class="form-group">
#Html.BootstrapLabel(propertyName1)
#Html.Editor(fullname, MyHelpers.TemplateHelpers.GetTemplateForProperty(subProp))
<p class="help-block">#subProp.Description</p>
#Html.ValidationMessage(fullname, new { #class = "color-red" })
</div>
}
</div>
</fieldset>
}
else
{
<div class="form-group">
#Html.BootstrapLabel(propertyName)
#Html.Editor(propertyName, MyHelpers.TemplateHelpers.GetTemplateForProperty(prop))
<p class="help-block">#prop.Description</p>
#Html.ValidationMessage(propertyName, new { #class = "color-red" })
</div>
}
}
}
My razor view:
#model MyMvc45Template.Areas.SampleTests.Models.Product
#{
ViewBag.Title = "ForModel";
}
#*<h2>BsFormGroupFor</h2>
#Html.BsFormGroupFor(m => m.ProductName)*#
<h2>For Model Test</h2>
#using (Html.BeginForm("ForModel", "FormTests", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.EditorForModel()
<div class="form-group">
<input type="submit" value="Save" class="btn btn-success" />
</div>
}
And my sample classes:
public class Product
{
public int Id { get; set; }
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[HasChildProperties] //I am thinking I can use this custom ValidationAttribute as a flag in Object.cshtml
public Address Address { get; set; }
[DataType(DataType.PhoneNumber)]
public string Phone { get; set; }
[DataType(DataType.Upload)]
public HttpPostedFileBase File { get; set; }
[DataType(DataType.Url)]
public string Url { get; set; }
}
Address class:
[ComplexType]
public class Address
{
[Display(Description = "This is the help block text")]
public string Line1 { get; set; }
public string Line2 { get; set; }
public string City { get; set; }
}
And what the output looks like:
As the image shows, my File property ends up having its members listed rather than rendering my Upload.cshtml:
#model dynamic
#Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue,
new { #class = "form-control", placeholder = ViewData.ModelMetadata.Watermark,
type = "file" })
I was thinking I could use this ValidationAttribute as a flag in Object.cshtml:
/// <summary>
/// This will make the editor template for Object.cshtml list any child properties in an edit form
/// </summary>
public class HasChildProperties : ValidationAttribute
{
}
But I can't find my custom ValidationAttribute in the metadata. How can I access this attribute? Is there a more elegant solution?
I ended up using UIHint:
[UIHint("HasChildProperties")]
public Address Address { get; set; }
if (prop.TemplateHint == "HasChildProperties") {...}

How do I pass this child model reference to a virtual ICollection in parent domain model in ASP.NET MVC?

I'm creating a commenting system for my ASP.NET MVC blog engine using a view form that triggers a basic controller action method:
FORM:
#if (User.Identity.IsAuthenticated)
{
//using (Html.BeginForm())
// {
<div class="new_comment">
<h6 id="shortcodes" class="page-header"><i class="fa fa-file-text-o"></i> Leave a comment</h6>
<div class="hline"></div>
<form class="form-horizontal" action="#Url.Action("CreateComment")" method="post" role="form">
<div class="form-group">
<div class="col-sm-4 col-md-4">
<textarea rows="7" class="form-control" name="Message" placeholder="Your Comment Here..."></textarea>
#Html.AntiForgeryToken()
<input type="hidden" name="Slug" value="#Model.Slug"/>
<input type="hidden" name="PostId" value="#Model.Id"/>
<br/>
</div>
</div>
<div class="form-group">
<input type="submit" value="Post Comment" class="btn btn-primary" style="margin-left: 12px"/>
</div>
</form>
</div>
//}
}
CONTROLLER:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateComment([Bind(Include = "PostId,Message,Username,DatePosted")]Comment comment)
{
var post = db.BlogPosts.Find(comment.PostId);
if (post == null)
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
if (ModelState.IsValid)
{
comment.Username = User.Identity.GetUserId();
comment.DatePosted = DateTimeOffset.Now;
db.Comments.Add(comment);
db.SaveChanges();
}
return RedirectToAction("BlogPostDetails", new { slug = post.Slug});
}
I've set breakpoints aside each of the expressions contained within the if statement and confirmed that none of the data values being passed ("PostId, Message, Username, DatePosted") are null, and that db.SaveChances() is commiting changes to the database. Next, here isModels.BlogPosts...
MODELS:
public class BlogPosts
{
public BlogPosts()
{
this.Comments = new HashSet<Comment>();
}
public int Id { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset? Updated { get; set; }
[AllowHtml]
[Required]
public string Title { get; set; }
public string Slug { get; set; }
[Required]
public string Category { get; set; }
[AllowHtml]
[Required]
public string Body { get; set; }
public string MediaURL { get; set; }
public bool Published { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int PostId { get; set; }
public string Username { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Message { get; set; }
public DateTimeOffset DatePosted { get; set; }
public Nullable<System.DateTimeOffset> Edited { get; set; }
public virtual BlogPosts BlogPost { get; set; }
public virtual ApplicationUser Author { get; set; }
//public int? ParentId { get; set; }
//[ForeignKey("ParentId")]
//public virtual ICollection<Comment> Children { get; set; }
//public string ParentComment { get; internal set; }
}
And here is the view that fails to execute:
VIEW THAT DOES NOT EXECUTE
#foreach (var item in Model.Comments.OrderByDescending(c => c.DatePosted))
{
<div class="comment">
<p>
#if (item.Username == null)
{
<small>By: Anonymous</small><span>|</span>
}
else
{
<small>By: #Html.DisplayFor(modelItem => item.Username)</small><span>|</span>
}
<small>Date: #Html.DisplayFor(modelItem => item.DatePosted)</small>
#if (item.Edited != null)
{
<span>|</span><small>Updated: #Html.DisplayFor(modelItem => item.Edited)</small>
}
</p>
<div>
#Html.DisplayFor(modelItem => item.Message)
</div>
</div>
if (item.Username == User.Identity.GetUserId() || User.IsInRole("Admin") || User.IsInRole("Moderator"))
{
<div>
#Html.ActionLink("Edit", "_EditComment", new { id = item.Id }) <span>|</span>
#Html.ActionLink("Delete", "_DeleteComment", new { id = item.Id })
</div>
}
<br />
<!--<div class="hline"></div>-->
}
<div>
<input type="button" class="btn btn-primary" value="Return to Blog Roll" onclick="location.href = '#Url.Action("BlogIndex")'">
</div>
<br />
#if (User.Identity.IsAuthenticated || User.IsInRole("Admin") || User.IsInRole("Moderator"))
{
<input type="button" class="btn btn-primary" value="Modify Post" onclick="location.href = '#Url.Action("BlogAdmin")'">
<br />
<br />
}
When setting a breakpoint on the first line in the view above: #foreach (var item in Model.Comments.OrderByDescending(c => c.DatePosted)), the reference to
public virtual ICollection<Comment> Comments within the Models.BlogPosts class remains null (which obviously means the logic in my view fails to execute and no comment is posted).
I am new to ASP.NET MVC, EF Code-First, etc., and clearly do not understand how my controller is failing to pass the comment values in the child model to the public virtual ICollection<Comment> Comments in the parent... How is it that Models.Comment as referenced in my CommentCreate controller contains a value, and the very same virtual reference in my Models.BlogPosts does not?
EDIT: After great feedback from several users on both the cosmetic errors and critical errors in my code, as well as helpful ASP.NET MVC resource pointers, I determined that the null references being passed had to do with incorrect property-naming conventions in my domain models. See answer below.
Youre including PostId however the actual property name is Id. Also you need to show what model your view is receiving. If youre concerened with what youre exposing (e.g. Id, why dont you just mark it as a hidden field?).
In the controller pass in nothing but the model that you want to edit, e.g. keep a controller solely for url routing, keep your model solely for your object and your view should be only for the model youre passing in.
Finally figured out that the BlogPost property in public virtual BlogPosts BlogPost in my Models.Comment domain model needs to be renamed to match the domain model's foreign key: public int PostId. The solution was executed as follows:
Changing the property name to Post,
then manually deleting the null PostId values from the Comment table in my database,
and then running update-database -f command in NuGet.

C# MVC - Razor Complex Nested Form Submission not binding

Long time lurker... first time question asker...
I have a complex form which returns null when being submitted. Essentially I am trying to build a database driven forms.
The form contains a list of either sections or questions
A section contains a list of either another section, or questions
Model 1:
public FormViewModel {
public List<FormSetsViewModel> formSets { get; set; }
}
Model 2:
public FormSetsViewModel{
QAViewModel questionAnswerViewModel { get; set; }
SectionViewModel sectionViewModel { get; set; }
bool isQuestion { get; set; }
bool isSection { get; set; }
}
Model 3:
public SectionViewModel {
public List<FormSectionQuestionsViewModel> formSectionQuestions { get; set; }
}
Model 4:
public FormSectionQuestionsViewModel {
public QuestionAnswerViewModel questionAnswers;
public SectionViewModel childSection;
int orderNumber;
}
Model 5:
public QAViewModel {
int id { get; set; }
string answer { get; set; }
string question { get; set;}
}
The views are as follows:
FormViewModel.cshtml
#model FormViewModel
#using (Html.BeginForm("Save", "Forms"))
{
<div class="row">
#Html.EditorFor(model => model.formSetsViewModels)
</div>
<div class="controls">
<input type="submit" value="Confirm" class="button" name="save" />
</div>
}
#model FormSetsViewModel
<div class="control-group">
#if (Model.isQuestion)
{
#Html.EditorFor(m => m.questionViewModel);
}
else
{
#Html.EditorFor(m => m.sectionViewModel);
}
</div>
SectionViewModel.cshtml
#model SectionViewModel
#Html.EditorFor(m => m.formSectionQuestions)
FormSectionQuestionsViewModel.cshtml
#model FormSectionQuestionsViewModel
#if (Model.childSection != null)
{
#Html.EditorFor(m => m.childSection)
}
else
{
#Html.EditorFor(m => m.questionAnswers)
}
QAViewModel.cshtml
#model QAViewModel
<p><div class="question-text-edit">#Html.Raw(Model.questionText)</div>
#Html.TextAreaFor(m => m.answer, new { style = "width: 90%; height: 80px;" })
The controller:
[HttpPost]
public ActionResult Save(int caseID, List<FormSetsViewModel> formSets = null)
{
return Index(caseID);
}
The view works great as a database driven form. However, when I submit the form, it seems that the formsets cannot bind, and returns null.
From Html, it created an input like this:
<input id="formSetsViewModels_d762713a-7a2f-497a-9417-4c6e91d33cb8__sectionViewModel_formSectionQuestions_48e738da-10d3-4518-be59-2493e2b7a7cc__questionAnswers_answer" name="formSetsViewModels[d762713a-7a2f-497a-9417-4c6e91d33cb8].sectionViewModel.formSectionQuestions[48e738da-10d3-4518-be59-2493e2b7a7cc].questionAnswers.answer" type="text" value="">
Finally found the answer!
The variable name for the FormSetsViewModel in the
public ActionResult Save(int caseID, List<FormSetsViewModel> formSets = null)
needs to be formSetsViewModel for the model to be able to be binded.
The other thing is that, some public variables in the class does not have { get; set; } method.
All variables that we want to be bind needs the { get; set; } method. Adding this solve the issue.

Categories