I am using strongly typed views in an MVC3 web app. I've noticed that when a form is submitted, the ViewModel that is passed to the controller only has values for properties that have form elements associated with them. For instance, the example below shows a simple confirmation View with a checkbox and a phone number that the user must confirm before proceeding. When the form is submitted to the controller action, the UserConfirmed property contains a value, but the PhoneNumber property is null.
Is there any way for the ViewModel to retain all of its values or do I have to repopulate the ViewModel properties that do not have form elements associated with them?
The View
#model WebMeterReplacement.ViewModels.Appointment.ScheduleConfirmationViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
#Html.CheckBoxFor(model => model.UserConfirmed)
<span>Please confirm before proceeding</span>
<div>
Phone Number: #Model.PhoneNumber
</div>
<input type="submit" value="Confirm"/>
The Controller
[HttpPost]
public ActionResult ScheduleConfirmation(ScheduleConfirmationViewModel model)
{
if (model.UserConfirmed)
{
// add ViewModel data to repository
}
else
{
ModelState.AddModelError("ERROR", WebResources.strERROR_ConfirmSchedule);
}
return View(model);
}
Since your writing the phonenumber as output to the page it won't be automatically posted back (you've found out that part) What you can do is populate an hidden or read-only field with the phonenumber so that it will be posted back to your controller. An second option is to make a new call to your datasource and repopulate your object before saving it back to your datasource.
I generally POST back information like this in a hidden input. I personally use this heavily to pass data needed to return the user exactly where they where before pressing edit.
In your case it's as simple as
#model WebMeterReplacement.ViewModels.Appointment.ScheduleConfirmationViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(false)
#Html.CheckBoxFor(model => model.UserConfirmed)
<span>Please confirm before proceeding</span>
<div>
#Html.HiddenFor(m => m.PhoneNumber)
Phone Number: #Model.PhoneNumber
</div>
<input type="submit" value="Confirm"/>
For future reference:
If your passing complex objects back you need one hidden field per attribute (Hiddenfor does NOT iterate)
View
WRONG
#Html.HiddenFor(m => m.PagingData)
RIGHT
#Html.HiddenFor(m => m.PagingData.Count)
#Html.HiddenFor(m => m.PagingData.Skip)
#Html.HiddenFor(m => m.PagingData.PageSize)
Action
public HomeController(AViewModel Model)
{
PagingData PagingData = Model.PagingData;
Skip = PagingData.Skip;
}
If your passing Arrays you can do it like this
View
#if (Model.HiddenFields != null)
{
foreach (string HiddenField in Model.HiddenFields)
{
#Html.Hidden("HiddenFields", HiddenField)
}
}
Action
public HomeController(AViewModel Model)
{
String[] HiddenFields = Model.HiddenFields;
}
Well, the form will only POST elements that you have created. As you found out, simply writing the phone number out to the page will not suffice. The model binder can only bind those properties which exist in the posted data.
Generally you have a couple of options here:
1) You can create Input elements for all of the properties in your model, using visible elements (like a textbox) for those properties you want to edit, and hidden elements which should be posted back but have no UI
2) Post back a partial representation of your model (as you are doing now), read the entity back in from it's data source (I assume you're using some kind of data source, EF maybe) and then alter the properties of that entity with the ones from your form.
Both scenarios are common but it really depends on the complexity of your model.
I know this thread is a bit old, but thought I'd resurrect it to get feed back on my solution to this.
I'm in a similar situation where my objects are passed to a view, and the view may only display part of that object for editing. Obviously, when the controller receives the model back from the default model binder, and values not posted back become null.. and saving this means that a DB value becomes null just because it wasn't displayed/returned from a view.
I didn't like the idea of creating a model for each view. I know it's probably the right way... but I was looking for a reusable pattern that can be implemented fairly quickly.
See the "MergeWith" method... as this would be used to take a copy of the object from the database and merge it with the one returned from the view (posted back)
namespace SIP.Models
{
[Table("agents")]
public class Agent
{
[Key]
public int id { get; set; }
[Searchable]
[DisplayName("Name")]
[Column("name")]
[Required]
[StringLength(50, MinimumLength = 4)]
public string AgentName { get; set; }
[Searchable]
[DisplayName("Address")]
[Column("address")]
[DataType(DataType.MultilineText)]
public string Address { get; set; }
[DisplayName("Region")]
[Searchable]
[Column("region")]
[StringLength(50, MinimumLength = 3)]
public string Region { get; set; }
[DisplayName("Phone")]
[Column("phone")]
[StringLength(50, MinimumLength = 4)]
public string Phone { get; set; }
[DisplayName("Fax")]
[Column("fax")]
[StringLength(50, MinimumLength = 4)]
public string Fax { get; set; }
[DisplayName("Email")]
[RegularExpression(#"(\S)+", ErrorMessage = "White space is not allowed")]
[Column("email")]
[StringLength(50, MinimumLength = 4)]
public string Email { get; set; }
[DisplayName("Notes")]
[Column("notes")]
[DataType(DataType.MultilineText)]
public string Notes{ get; set; }
[DisplayName("Active")]
[Column("active")]
public bool Active { get; set; }
public override string ToString()
{
return AgentName;
}
public bool MergeWith(Agent a, string[] fields)
{
try
{
foreach (PropertyInfo pi in this.GetType().GetProperties())
{
foreach (string f in fields)
{
if (pi.Name == f && pi.Name.ToLower() != "id")
{
var newVal = a.GetType().GetProperty(f).GetValue(a,null);
pi.SetValue(this, newVal, null);
}
}
}
}
catch (Exception ex)
{
return false;
//todo: Log output to file...
}
return true;
}
}
}
And to use this in the controller.. you'd have something like..
[HttpPost]
public ActionResult Edit(Agent agent)
{
if (ModelState.IsValid)
{
Agent ag = db.Agents.Where(a => a.id == agent.id).ToList<Agent>().First<Agent>();
ag.MergeWith(agent, Request.Params.AllKeys);
db.Entry(ag).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(agent);
}
This way, during post back, it takes the object from the database, and updates it with object from view... but only updates the values that were posted back.. So if you have a field like "address" or something that doesn't appear in the view.. it doesn't get touched during the update.
I've tested this so far and i works for my purposes, tho i welcome any feedback as I'm keen to see how others have overcome this situation. It's a first version and i'm sure it can be implemented better like through an extension method or something.. but for now the MergeWith can be copy/pasted to each model object.
Yes, Just place hidden fields in the form for those values which you are not using and want to return to server control.
Thanks
Related
I'm working on the first MVC3 project at our company, and I've hit a block. No one can seem to figure out what's going on.
I have a complex Model that I'm using on the page:
public class SpaceModels : List<SpaceModel> {
public bool HideValidation { get; set; }
[Required(ErrorMessage=Utilities.EffectiveDate + Utilities.NotBlank)]
public DateTime EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
}
In the Controller, I create a SpaceModels object with blank SpaceModels for when Spaces get combined (this would be the destination Space).
// Need a list of the models for the View.
SpaceModels models = new SpaceModels();
models.EffectiveDate = DateTime.Now.Date;
models.DisplayEffectiveDate = true;
models.Add(new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
return View("CombineSpaces", models);
Then in the View, I am using that SpaceModels object as the Model, and in the form making a TextBox for the Effective Date:
#model Data.SpaceModels
#using (Html.BeginForm("CombineSpaces", "Space")) {
<div class="EditLine">
<span class="EditLabel LongText">
New Space Open Date
</span>
#Html.TextBoxFor(m => m.EffectiveDate, new {
size = "20",
#class = "datecontrol",
// Make this as a nullable DateTime for Display purposes so we don't start the Calendar at 1/1/0000.
#Value = Utilities.ToStringOrDefault(Model.EffectiveDate == DateTime.MinValue ? null : (DateTime?)Model.EffectiveDate, "MM/dd/yyyy", string.Empty)
})
#Html.ValidationMessageFor(m => m.EffectiveDate)
</div>
<hr />
Html.RenderPartial("_SpaceEntry", Model);
}
The Partial View that gets rendered iterates through all SpaceModels, and creates a containing the Edit fields for the individual SpaceModel objects. (I'm using the List to use the same Views for when the Spaces get Subdivided as well.)
Then on the HttpPost, the EffectiveDate is still back at it's DateTime.MinValue default:
[HttpPost]
public ActionResult CombineSpaces(SpaceModels model, long siteID, long storeID, DateTime? effectiveDate) {
// processing code
}
I added that DateTime? effectiveDate parameter to prove that the value when it gets changed does in fact come back. I even tried moving the rendering of the TextBox into the _SpaceEntry Partial View, but nothing worked there either.
I did also try using the #Html.EditorFor(m => m.EffectiveDate) in place of the #Html.TextBoxFor(), but that still returned DateTime.MinValue. (My boss doesn't like giving up the control of rendering using the #Html.EditorForModel by the way.)
There has to be something simple that I'm missing. Please let me know if you need anything else.
Looking at the source code for DefaultModelBinder, specifically BindComplexModel(), if it detects a collection type it will bind the individual elements but will not attempt to bind properties of the list object itself.
What model binding does is attempt to match the names of things or elements in the view to properties in your model or parameters in your action method. You do not have to pass all of those parameters, all you have to do is add them to your view model, then call TryUpdateModel in your action method. I am not sure what you are trying to do with SpaceModel or List but I do not see the need to inherit from the List. Im sure you have a good reason for doing it. Here is how I would do it.
The view model
public class SpacesViewModel
{
public DateTime? EffectiveDate { get; set; }
public bool DisplayEffectiveDate { get; set; }
public List<SpaceModel> SpaceModels { get; set; }
}
The GET action method
[ActionName("_SpaceEntry")]
public PartialViewResult SpaceEntry()
{
var spaceModels = new List<SpaceModel>();
spaceModels.Add(
new SpaceModel { StoreID = storeID, SiteID = siteID, IsActive = true });
var spacesVm = new SpacesViewModel
{
EffectiveDate = DateTime.Now,
DisplayEffectiveDate = true,
SpaceModels = spaceModels
};
return PartialView("_SpaceEntry", spacesVm);
}
The POST action method
[HttpPost]
public ActionResult CombineSpaces()
{
var spacesVm = new SpacesViewModel();
// this forces model binding and calls ModelState.IsValid
// and returns true if the model is Valid
if (TryUpdateModel(spacesVm))
{
// process your data here
}
return RedirectToAction("Index", "Home");
}
And the view
<label>Effective date: </label>
#Html.TextBox("EffectiveDate", Model.EffectiveDate.HasValue ?
Model.EffectiveDate.Value.ToString("MM/dd/yyyy") : string.empty,
new { #class = "datecontrol" })
Sometimes you need to explicitly bind form data using hidden fields such as
#Html.HiddenField("EffectiveDate", Model.EfectiveDate.)
In order to bind the properties of the SpaceModel object you can add individual properties such as SiteID to the view model or add a SpaceModel property for a single SpaceModel. If you want to successfully bind a complex model, add it as a Dictionary populated with key-value pairs rather than a List. You should then add the dictionary to the view model. You can even add a dictionary of dictionaries for hierarchical data.
I hope this helps :)
I've written a form in ASP.NET MVC3, and I can't get the entry to save the changes I make in the database, but while debugging, I noticed that the changes were reflected in the data context. I am experiencing no errors running this code. Let me know if you need more. Thanks!
Controller
[HttpPost]
public ActionResult Edit(Tool tool, FormCollection collection)
{
if (collection["Tool.Person.PersonID"] != "")
{
tool.Person= context.People.Find(
Convert.ToInt32(collection["Tool.Person.PersonID"])
);
}
if (collection["Tool.Company.CompanyID"] != "")
{
tool.Company = context.Companies.Find(
Convert.ToInt32(collection["Tool.Company.CompanyID"])
);
}
if (ModelState.IsValid)
{
context.Entry(tool).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(tool);
}
The first two if statements are checking to see if the user inputted a person or company, and the information is passed via the FormCollection. PersonID and CompanyID are primary keys for Person and Company, respectively. I went through the method line by line multiple times and achieve the same result - after context.SaveChanges();, the context reflects the changes, but the database entries remain null for both Person_PersonID and Company_CompanyID.
Try using a view model and accessing the database after the user submits the form.
This should get you well on your way.
ViewModel
using System.ComponentModel.DataAnnotations;
namespace Project.ViewModels
{
public class _tools
{
[Required(ErrorMessage="ToolID is required")]
public int32 ToolID{ get; set; } //whatever ID you use to retrieve the Tool from the database.
[Required(ErrorMessage="PersonID is required")]
public int32 PersonID{ get; set; }
[Required(ErrorMessage="CompanyID is required")]
public int32 CompanyID{ get; set; }
}
}
Controller Post
[HttpPost]
public ActionResult Edit(_tool viewModel)
{
if (ModelState.IsValid)
{
Tool tool = db.GetTool(viewModel.ToolID) //whatever method you use to get a current version of the row. You already do this before you send the data to the client, so just copy that code
tool.Person = viewModel.PersonID
tool.Company = viewModel.CompanyID
context.Entry(tool).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(tool);
}
View
#model = _tool
#using(Html.BeginForm("Edit", "ControllerNameHere", FormMethod.Post, null))
{
#Html.HiddenFor(model => model.ToolID)
#*Also add whatever inputs you use to get PersonID and CompanyID from the user.
Make sure to either use the #Html helpers or to give them names that correspond.
i.e. name one input PersonID and the other CompanyID*#
<input type="submit" value="Edit">
}
I have two classes as follows
public class ODCTE_Major
{
public int ODCTE_MajorId { get; set; }
public string OfficialMajorName { get; set; }
public string MajorCode { get; set; }
... More unrelated code ....
}
AND
public class CareerMajor
{
...lots of unrealted code to this question left out
public int ODCTE_MajorId { get; set; }
public virtual ODCTE_Major ODCTE_Major { get; set; }
}
I added a controller with CRUD methods and in the create.cshtml there is this line
<div class="editor-field">
#Html.DropDownList("ODCTE_MajorId", String.Empty)
#Html.ValidationMessageFor(model => model.ODCTE_MajorId)
</div>
The select list populates it with the OfficialMajorName from ODCTE_Major. I need the select list to populate with the MajorCode or a value that looks like MajorCode - OfficialMajorName.
Could someone provide assistance for how this is done, please?
Thanks.
Add this to ODCTE_Major:
public string MajorDisplayName
{
get { return string.Format("{0} - {1}", MajorCode, OfficialMajorName); }
}
This is just a read only property used to create the display text in the format you want the menu to use.
Then in CareerMajor, add:
public IEnumerable<ODCTE_Major> Majors{ set; get; } // Thank you Shyju!
This will give you a place in your view model to pass the list of Majors you want in your menu to the view.
Then in your action method when you're creating a CareerMajor view model to send to the view, populate the new IEnumberable with the ODCTE_Major entities you'd like displayed in your menu.
On the view page:
#Html.DropDownListFor(m => m.ODCTE_MajorId, new SelectList(Model.Majors, "ODCTE_MajorId", "MajorDisplayName", Model.ODCTE_MajorId), "Select One")
This creates a SelectList to populate the drop down with. The SelectList constructor is saying use ODCTE_MajorId as the value for a SelectListItem in the menu, and to use MajorDisplayName as the text to actually display in the menu. It sets the selected value, if there is one, and adds a null item with the text "Select One" to the top of the menu. Feel free to take that final argument out if you don't want the null text.
Have your ViewModel hold a Collection property to represent all available Majors (for poulating the Dropdown)
public class CareerMajor
{
//other proiperties
public int ODCTE_MajorId { get; set; }
public IEnumerable<ODCTE_Major> Majors{ set; get; }
}
And in your GET Action, fill it and send it to your strongly typed view
pubilc ACtionResult Create()
{
var viewModel=new CareerMajor();
viewModel.Majors=db.GetllAllMajors(); // getting a list of ODCTE_Major objects
return View(viewModel);
}
and in the View, use the DropDownListFor HTML Helper method.
#model CareerMajor
#Html.BeginForm())
{
#Html.DropDownListFor(m=>m.ODCTE_MajorId,
new SelectList(Model.Majors,"ODCTE_MajorId ","MajorCode"),"select one..")
//other elements
}
In your controller action:
ViewBag.ODCTE_MajorId = new SelectList(availableMajors, "ODCTE_MajorId", "MajorCode");
*second and third parameters are the names of the value and text fields respectively
Then in your view:
#Html.DropDownList("ODCTE_MajorId", String.Empty)
where availableMajors is an IEnumerable that contains the majors you want to list.
I'm new to MVC3, but so far I have managed to get along with my code just great.
Now, I would like to make a simple form, that allows the user to input a text string, representing the name of an employee. I would then like this form to be submitted and stored in my model, in a sort of list. The form should then re-display, with a for-each loop writing out my already added names. When I'm done and moving on, I need to store this information to my database.
What I can't figure out, is how to store this temporary information, until i push it to my database. Pushing everytime I submit I can do, but this has cause me alot of headaches.
Hope you guys see what I'm trying to do, and have an awesome solution for it. :)
This is a simplified version of what I've been trying to do:
Model
public class OrderModel
{
public virtual ICollection<Employees> EmployeesList { get; set; }
public virtual Employees Employees { get; set; }
}
public class Employees
{
[Key]
public int ID { get; set; }
public string Name { get; set; }
}
View
#model OrderModel
#{
if (Model.EmployeesList != null)
{
foreach (var c in Model.EmployeesList)
{
#c.Name<br />
}
}
}
#using(Html.BeginForm())
{
#Html.TextBoxFor(m => m.Employees.Name)
<input type="submit" value="Add"/>
}
Controller
[HttpPost]
public ActionResult Index(OrderModel model)
{
model.EmployeesList.Add(model.Employees);
// This line gives me the error: "System.NullReferenceException: Object reference not set to an instance of an object."
return View(model);
}
I think you should handle this by burning the employee list into the page. Right now, you're not giving your form any way of recognizing the list.
In an EditorTemplates file named Employees:
#model Employees
#Html.HiddenFor(m => m.ID)
#Html.HiddenFor(m => m.Name);
In your view:
#using(Html.BeginForm())
{
#Html.EditorFor(m => m.EmployeesList)
#Html.TextBoxFor(m => m.Employees.Name)
<input type="submit" value="Add"/>
}
[HttpPost]
public ActionResult Index(OrderModel model)
{
if (model.EmployeesList == null)
model.EmployeesList = new List<Employees>();
model.EmployeesList.Add(model.Employees);
return View(model);
}
As an added bonus to this method, it would be easy to add ajax so the user never has to leave the page when they add new employees (You might be able to just insert a new hidden value with javascript and avoid ajax. It would depend on if you do anything other than add to your list in your post).
I think this would be a good use for TempData. You can store anything in there, kind of like the cache, but unlike the cache it only lasts until the next request. To implement this, change the action method like this (example only):
[HttpPost]
public ActionResult Index(OrderModel model)
{
dynamic existingItems = TempData["existing"];
if (existingItems != null)
{
foreach (Employee empl in existingItems)
model.EmployeesList.Add(empl );
}
model.EmployeesList.Add(model.Employees);
TempData["existing"] = model.EmployeesList;
return View(model);
}
I'm experiencing very odd behavior in the way an ASP.NET MVC3 view model is emitted -- for one field, ModelMetadata is not propagated. I'm using the templated helpers after Brad Wilson, though updated for Razor. Here's my view model:
public class FamilyBaseViewModel : ViewModelBase
{
[Display(Order = 10)]
public string FamilyName { get; set; }
[Display(Order = 30)]
[StringLength(50, ErrorMessage = "Street name can only be 50 characters long.")]
public string Street { get; set; }
}
public class FamilyPrivateViewModel : FamilyBaseViewModel
{
[Display(Name = "Date Started", Order = 20)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:d}")]
[DataType(DataType.Date)]
public DateTime? DateStarted { get; set; }
}
The object.cshtml template runs through the properties and uses Html.Display to show them:
// object.cshtml
<ol>
#foreach (var prop in
ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay
&& !ViewData.TemplateInfo.Visited(pm)
&& pm.ModelType != typeof(System.Data.EntityState)))
{
<li>
#Html.Display(prop.PropertyName)
</li>
}
</ol>
In the above scenario, all three fields have the right descriptors in the object.cshtml call (prop.DisplayName, prop.TemplateHint), but when the first property -- FamilyName -- is passed to String.cshtml, the ViewData.ModelMetadata is not populated at all. As a result, the template can't display a label (except "String"), nor assign the ID of the control, etc.
Street and DateStarted are emitted normally, with the ID and all. So I'm completely at a loss as to why the one property would fail to set the ViewData properties -- nor do I know how to step through past the Html.Display call to see what might be happening.
Any ideas for a next place to look?
So the problem was in the controller action, which for unrelated reasons used "FamilyName" for a ViewData value:
ViewBag.FamilyName = familyName;
And this caused all heck to break loose in the mapping of model fields with the same name -- that is, ModelMetadata will not propagate. So, the lesson is: don't give ViewData dictionary items keys with the same name as a field in your view model.