I'm using an AJAX form to update an item to the database. When it gets done, it returns a partial view that re-lists all the items and displays them all in a table. The problem occurs when I have to add a modelstate error in my controller action. I don't want to return the list of items when there is a modelstate error because I want to show the user the error using the ValidationMessage. My thinking is that I could do something like this in my controller:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UpdateNewsItem(int newsID, string newsTitle, string newsDescription, string newsBeginningDate, string newsEndingDate)
{
List<Models.News> lstNewsItem = new List<News>();
//we need to grab the member so we can capture the user id
//for the corresponding news property
MembershipUser member = Membership.GetUser(User.Identity.Name);
//the news instance to use in case the viewdata is invalid
Models.News newsError = new Models.News();
//create the datetime objects
DateTime dtBeginningDate = DateTime.MinValue;
DateTime dtEndingDate = DateTime.MaxValue;
//the message we want to send whenever the user enters an invalid date
string strInvalidDateError = "Invalid date. Please use a format like '12/25/2008'";
//clean user input
newsTitle = Models.clsGlobals.CleanString(newsTitle);
newsDescription = Models.clsGlobals.CleanParagraph(newsDescription);
//newsTitle
if (string.IsNullOrEmpty(newsTitle))
{
newsError.Title = string.Empty;
ModelState.AddModelError("newsTitle", "You must enter a news title.");
}
//description
if (string.IsNullOrEmpty(newsDescription))
{
newsError.Description = string.Empty;
ModelState.AddModelError("newsDescription", "You must enter a news description.");
}
//beginningDate
if (string.IsNullOrEmpty(newsBeginningDate))
{
ModelState.AddModelError("newsBeginningDate", "You must enter a beginning date.");
}
//endingDate
if (string.IsNullOrEmpty(newsEndingDate))
{
ModelState.AddModelError("newsEndingDate", "You must enter an ending date.");
}
//set the beginning date
try
{
dtBeginningDate = DateTime.Parse(newsBeginningDate);
newsError.BeginningDate = dtBeginningDate;
}
catch (FormatException)
{
ModelState.AddModelError("newsBeginningDate", strInvalidDateError);
}
//set the ending date
try
{
dtEndingDate = DateTime.Parse(newsEndingDate);
newsError.EndingDate = dtEndingDate;
}
catch (FormatException)
{
ModelState.AddModelError("newsEndingDate", strInvalidDateError);
}
//data is validated, so we can begin the update
if (ModelState.IsValid == true)
{
try
{
//use to perform actions on db
Models.NewsDataContext dcNews = new Models.NewsDataContext();
//fetch the items that match what the user requested to edit
lstNewsItem = this.GetNewsItem(newsID);
//set news properties
foreach (Models.News news in lstNewsItem)
{
news.UserId = (Guid)member.ProviderUserKey;
news.Title = newsTitle;
news.Description = newsDescription;
news.EntryDate = DateTime.Now;
news.BeginningDate = dtBeginningDate;
news.EndingDate = dtEndingDate;
}//next
//update the transaction
dcNews.SubmitChanges();
//update the news list
return PartialView("NewsList", this.GetNewsItems());
}
//just to make sure everything goes as planned,
// catch any unhandled exceptions
catch (Exception ex)
{
ModelState.AddModelError("_FORM", ex);
}//end catch
}//end if valid modelstate
//invalid modelstate, so repopulate the viewdata and
//send it back
//the list to hold the entries
List<Models.News> lstErrorNewsItems = new List<Models.News>();
//set the remaining error properties
newsError.UserId = (Guid)member.ProviderUserKey;
newsError.NewsID = newsID;
newsError.EntryDate = DateTime.Now;
//add the item--there will only be one
//but the view is expecting a list so we will
//treat it like one
lstErrorNewsItems.Add(newsError);
return PartialView("EditNews", lstErrorNewsItems);
}//end actionresult
The problem is that when a modelstate error occurs, the modelstate viewdata isn't returned. I suspect it's possibly because I'm not specifying an update target id. But I can't set another updatetargetid because I already have one. Any ideas?
if (!Model.IsValid) {
return PartialView("YourEditForm");
}
else
{
return View("YourIndexView");
}
Should work fine for redisplaying your edit form with validation errors and all.
ViewData gets populated from Post data.
Big Update
I've made some testing to figure it out. And came up with a working test project.
here are some listings:
My Testing controller
public class TestController : Controller
{
//
// GET: /Test/
List<TestData> data;
public TestController()
{
data = new List<TestData>();
for (var i = 0; i < 10; i++)
{
data.Add(new TestData(){Name=string.Format("TestData{0}",i.ToString().PadLeft(4,'0'))});
}
}
public ActionResult Index()
{
return View( data);
}
public ActionResult Edit(string name)
{
if (Request.IsAjaxRequest())
{
return PartialView("AjaxEdit", new TestData() { Name = name });
}
return View(new TestData() { Name = name });
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(TestData testData)
{
ModelState.AddModelError("name", "incorrect name");
if (!ModelState.IsValid)
{
if (Request.IsAjaxRequest())
{
return PartialView("AjaxEdit");
}
}
return View();
}
}
My Edit View:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<AjaxValidationPartial.Models.TestData>" %>
<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
Edit
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Edit</h2>
<div id="editForm">
<% Html.RenderPartial("AjaxEdit",Model );%>
</div>
</asp:Content>
My Ajax Partial Edit View
" %>
<%= Html.ValidationSummary("Edit was unsuccessful. Please correct the errors and try again.") %>
<% using (Ajax.BeginForm(new AjaxOptions { UpdateTargetId = "editForm" }))
{%>
<fieldset>
<legend>Fields</legend>
<p>
<label for="Name">Name:</label>
<%= Html.TextBox("Name")%>
<%= Html.ValidationMessage("Name", "*")%>
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
<div>
<%=Html.ActionLink("Back to List", "Index") %>
</div>
And my TestData model class
public class TestData
{
public string Name { get; set; }
}
with this code it works just as you wanted.
notice that i do not pass any model to partial view in my "Post" Edit action.
View that gets rendered gets all the values it needs from a post request.
Alright, I've figured it out. Thanks for the response. For future reference, what I had to do was set up one master divider that all of my content goes into. Then, I always set the updateTargetID to that divider so that it doesn't matter what content it displays, just that it displays it. This actually turns out to be easier because you don't have to waste Javascript functions setting other div tags on and off because you're only using one that is continuously updated.
Related
I am building a store locator for an online ordering service. I have a view and a controller. My view displays a list of stores based on a zip code search. from this list, you can click a button to place an order with that specific store. I have it linking to a new view where I want to display details about the specific store you selected. How can I do this?
<div class="store-list-box">
#{
foreach (var store in (List<Site>)ViewData["sites"])
{
<div class="store-list-style">
<h3>#store.name</h3>
<p>#store.address1</p>
<p>#store.city, #store.state #store.zip</p>
<p>#store.phone</p>
<button class="btn" type="submit" onclick='window.location = "#Url.Action("StoreInfo", "OrderTime")";'>Place Order</button>
</div>
<hr />
}
}
</div>
This is the code I have where it displays the list of stores.
public ActionResult Index(string search)
{
List<Site> result = SiteMgmt.GetByZip(search);
var emptyResult = new List<Site>();
if (string.IsNullOrEmpty(search))
{
View().ViewData["sites"] = emptyResult;
}
else
{
View().ViewData["sites"] = result;
}
return View();
}
This is the controller which displays the stores. On button click you go to the new view. How do I show the specific store details in that new view?
This is a screenshot of the list I currently display
Here is the StoreInfor() code.
public class OrderTimeController : Controller
{
public ActionResult StoreInfo(int siteID)
{
Site result = SiteMgmt.GetSite(siteID);
var emptyResult = new Site();
if (siteID == 0)
{
View().ViewData["sites"] = emptyResult;
}
else
{
View().ViewData["sites"] = result;
}
return View();
}
}
have 2 action with same name.(AddNewUser) one of them work with HttpGet and another with HttpPost.
[HTTPGet]
public ActionResult AddNewUser()
{
User user = Utilities.SessionProvider.GetCurrentUser();
if (user.ID_User == 0)
return Redirect("Apps.kosarfci.ir");
RoleType role = (RoleType)RoleDeterminer.RoleDeterminate();
if (role != RoleType.Center)
{
return RedirectToAction("Restriction");
}
return View("VNewUser");
}
[HttpPost]
public ActionResult AddNewUser(VMNewUser InModel)
{
User user = Utilities.SessionProvider.GetCurrentUser();
if (user.ID_User == 0)
return Redirect("Apps.kosarfci.ir");
RoleType role = (RoleType)RoleDeterminer.RoleDeterminate();
if (role != RoleType.Center)
{
return RedirectToAction("Restriction");
}
IUserBL centerUserBL = new CenterUserBL();
InModel.User.UserName = InModell.User.NationalCode;
InModel.User.Password = InModell.User.PersonalCode;
bool confirmedBL = centerUserBL.AddUser(InModel.User);
_msgList.Add(new Message() { MsgType = MessageType.Success, MsgContent = MessageProvider.GetMessage(MessageContent.Submit_Success_NewUser) });
ViewBag.Message = _msgList;
return View("VNewUser");
}
AddNewUser() ,return a form with input entry and then form submited to AddNewUser(VMNewUser InModel). but after that a form with filled entry with posted model is displayed. i expect that a form with blank input entry display because i dont send pre-filled model(VMNewUser) in to the view.
why?
--VMNewUser--
#model PersonManagement.Views.User.VMNewUser
<style>
table{
font-family:Tahoma;
}
</style>
#using (Html.BeginForm("AddNewUser", "User", FormMethod.Post))
{
<table>
<tr>
<td>name</td>
<td>#Html.TextBoxFor(f=>f.User.FirstName)</td>
</tr>
<tr>
<td>family</td>
<td>#Html.TextBoxFor(f=>f.User.LastName)</td>
</tr>
<tr>
<td>personalcode</td>
<td>#Html.TextBoxFor(f=>f.User.PersonalCode)</td>
</tr>
<tr>
<td>nationalcode</td>
<td>#Html.TextBoxFor(f=>f.User.NationalCode)</td>
</tr>
<tr>
<td colspan="2"><input type="submit" value="create" style="font-family:Tahoma;float:left" /></td>
</tr>
</table>
}
The main reason for sending back the populated model is to prevent the user from having to re-populate the form if there is an error processing the request server-side. It also provides the client with information about what fields failed and if there are any particular error messages that should be displayed.
It's recommended you use the PRG pattern therefore if the request is successful, you would redirect the user to a new page. In your case, you could redirect the user back to AddNewUser action which would present the user with the empty form again e.g.
return RedirectToAction("AddNewUser");
Way 1:
In your HttpPost method return to Action instead of returning view like this:
return RedirectToAction("AddNewUser");
Way 2:
or simply return View() without passing Model object which means null model and the fields will not be populated obviously:
return View();
Way 3 :
you can also do like this, if Model is valid save data and return View with empty fields, otherwise return view with populated data to user.
Like this :
[HttpPost]
public ActionResult AddNewUser(VMNewUser InModel)
{
if(ModelState.IsValid)
{
User user = Utilities.SessionProvider.GetCurrentUser();
if (user.ID_User == 0)
return Redirect("Apps.kosarfci.ir");
RoleType role = (RoleType)RoleDeterminer.RoleDeterminate();
if (role != RoleType.Center)
{
return RedirectToAction("Restriction");
}
IUserBL centerUserBL = new CenterUserBL();
InModel.User.UserName = InModell.User.NationalCode;
InModel.User.Password = InModell.User.PersonalCode;
bool confirmedBL = centerUserBL.AddUser(InModel.User);
_msgList.Add(new Message() { MsgType = MessageType.Success, MsgContent = MessageProvider.GetMessage(MessageContent.Submit_Success_NewUser) });
ViewBag.Message = _msgList;
return View();
}
else
{
return View(InModel);
}
}
I have a page with 2 input type=text..
#model MVC3.ViewModel.TalkToUsVM
#using (Html.BeginForm())
{
<ul>
<li>#Html.TextBoxFor(m => m.TalkToUsRequest.Name)</li>
<li>#Html.TextBoxFor(m => m.TalkToUsRequest.Email)</li>
</ul>
<input type="submit" value="Save"/>
}
in my controller I do this:
[HttpPost]
public ActionResult Create(TalkToUsRequest talkToUsRequest)
{
var vm = new TalkToUsVM();
if (TryValidateModel(talkToUsRequest))
{
vm.Result = "Success";
return View("Create",vm);
}
vm = new TalkToUsVM
{
Result = "Errrooooooor",
TalkToUsRequest = talkToUsRequest
};
return View(vm);
}
so the problem.. when my model is valid, I set the result to "Success" and in this point vm.TalkToUsRequest is null.. but when page is rendered, all fields are with the same value that when I submited.. even I setting vm.TalkToUsRequest = null!!
How can I clear this fields?
So in this scenario you have to clear your model state if you return back to the same view.
Try following:
ModelState.Clear();
return View(vm);
}
Your answer :
TryUpdateModel(yourmodelname);
and it will update your view state
and if you also want to clear all Modelstate.error at the same time you can
also use :
ModelState.Clear();
Following this example .. http://msdn.microsoft.com/en-us/data/gg685489
I am running into issues with Delete functionality.
[HttpPost]
public ActionResult Delete(int id, Blog blog)
{
try
{
using (var db = new BlogDataEntities())
{
//Having issue here.. as soon as the next line is run in debug
//mode .. it goes to catch.. and returns a view with null values.
db.Entry(blog).State = System.Data.EntityState.Deleted;
db.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
In the parameters I checked 'blog' does not get the actual blog model that needs to be deleted. All the other methods work fine (Edit, Delete (get)..etc..
but Delete post fails. Am I missing something? thanks in advance for the help.
EDIT:
view code
#model DBFirstMVC.Models.Blog
#{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
<fieldset>
<legend>Blog</legend>
<div class="display-label">Title</div>
<div class="display-field">#Model.Title</div>
<div class="display-label">BloggerName</div>
<div class="display-field">#Model.BloggerName</div>
</fieldset>
#using (Html.BeginForm()) {
<p>
<input type="submit" value="Delete" /> |
#Html.ActionLink("Back to List", "Index")
</p>
}
EDIT 2:
Non Razor Code in view:
<% using (Html.BeginForm()) { %>
<p>
<input type="submit" value="Delete" /> |
<%: Html.ActionLink("Back to List", "Index") %>
</p>
<% } %>
EDIT 3: (I tried in aspx)
<% using (Html.BeginForm()) { %>
<p>
<%=Html.DisplayForModel();%> //Tried Html.EditorForModel also..
<input type="submit" value="Delete" /> |
<%: Html.ActionLink("Back to List", "Index") %>
</p>
<% } %>
FINAL EDIT (Corrected Solution)
#model DBFirstMVC.Models.Blog
#{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h3>Are you sure you want to delete this?</h3>
#using (Html.BeginForm()) {
<p>
<fieldset>
<legend>Blog</legend>
<div class="display-label">Title</div>
<div class="display-field">#Model.Title</div>
<div class="display-label">BloggerName</div>
<div class="display-field">#Model.BloggerName</div>
<input type="submit" value="Delete" /> |
#Html.ActionLink("Back to List", "Index")
</fieldset>
</p>
}
The context probably doesn't have an Entry for your Blog because it isn't attached to the Context.
You probably need to retrieve the Blog first and then mark it as deleted using the Entry method:
[HttpPost]
public ActionResult Delete(int id, Blog blog)
{
try
{
using (var db = new BlogDataEntities())
{
// retrieve the blog from the database
var realBlog = db.Blogs.Find(blog.Id);
// nothing to do here, just redirect
if( realBlog == null )
return RedirectToAction("Index");
// since you have the entity just do this instead:
db.Blogs.Remove(realBlog);
db.SaveChanges();
}
return RedirectToAction("Index");
}
catch( Exception )
{
return View();
}
}
I don't really agree with the idea of using your entities as your models though. You should use View Models instead.
EDIT
Since you now are saying that Blog isn't being passed, try this:
#model Blog
#using ( Html.BeginForm() )
{
#Html.EditorForModel()
<input type="submit" value="Delete" />
}
You weren't actually giving the model binder any of the details it needed to construct your model.
is it possible to try the following:
[HttpPost]
public ActionResult Delete(Blog deletedBlog)
{
try
{
using (var db = new BlogDataEntities())
{
// get blog entry from db context!!
Blog blog = db.Blogs.Find(deletedBlog.Id);
//Having issue here.. as soon as the next line is run in debug
//mode .. it goes to catch.. and returns a view with null values.
db.Entry(blog).State = System.Data.EntityState.Deleted;
db.SaveChanges();
}
return RedirectToAction("Index");
}
catch(Exception e)
{
// should catch more specific exception
// but catching 'e' should be a start
return View();
}
}
[Update] - pass in your Blog model from the view, tho as Dismissile says, you should really use a viewmodel, rather than the entity model for this purpose.
Also, you should catch the inner exception message and examine that for further clues.
The blog parameter in your Delete Action is null most likely because you are only posting the blog's id, not the entire blog object. I would either modify the Delete Action to accept just the id (per Dismissile's answer), or modify the Delete View to post the entire blog object and remove the id from the Action (since it belongs to the blog object):
[HttpPost]
public ActionResult Delete(Blog blog)
{
try
{
using (var db = new BlogDataEntities())
{
db.Entry(blog).State = System.Data.EntityState.Deleted;
db.SaveChanges();
}
return RedirectToAction("Index");
}
catch
{
return View();
}
}
I posted this in a few comments, but I felt it merited a separate answer for an "alternative".
Your controllers should be slim and adhere as best as possible to the single responsibility principle.
BlogController
public class BlogController : Controller
{
private BlogService blogService;
public BlogService()
{
blogService = new BlogService();
}
[HttpPost]
public ActionResult Delete(int id)
{
// make sure the user has permission to delete before actually deleting
// now that we know the user has permission
if (blogService.Delete(id))
{
return RedirectToAction("Index");
}
else
{
return View();
}
}
}
Now you'll have a reusable service layer that adheres to the single responsibility principle.
BlogService
public class BlogService
{
private BlogDataEntities dc;
public BlogService()
{
dc = new BlogDataEntities();
}
public bool Delete(int Id)
{
try
{
var blog= (from b in dc.Blogs where Blog.ID == Id select b).First();
// blog doesn't exist, exit and return false.
if( blog == null )
return false;
// blog exists, remove it
dc.Blogs.Remove(blog);
// push the delete to the database
SaveChanges();
// it worked, return true.
return true;
}
catch(System.Exception ex)
{
// an error happened, handle it and return false.
return false;
}
}
// I like to keep my `SubmitChanges()` Separate in case I need to
// stack up a few processes before hitting the database.
public void SaveChanges()
{
dc.SaveChanges();
}
}
I'm having an issue where my validation messages are showing up fine on an add operation, but when it comes to the update page, the validation messages are not showing:
This is my action, IsValid is coming out as false, and action redirects to the edit view, but none of the validation messages are shown. Is there something wrong in my approach?
[Authorize]
public ActionResult UpdateCar(CarDTO car)
{
try
{
_carTask.Update(car); //required Name field not set
}
catch (RulesException ex)
{
ex.AddModelStateErrors(ModelState, null);
}
if (!ModelState.IsValid)
{
return RedirectToAction(ViewNames.EditCar, new {carKey = car.carKey});
}
return RedirectToAction(ViewNames.Home, new {carKey = car.carKey});
}
<li>
<label for="Name">Car Name:</label>
<%= Html.TextBoxFor(x => x.Name, new { watermark="Car Name" })%>
<br />
<%= Html.ValidationMessage("Name") %>
</li>
If the form is invalid then you are redirecting to a new page which will loose any modal error values you set. Instead just return the View. Haven't checked the syntax but something like the below.
if (!ModelState.IsValid)
{
return View(ViewNames.EditCar, new {carKey = car.carKey});
}
return RedirectToAction(ViewNames.Home, new {carKey = car.carKey});