a bit of a strange one here. I've got a blog that's another 'dip my toe into MVC' project that will highlight the benefits of our new Intranet. I have a typical blog post page with the article, comments and a form to submit your comments against a given article.
I have a stringly typed main View ("Post") which has the article content, then two Html.RenderAction methods; one to get the comments and one to add comments via a form.
My issue is that when a comment is posted, my GetComments method is called before AddComment, so when the page is refreshed the new comment isn't visible although it has been added to the database. A quick F5 confirms this. I understand that GetComments is being called first due to it being declared first in the view but I'm not sure how to tell the view to do an add before a get.
Here's my code:
Controller:
public ActionResult AddComment()
{
return PartialView("AddComment");
}
[HttpPost]
public ActionResult AddComment(Comment comment)
{
comment.DateSubmitted = DateTime.Now;
db.Comments.Add(comment);
db.SaveChanges();
return PartialView(comment);
}
public ActionResult GetComments(int articleid)
{
var comments = db.Comments.Where(c => c.ArticleID == articleid).ToList();
return PartialView(comments);
}
Post view
#model IntranetBlog.Models.Article
#{
ViewBag.Title = "Post";
}
<div class="row">
<div class="span12">
<h3>#Html.DisplayFor(modelItem => Model.Title)</h3>
<small>by Ruth Barlow on #Html.DisplayFor(modelItem => Model.DateCreated)</small>
#if (Model.Image != null)
{
<p>
<img src="#Url.Action("GetImage", "Home", new { articleID = Model.ArticleID })" alt="" width="150" height="150" />
</p>
}
<div>
#Html.DisplayFor(modelItem => Model.Body)
</div>
<small>Posted under #Html.DisplayFor(modelItem => Model.Category.Name)</small>
</div>
<div class="span12">
#{
Html.RenderAction("GetComments", "Home", new { articleID = Model.ArticleID });
}
</div>
<div class="span12">
#{
Html.RenderAction("AddComment", "Home", new { articleID = Model.ArticleID });
}
</div>
</div>
GetComments partial:
#model IEnumerable<IntranetBlog.Models.Comment>
#if (Model.Any())
{
<h3>What you're saying</h3>
foreach (var item in Model)
{
<div>
Comment: #Html.DisplayFor(modelItem => item.Body)
</div>
<div>
Submitted by: #Html.DisplayFor(modelItem => item.SubmittedBy)
on #Html.DisplayFor(modelItem => item.DateSubmitted)
</div>
<hr />
}
}
else
{
<p>There are no comments for this post. Why not add one?</p>
}
AddComment partial
#model IntranetBlog.Models.Comment
#using (Html.BeginForm())
{
<h3>Why not leave us a comment?</h3>
#Html.ValidationSummary()
<fieldset>
<div class="editor-label">
#Html.LabelFor(model => model.Body)
</div>
<div class="editor-field">
#Html.TextAreaFor(model => model.Body, 20, 20, null)
#Html.ValidationMessageFor(model => model.Body)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.SubmittedBy)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.SubmittedBy)
#Html.ValidationMessageFor(model => model.SubmittedBy)
</div>
<p>
<input type="submit" value="Add comment" id="AddComment" class="btn btn- primary" />
</p>
</fieldset>
}
Hope this makes sense.
Thanks in advance.
The trick is to use Html.Action instead of Html.RenderAction this will allow you to store the result in a variable and then add it to the display where it is needed.
This will allow you to generate the PartialView in the logical order you need, while displaying them in an other order.
See this post for a quick example on how to do it : https://stackoverflow.com/a/13301057/971693
I would suggest to you a slightly different approach. Instead of using Html.BeginForm, think about using Ajax.BeginForm to submit the comment. Being ajax, it will have better performance as it allows your method to just return the comments so you can replace the old ones, or even just the newly added one so it can be added to the bottom of the list. Having said that, this solution does rely on the use of javascript and preferably jquery-unobtrusive-ajax to work and render decent looking client code. You don't have to know javascript to use this method thanks to the AjaxOptions class which has some powerful yet easy to use options built into it.
Related
I have a partial view which shows a search box with a couple of options
model PatientSearchViewModel
#using (Html.BeginForm("Index", "Patient", FormMethod.Post, new { #class = "form-inline" }))
{
<form class="form-inline">
<label style="padding-right:20px;">Search for a patient</label>
#Html.EditorFor(model => model.SearchText, new { htmlAttributes = new { #class = "form-control", placeholder = "Search...", autocomplete = "off" } })
<div class="checkbox">
<div class="checkbox" style="padding-left:20px; padding-right:20px;">
<label>#Html.EditorFor(model => model.ShowOnlyOpenClients) Show only open clients</label>
</div>
</div>
<button type='submit' name='seach' id='search-btn' class="btn btn-primary"><i class="fa fa-search"></i>Search</button>
</form>
}
When the submit button is hit I want it to go to my method below with the search content already complete, however instead it goes to the HttpGet method which is bringing back a null for the PatientSearchViewModel.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Index(PatientSearchViewModel patient)
{
PatientSearchViewModel patientVM = GetSearchResults(patient);
return View(patientVM);
}
Can anyone explain how I can get this to work?
I have executed the code snippet you pasted here and found that
As you are making the use of the [ValidateAntiForgeryToken]
attribute so you need to add the #Html.AntiForgeryToken(); this
statement in your partial view inside the
#using(Html.BeginForm()){ } block.
One more thing make sure that your action method will be inside the
PatientController.
For your reference (this is my code)
#model WebApplication.Models.PatientSearchViewModel
#using (Html.BeginForm("Index", "Patient", FormMethod.Post, new { #class = "form-inline" }))
{
#Html.AntiForgeryToken();
<label style="padding-right:20px;">Search for a patient</label>
#Html.EditorFor(model => model.SearchText, new { htmlAttributes = new { #class = "form-control", placeholder = "Search...", autocomplete = "off" } })
<div class="checkbox">
<div class="checkbox" style="padding-left:20px; padding-right:20px;">
<label>#Html.EditorFor(model => model.ShowOnlyOpenClients) Show only open clients</label>
</div>
</div>
<button type='submit' name='seach' id='search-btn' class="btn btn-primary"><i class="fa fa-search"></i>Search</button>
}
Suggestion: As you have used the #using(Html.BeginForm()) which will any way going to convert to <form> tag so instead of using another <form class="form-inline"> inside it is creating the confusion please you may remove it.
I hope this will solve your problem.
I have created a ViewModel with two things, a Contact and a list of Phones for that Contact.
My goal is to add data for a new Contact, and add a few Phones, and then save by a Controller action.
I've edited the scaffolded Create.cshtml for my Contact, added a grid for the phones. Added a javascript for creating the phones. So far so good.
The problem is when I click the Create button, and I get back to the Controller, I get no Phones. How do I (in the View) add the phone-rows to my IEnumerable?
EDIT:
Took out code in view that was not correct in this context.
My ViewModel:
public class ContactViewModel
{
public Contact Contact {get; set;}
public IEnumerable<Phone> Phones { get; set; }
}
My view:
#model PilotKibsNet.Controllers.ContactViewModel
<script type="text/javascript" src="../../Scripts/jquery-1.5.1.js"></script>
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<script type="text/javascript">
function Add() {
$("#tbl > tbody:last").append("<tr><td>" + $("#Number").val() + "</td><td>" + $("#Kind").val() + "</td><td></td></tr>");
$("#Number").val("");
$("#Kind").val("");
}
</script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Contact</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Contact.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Contact.Name)
#Html.ValidationMessageFor(model => model.Contact.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Contact.Address)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Contact.Address)
#Html.ValidationMessageFor(model => model.Contact.Address)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Contact.City)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Contact.City)
#Html.ValidationMessageFor(model => model.Contact.City)
</div>
<legend>Phone numbers</legend>
<label>Number :</label>
#Html.TextBox("Number")
<label>Kind :</label>
#Html.TextBox("Kind")
<input type="button" value="Add" onclick="Add()" />
<table id="tbl">
<tr>
<th>
Phone
</th>
<th>
Kind
</th>
<th></th>
</tr>
<tbody>
</tbody>
</table>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
And then, in by Controller action, the Contact has the data, but the Phone is an empty list.
[HttpPost]
public ActionResult Create(ContactViewModel contactViewModel)
{
if (ModelState.IsValid)
{
contactViewModel.Contact.id = Guid.NewGuid();
db.Contacts.AddObject(contactViewModel.Contact);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(contactViewModel.Contact);
}
How do I get the Phones back to the server?!?
You have only display templates for those Phones collection. No value at all will be sent to the server. You could use hidden fields if the user is not supposed to edit the values or textboxes if he is.
Also I would replace this foreach loop in your view by an editor template:
if (Model != null)
{
#Html.EditorFor(x => x.Phones)
}
and then I will define an editor template which would be rendered for each element of the Phones collection (~/Views/Shared/EditorTemplates/Phone.cshtml):
#model Phone
<tr>
<td>
#Html.DisplayFor(x => x.Number)
#Html.HiddenFor(x => x.Number)
</td>
<td>
#Html.DisplayFor(x => x.Type)
#Html.HiddenFor(x => x.Type)
</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = Model.id }) |
#Html.ActionLink("Details", "Details", new { id = Model.id }) |
#Html.ActionLink("Delete", "Delete", new { id = Model.id })
</td>
</tr>
I have used hidden fields here to persist the values of the model so that when you post the form to the server they would be sent.
Another and IMHO better approach if the user is not supposed to edit those values in the table is to simply refetch them in your POST action from your database.
I have a form in a view like so:
#using (Html.BeginForm("CreateDTActionBasedOnSelectedMetaAction", "TestCase", FormMethod.Post))
And an action method with the following signature:
[AcceptVerbs( new string[]{"GET","POST"})]
public void CreateDTActionBasedOnSelectedMetaAction(FormCollection fc)
However, when the 'submit' button (located in the form) is clicked, it comes to the action method, but the Request.HttpMethod property shows a "GET", and obviously the form data is then not available in the FormCollection object as it wasn't posted.
Any thoughts?
UPDATE:part of the View:
#using (Html.BeginForm("CreateDTActionBasedOnSelectedMetaAction", "TestCase", FormMethod.Post)){
<fieldset>
<legend>Test Case</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
<p>#DTContext.CurrentTestCase.Name</p>
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Criteria)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Criteria)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DTTestCaseReqSet.DTMetaReqProcessor.DTMetaActions)
</div>
<div class="editor-field">
#Html.ListBox("MyListBox", new SelectList(Model.DTTestCaseReqSet.DTMetaReqProcessor.DTMetaActions.Where(p => p.Enabled == true), "NameWithID", "NameWithID"));
</div>
<p>
<input type="submit" value="Select" />
</p>
</fieldset>
}
UPDATE2:
Okay that was silly. Turns out that the app has a custom routing system written by another dev, which expected a certain parameter in the query string to be preserved, which my code wasn't doing. This resulted in the routing system taking the POST from the form, being unable to find a suitable method, it converted it to a GET, which then found my actionmethod.
I would request this question to be deleted.
Create two action methods. One for get and one for post.
[HttpPost]
public void CreateDTActionBasedOnSelectedMetaAction(FormCollection fc)
[HttpGet]
public void CreateDTActionBasedOnSelectedMetaAction()
I am new to MVC, so please forgive me if I am being so noob :).
In my new project, I want to make a 'Create Song' page which, of course, creates a new song. In model design, one song can have many artists. So, on the create song page, a user should be able to search for artists and add artists from the search results by pressing the 'Add' links of artists in the search result.
Currently, I have two submit buttons in form which are "Submit" and "SearchArtist" and they call http POST "Create" action method. A ViewModel object(CreateSongScrnData) is used to post the screen data. And it's all working ok. But when I try to add an artist from Artist search result, using #Url.Action passing ViewModel data to "Create" http get method, the viewmodel object parameter is always null when it come into the actiona method. Please help me with some advice and enlighten me if I am missing some concepts. Thank you so much in advance.
Code is as follows.
public ActionResult Create(CreateSongScrnData modelData, Int32? artistIDToAdd)
{
if (modelData != null && artistIDToAdd != null)
{
var artist = db.Artists.Find(artistIDToAdd);
modelData.Song.Artists.Add(artist);
}
ViewBag.GenreID = new SelectList(db.Genres, "GenreID", "Name");
ViewBag.AlbumID = new SelectList(db.Albums, "AlbumID", "Name");
return View(modelData);
}
[HttpPost]
public ActionResult Create(CreateSongScrnData modelData)
{
switch (modelData.SubmitCommand)
{
case "AddNewSong":
if (ModelState.IsValid)
{
db.Songs.Add(modelData.Song);
db.SaveChanges();
return RedirectToAction("Index");
}
break;
default:
modelData.SearchResult = db.Artists.Where(a => a.Name.Contains(modelData.ArtistSearchString)).ToList();
break;
}
ViewBag.GenreID = new SelectList(db.Genres, "GenreID", "Name", modelData.Song.GenreID);
ViewBag.AlbumID = new SelectList(db.Albums, "AlbumID", "Name", modelData.Song.AlbumID);
return View(modelData);
}
Code snippet of search in the view is as follows:
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<div class="editor-label">
#Html.LabelFor(model => model.Song.Name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Song.Name)
#Html.ValidationMessageFor(model => model.Song.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Song.MyanmarName)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Song.MyanmarName)
#Html.ValidationMessageFor(model => model.Song.MyanmarName)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Song.Genre)
</div>
<div>
#Html.DropDownList("GenreID", "Choose Genre")
#Html.ValidationMessageFor(model => model.Song.GenreID)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Song.AlbumID, "Album")
</div>
<div class="editor-field">
#Html.DropDownList("AlbumID", "Choose Album")
#Html.ValidationMessageFor(model => model.Song.AlbumID)
</div>
<div class="editor-label">
<p>Sing by this artists:</p>
#if (Model.AddedArtists != null)
{
<table><tr><th>Name</th></tr>
#foreach (var item in Model.AddedArtists)
{
<tr><td>#item.Name</td></tr>
}
</table>
}
</div>
<div style=" border-top:1px solid #ccc; width: 400px; height:auto; margin-top:30px; margin-bottom:30px;"></div>
<div>
<p>Search and add artists</p>
#Html.TextBoxFor(model => model.ArtistSearchString)
<input type="submit" name="SubmitCommand" value="SearchArtist" class="cancel" />
#if (Model.SearchResult != null)
{
<div>
<table><tr><th>Name</th><th></th></tr>
#foreach (var item in Model.SearchResult)
{
<tr><td>#item.Name</td>
<td>Add</td>
</tr>
}
</table>
</div>
}
</div>
<div style=" border-top:1px solid #ccc; width: 400px; height:auto; margin-top:30px; margin-bottom:30px;"></div>
<p>
<input type="submit" name="SubmitCommand" value="AddNewSong" />
</p>
}
I think you are mixing too things in a single form.
Solution 1)
Create a first step where user insert data and create song.
Create a second step where user add artists to the song.
Solution 2)
Let to the submit button the function to store data.
For the search of artist use a ajax compnent like this:
http://harvesthq.github.com/chosen/
I have a View Model that consists of an Applicant object and a TeamMember collection. When I post the model back the Team collection is always null. I've tried changing the collection from my original IEnumarable to a List but that didn't make a difference. So I changed the Controllers Edit Action to accept the FormCollection, and verified there was data in viewModel["member.FirstName"]. I'm lost as to why the binding isn't working. I tried to clean out my code samples as much as possible but I'm confused at what I'm missing. Any help is greatly appreciated!
View Model Properties
public class MyViewModel
{
public Applicant ApplicantInfo { get; set; }
public List<TeamMember> TeamMembers { get; set; }
}
Controller
[HttpPost]
public ActionResult Edit(MyViewModel viewModel)
{
// viewModel.ApplicantInfo has the form data
// viewModel.TeamMembers = null
}
View
<% using (Html.BeginForm())
{%>
<h3>
Applicant Information
</h3>
<label>
City
<%: Html.TextBoxFor(m => Model.ApplicantInfo.City)%>
</label>
<label>
State
<%: Html.TextBoxFor(m => Model.ApplicantInfo.State)%>
</label>
<h3>
Team
</h3>
<div>
<% foreach (var member in Model.TeamMembers)
{ %>
<div class="editor-field">
<%: Html.DropDownList("member.Type", Model.GetMemberTypes(member.MemberType.TypeId))%>
</div>
<div class="editor-field">
<%: Html.EditorFor(m => member.FirstName)%>
</div>
<div class="editor-field">
<%: Html.EditorFor(m => member.LastName)%>
</div>
<div class="editor-field">
<%: Html.EditorFor(m => member.Title)%>
</div>
<%} %>
</div>
<p>
<input type="submit" value="Save" />
</p>
<% } %>
I believe that input tags associated with items in a collection (when the model itself is not a collection) need to have an index in the name attribute before you can bind posted data to a view model. Here is the way I usually accomplish this...
<% for (int i=0; i<Model.TeamMembers.Count; i++) { %>
<div class="editor-field">
<%: Html.EditorFor(m => m.TeamMembers[i].FirstName)%>
</div>
<div class="editor-field">
<%: Html.EditorFor(m => m.TeamMembers[i].LastName)%>
</div>
<% } %>
I've also used the template as suggested by Shea, but I have a tad more code trying to force it to render brackets/indexes.
<% foreach (var member in Model.TeamMembers) { %>
<%: Html.EditorFor(x =>
member,
"TeamMember",
"TeamMembers["+(member.Number-1)+"]",
new { MemberTypes = Model.GetMemberTypes(member.MemberType.TypeId) })%>
<% } %>
Here is an old but still relevant article from Phil Haack on the topic.
Try using this:
<%: Html.EditorFor(m => m.TeamMembers) %>
Then, make a shared editor template with a model type of TeamMember. MVC should handle binding everything back to your viewmodel on post for you.