I have a complex object that I want to use in an edit view. To simplify things I have created a ViewModel and have successfully created the edit view page, and everything renders correctly. When I hit save, everything falls apart.
The ViewModel is as follows:
public class ClosureEditViewModel
{
public Model.Closure Closure { get; set; }
public Model.School School { get; set; }
public Model.ClosureDetail CurrentDetails { get; set; }
}
Some of the View is as follows:
<div class="display-label">School</div>
<div class="display-field">
#Html.DisplayFor(model => model.Closure.School.Name)
</div>
<div class="display-label">Closed</div>
<div class="display-field">
#Html.DisplayFor(model => model.Closure.Logged)
</div>
....
<div class="editor-label">
#Html.LabelFor(model => model.CurrentDetails.DateOpening, "Date Opening (dd/mm/yyyy)")
</div>
<div class="editor-field">
#Html.TextBox("DateOpening", Model.CurrentDetails.DateOpening.ToString("dd/MM/yyyy"))
#Html.ValidationMessageFor(model => model.CurrentDetails.DateOpening)
</div>
....
<tr>
<td>
#Html.CheckBoxFor(model => model.CurrentDetails.Nursery, (Model.School.Nursery ? null : new { #disabled = "disabled" }))
</td>
The important parts of the controller are as follows:
public ActionResult Edit(int id)
{
Data.IClosureReasonRepository reasonRepository = new Data.SqlServer.Repositories.ClosureReasonRepository(UnitOfWork);
IEnumerable<Model.ClosureReason> reasons = reasonRepository.GetAll();
Model.Closure closure = ClosureRepository.GetClosure(id);
Model.ClosureDetail currentDetail = closure.ClosureDetails.Last();
ViewModels.ClosureEditViewModel editClosure = new ViewModels.ClosureEditViewModel() { Closure = closure, School = closure.School, CurrentDetails = closure.ClosureDetails.Last() };
ViewBag.ReasonId = new SelectList(reasons, "Id", "Name", currentDetail.ReasonId);
return View(editClosure);
}
[HttpPost]
public ActionResult Edit(ViewModels.ClosureEditViewModel newDetail)
{
//if (ModelState.IsValid)
//{
//}
Data.IClosureReasonRepository reasonRepository = new Data.SqlServer.Repositories.ClosureReasonRepository(UnitOfWork);
IEnumerable<Model.ClosureReason> reasons = reasonRepository.GetAll();
ViewBag.ReasonId = new SelectList(reasons, "Id", "Name", newDetail.CurrentDetails.ReasonId);
return View(newDetail);
}
When I hit save the following message appears:
Object reference not set to an instance of an object.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Source Error:
Line 94: </td>
Line 95: <td>
Line 96: #Html.CheckBoxFor(model => model.CurrentDetails.P1, (Model.School.P1 ? null : new { #disabled = "disabled" }))
Line 97: </td>
Line 98: <td>
I just can't figure out why it is having problems with the School property but neither of the other two.
James :-)
It seems that Model.School is null when you render the view once again in the POST action. Make sure that it isn't null because in your view you don't have a single input field bound to the School property => this property will be null inside your POST controller action.
[HttpPost]
public ActionResult Edit(ClosureEditViewModel viewModel)
{
... some operations
// Make sure that viewModel.School is not null
// Remember that the checkbox is bound to CurrentDetails.P1 so
// when you post to this action there is nothing that will initialize
// the School property => you should do whatever you did in your GET
// action in order to initialize this property before returning the view
return View(viewModel);
}
Related
using the JQuery sortable, and trying to send the new order back to my controller, but not having a whole lot of luck. My view is:
using (Ajax.BeginForm("EditTickerOrder", new AjaxOptions { InsertionMode = InsertionMode.Replace, HttpMethod = "POST", }))
{
<div id="editableticker">
#Html.HiddenFor(m => m.ProjectGUID)
<ul id="sortablediv">
#foreach (DGI.CoBRA.Tools.BussinessObjects.CollabLibrary.TickerObjects.Ticker t in Model)
{
<li class="ui-state-default" id="#t.pKeyGuid.ToString()">
<p>#Html.CheckBox(t.pKeyGuid.ToString(), t.Display, new { #class = "activechk" })
<span style="font-weight: bold">
#t.Text
</span>
</p>
</li>
}
</ul>
<input type="submit" value="Save New Ticker Order" />
}
and my controller is:
[HttpPost]
public ActionResult EditTickerOrder(Guid ProjectGUID, List<string> items)
{
TickerCollectionModel TickerData = new TickerCollectionModel();
TickerData.ProjectGUID = ProjectGUID;
TickerData.ListAllBySession(ProjectGUID);
return PartialView("TickerList", TickerData);
}
yet the list<string> items is always null. Any ideas?
You are writing foreach loops, most definitely violating the naming conventions for your form input fields that the default model binder expects for working with collections. If you don't respect the established wire format, you cannot expect the default model binder to be able to rehydrate your models in the POST action.
In fact, why don't you use view models and editor templates? They make everything trivial in ASP.NET MVC.
So let's define a view model that will reflect your view requirements (or at least those shown in your question => you could of course enrich it with additional properties that you want to handle):
public class TickerViewModel
{
public Guid Id { get; set; }
public bool IsDisplay { get; set; }
public string Text { get; set; }
}
public class ProjectViewModel
{
public Guid ProjectGUID { get; set; }
public IEnumerable<TickerViewModel> Tickers { get; set; }
}
and then a controller whose responsibility is to query your DAL layer, retrieve a domain model, map the domain model into the view model we defined for this view and pass the view model to the view. Inversely, the POST action receives a view model from the view, maps the view model back into some domain model, passes the domain model to your DAL layer for processing and renders some view or redirects to a success action:
public class HomeController : Controller
{
public ActionResult Index()
{
// TODO: those values come from a data layer of course
var model = new ProjectViewModel
{
ProjectGUID = Guid.NewGuid(),
Tickers = new[]
{
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 1" },
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 2" },
new TickerViewModel { Id = Guid.NewGuid(), Text = "ticker 3" },
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(ProjectViewModel model)
{
// Everything will be correctly bound here => map the view model
// back into your domain model and pass the domain model to
// your DAL layer for processing ...
return Content("Thanks for submitting");
}
}
a view (it is worth noting that in this example I have used a standard form instead of AJAX but it is trivial to convert it into an AJAX form):
#model ProjectViewModel
#using (Html.BeginForm())
{
#Html.HiddenFor(m => m.ProjectGUID)
<div id="editableticker">
<ul id="sortablediv">
#Html.EditorFor(x => x.Tickers)
</ul>
</div>
<button type="submit">OK</button>
}
and finally the corresponding editor template which will automatically be rendered for each element of the Tickers collection (~/Views/Home/EditorTemplates/TickerViewModel.cshtml):
#model TickerViewModel
<li class="ui-state-default">
<p>
#Html.CheckBoxFor(x => x.IsDisplay, new { #class = "activechk" })
#Html.LabelFor(x => x.IsDisplay, Model.Text)
#Html.HiddenFor(x => x.Text)
#Html.HiddenFor(x => x.Id)
</p>
</li>
I have an object named Visit and i defined the following helper method (“CanBeEdited”) to specify if users can edit the object Status property or not:-
public partial class Visit
{
public bool CanBeEdited(string username)
{return (((DoctorID != null) && (DoctorID.ToUpper().Equals(username.ToUpper()))) && (StatusID == 5)); } }}
Then i have specified to show or hide certain dropdownlist on my Edit view depending on weather the CanBeEdited helper method returns true or false (if it returns true then the user can view and edit the Status dropdownlist, and if it returns false then the view will render an #Html.HiddenFor representing the old status value).
My edit view which includes the helper method looks as following:-
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Visit</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Note)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Note)
#Html.ValidationMessageFor(model => model.Note)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.DoctorID)
</div>
<div class="editor-field">
#Html.DropDownList("DoctorID", String.Empty)
#Html.ValidationMessageFor(model => model.DoctorID)
</div>
#{
if (Model.CanBeEdited(Context.User.Identity.Name))
{
<div class="editor-label">
#Html.LabelFor(model => model.StatusID)
</div>
<div class="editor-field">
#Html.DropDownList("StatusID", String.Empty)
#Html.ValidationMessageFor(model => model.StatusID)
</div>
}
else
{
#Html.HiddenFor(model => model.StatusID)}
}
<p>
#Html.HiddenFor(model => model.VisitTypeID)
#Html.HiddenFor(model => model.CreatedBy)
#Html.HiddenFor(model => model.Date)
#Html.HiddenFor(model => model.VisitID)
#Html.HiddenFor(model => model.PatientID)
#Html.HiddenFor(model => model.timestamp)
<input type="submit" value="Create" />
</p>
</fieldset>
}
To be honest it is the first time i implement such as case,, so is my approach sound valid ???,, or it have some weaknesses i am unaware of ??. As i need to implemented similar cases all around my web application...
Baring in mind that i am also checking for the CanBeEdited on the action methods..
Thanks in advance for any help.
Updated:-
My post action method look as follow:-
[HttpPost]
public ActionResult Edit(Visit visit)
{
if (!(visit.Editable(User.Identity.Name)))
{
return View("NotFound");
}
try
{
if (ModelState.IsValid)
{
repository.UpdateVisit(visit);
repository.Save();
return RedirectToAction("Index");
}
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var clientValues = (Visit)entry.Entity;
ModelState.AddModelError(string.Empty, "The record you attempted to edit "
+ "was modified by another user after you got the original value. The "
+ "edit operation was canceled and the current values in the database "
+ "have been displayed. If you still want to edit this record, click "
+ "the Save button again. Otherwise click the Back to List hyperlink.");
// patient.timestamp = databaseValues.timestamp;
}
catch (DataException)
{
//Log the error (add a variable name after Exception)
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
ViewBag.DoctorID = new SelectList(Membership.GetAllUsers(), "Username", "Username", visit.DoctorID);
ViewBag.StatusID = new SelectList(db.VisitStatus, "StatusID", "Description", visit.StatusID);
ViewBag.VisitTypeID = new SelectList(db.VisitTypes, "VisitTypeID", "Description", visit.VisitTypeID);
return View(visit);
}
I don't feel adding that in the View is a good idea. I would like to have My ViewModel to hold a property of boolean type to determine that it is editable or not. The value of that you can set in your controller after checking the relevant permissions.
public class ProductViewModel
{
public bool IsEditable { set;get;}
//other relevant properties
}
and controller action
public ActionResult GetProduct()
{
ProductViewModel objVM=new ProductViewModel();
objVm.IsEditable=CheckPermissions();
}
private bool CheckPermissions()
{
//Check the conditions and return true or false;
}
So view will be clean like ths
#if (Model.IsEditable)
{
//Markup for editable region
}
IMHO, it sounds valid enough.
UPDATE: removed irrelevant commentary, and edited to indicate a primary concern.
Now, taking a closer look, especially with the controller action, I strongly recommend that you eliminate the hidden fields (except the one that you need to re-load the record from your back end).
A savvy user can tamper with the hidden form data (all the form data) and your controller action will happily send it all back to the server.
In reality, you should post back only the fields that are permitted to be changed, rehydrate the record from the back end, and transfer the "editable" fields to the fresh copy. This also comes closer to addressing concurrent edit and stale record issues.
I have two models : Category and Picture which refers to two tables, Categories and Pictures respectively. The Category model has a navigation property to Picture model.
Now, I created a controller using Scaffolding feature with CRUD operations for Category. Following is the code :-
public ActionResult Create()
{
ViewBag.ParentCategoryId = new SelectList(db.Categories, "Id", "Name");
ViewBag.PictureId = new SelectList(db.Pictures, "Id", "PictureUrl");
return View();
}
The automatically generated controller actions uses SelectList for listing the available Picture entries in the database and passes it down to dropdownlist for selection. This is not the ideal scenario since what I want is to unable the user to upload the Picture and then the reference is added to Category model. Later, the entries are saved to Categories and Pictures table.
Create model like this:
public class FullCategoryModel
{
public HttpPostedFileBase Picture { get; set; }
public Category CategoryModel {get; set;}
}
In view:
#using (Html.BeginForm("Create", "Category", FormMethod.Post,
new { enctype = "multipart/form-data" }))
{
#Html.TextBoxFor(model => model.Category.Name) // example, put there all category details
<input type="file" name="Picture" id="Picture" />
<input type="submit" value="Upload" />
}
Then create action:
[ActionName("Create")]
[HttpPost]
public ActionResult Create(FullCategoryModel model)
{
// here you can get image in bytes and save it in db,
// also all category detail are avalliable here
MemoryStream ms = new MemoryStream();
model.Picture.InputStream.CopyTo(ms);
Image picture = System.Drawing.Image.FromStream(ms);
// save in db as separate objects, than redirect
return RedirectToAction("Index", "Category");
}
First of all, I would like to thank #NickLarsen for making me believe that my understanding is good and i can achieve the task myself.
The problem was not that too tough but since i was new to Asp.net MVC, things were bit baffling. From the very start, I had the notion that i will be needing a ViewModel merging Category and Price classes and then a picture uploading API. But, somehow I wasn't able to fit the pieces in right place. Therefore after various regression and research over the internet, I achieved the task in following manner :-
First of all, I created a ViewModel
public class CatPicView
{
public Category Category { get; set; }
public Picture Picture { get; set; }
}
Second, I added the Uploadify javascript API
<script type="text/javascript">
$('#file_upload').uploadify({
'uploader': '#Url.Content("~/uploadify/uploadify.swf")',
'script': '#Url.Action("Upload", "Category")',
'cancelImg': '#Url.Content("~/uploadify/cancel.png")',
'buttonText': 'Upload',
'folder': '#Url.Content("~/content/images")',
'fileDesc': 'Image Files',
'fileExt': '*.jpg;*.jpeg;*.gif;*.png',
'auto': true,
'onComplete': function (event, ID, fileObj, response, data) {
var json = jQuery.parseJSON(response);
$("#pictureImage").html("<img src='"+json+"' alt='"+json+"' height='100px' width='100px'/>");
$("#Picture_PictureUrl").val(json);
$("#pictureRemove").show();
}
});
</script>
Hooked the API to following Server Function for renaming and saving to folder
[HttpPost]
public ActionResult Upload(HttpPostedFileBase fileData)
{
if (fileData != null && fileData.ContentLength > 0)
{
//var fileName = Server.MapPath("~/Content/Images/" + Path.GetFileName(fileData.FileName));
int pictureCount = 800000;
pictureCount += db.Pictures.Count();
string extension = Path.GetExtension(fileData.FileName);
string renamedImage = Server.MapPath("~/Content/Images/Categories/cat" + pictureCount + extension);
fileData.SaveAs(renamedImage);
return Json("/Content/Images/Categories/" + Path.GetFileName(renamedImage));
}
return Json(false);
}
And at last, rewrote the Category create Function as below for saving changes to DB
[HttpPost]
public ActionResult Create(CatPicView catPic)
{
if (ModelState.IsValid)
{
if (!String.IsNullOrEmpty(catPic.Picture.PictureUrl))
{
Picture picture = new Picture();
picture.PictureUrl = catPic.Picture.PictureUrl;
db.Pictures.Add(picture);
catPic.Category.PictureId = picture.Id;
}
db.Categories.Add(catPic.Category);
db.SaveChanges();
return RedirectToAction("Index");
}
return View();
}
I think MVC scaffolding feature see the relation of two models as "Many to Many". That's why it created two drop down list for you. According to your scenario, you could do "Category" create page without "Picture" model data because "Picture" is the main entity here. So In the picture create action.
[HttpPost]
public ActionResult Create(Picture picture)
{
if (ModelState.IsValid)
{
databaseContext.Pictures.Add(picture);
databaseContext.SaveChanges();
return RedirectToAction("Index");
}
return View(picture);
}
In the view page of create picture
#model YourProjectName.Models.Picture
<h2>Create</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Picture</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Url)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Url)
#Html.ValidationMessageFor(model => model.Url)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Categories.CategoryID, "Category")
</div>
<div class="editor-field">
#Html.DropDownList("CategoryID", "Choose Category")
#Html.ValidationMessageFor(model => model.Categories.CategoryID)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
I am using paged list in a search actionresult to page my results. It returns events within a date range or category.The first page returned is fine, but the second never has any results. There are over 7 results that the query returns when I check a breakpoint. Here is my search controller;
public ActionResult Search(DateTime? q, DateTime? e, int? EventCategoryId, Event model)
{
var SearchEvents = db.Events
.OrderByDescending(r => r.start)
.Where(r =>
r.start >= q &&
r.end <= e &&
r.EventCategoryId == EventCategoryId ||
r.start >= q ||
r.EventCategoryId == EventCategoryId);
ViewBag.QValue = model.start;
ViewBag.EValue = model.end;
ViewBag.ListValue = model.EventCategoryId;
ViewBag.EventCategoryId = new SelectList(db.EventsCategories, "Id", "Name", ViewBag.ListValue);
var pageIndex = model.Page ?? 1;
var results = SearchEvents.ToPagedList(pageIndex, 4);
ViewBag.Names = results;
return View(results);
}
}
And here is my view;
#model IPagedList<RealKaac.Models.Event>
#{
ViewBag.Title = "SearchResults";
}
#using PagedList;
#using PagedList.Mvc;
#using System.Linq;
<div id="Content">
<h2>SearchResults</h2>
<div id="EventSearch">
#using (Html.BeginForm("Search", "Event"))
{
<p class="EventPar">Start Date: </p>
<input id ="datepicker" type ="text" name ="q" value="#ViewBag.QValue" />
<p class="EventPar">End Date: </p>
<input id ="datepickerend" type ="text" name ="e" value ="#ViewBag.EValue" />
<p class="EventPar">Category:</p>
#Html.DropDownList("EventCategoryId")
<input id="EventButton" style="padding:1px;" type ="submit" name ="Search" value = "Search" />
}
</div>
<div id="IndexEvents">
#foreach (var item in Model)
{
<div class ="event">
<div class="eventname">
#*<p>#Html.DisplayFor(modelItem => item.Name)</p> *#
</div>
<div class = "etitle">
<p>#Html.DisplayFor(modelItem => item.title)</p>
</div>
<div class="eventsdesc">
<p>#Html.DisplayFor(modelItem => item.EventDescription)</p>
</div>
<ul class ="datelink">
<li style="margin-right:120px;">This event starts on the
#Html.DisplayFor(modelItem => item.start) and ends on the
#Html.DisplayFor(modelItem => item.end)</li>
<li>Click #Html.DisplayFor(modelItem => item.EventWebsite) for more info.</li>
</ul>
</div>
}
</div>
</div>
#Html.PagedListPager((IPagedList)ViewBag.Names, page => Url.Action("Search", new { page }))
I'm fairly new at this so I'm probably missing something small, any advice is greatly appreciated.
Thanks.
The Url.Action has only one parameter to it (the page #), which doesn't match the controller's method. It isn't providing access to any of the other parameters (such as the start/end dates). So, the second "page" query for the next 4 Events likely isn't working. If you created a SearchModel that had the necessary properties on it, you should easily be able to send the values from one page to the next.
public ActionResult Search(SearchModel search) {
//... (your search code)
// ... then, before returning the View,
ViewBag.SearchParameters = search;
return View(results);
where SearchModel has properties like:
public class SearchModel {
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
public int Page? { get; set; }
// etc.
}
Then, you could:
#Html.PagedListPager((IPagedList)ViewBag.Names, page => Url.Action("Search", new SearchModel { search = ViewBag.SearchParameters }))
Or you could just use named parameters:
#Html.PagedListPager((IPagedList)ViewBag.Names, page => Url.Action("Search", new { page = page, q = ViewBag.QValue, /* ... etc. */ }))
I have a problem with a TryUpdateModel in MVC3
When the Edit (post) is fired, I have the following code:
public ActionResult Edit(int id, FormCollection collection)
{
var review = FoodDB.FindByID(id);
if (TryUpdateModel(review))
return RedirectToAction("Index");
return View(review);
}
The view is built directly by the VS (so not changed by me)
If I trace the code, I see the new values in FormCollection, but after executing TryUpdateModel, it returns true, doesn't throw any error, but the review object isn't updated.
What could I do wrong?
EDIT
I come up with some more details:
First, the db is not real DB, but just a "simulation" - class with one static genric List
List<Review> Review;
Review class is simply a POCO, as below:
public class Review
{
public string Message { get; set; }
public DateTime Created { get; set; }
public int ID { get; set; }
public int Rating { get; set; }
}
The view is strong-typed, generated by VS from the Edit method of the controller. Fields are defined as below:
<div class="editor-label">
#Html.LabelFor(model => model.Message)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Message)
#Html.ValidationMessageFor(model => model.Message)
</div>
#Html.HiddenFor(model => model.ID)
<div class="editor-label">
#Html.LabelFor(model => model.Rating)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Rating)
#Html.ValidationMessageFor(model => model.Rating)
</div>
Call to var review = FoodDB.FindByID(id); returns Review object
Even if TryUpdateModel(review) does not work (I trace through code, and I inspected review object before and after the call, as well as the collection, and it receives correct values), yet the review obj is not updated.
However, I replaced it with my own hand-written method, as below, and in this case the review object DOES get updated:
private void MyTryUpdateModel(Review review, FormCollection collection)
{
review.Message = collection["Message"];
review.Rating = int.Parse(collection["Rating"]);
}
So the TryUpdateMethod SHOULD find proper fields in collection for updating, as I understand.
So, what can be wrong?
Thanks all
Based on the code you posted, the review object is not updated, because the new values in FormCollection have not been bound to your model. You are not using the DefaultModelBinder.
If your view is strongly typed (and assuming the type class is named Food), change your method signature and method as follows:
public ActionResult Edit(Food food)
{
if (ModelState.IsValid)
{
FoodDB.Update(food);
return RedirectToAction("Index");
}
return View(food);
}
The DefaultModelBinder will take the values from the form and bind them to your model.