Trying to get UpdateModel to work for my User. The User class has basic string properties like CompanyName, FirstName, LastName, etc so nothing exotic.
Here is the header for my view:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Public.Master" Inherits="System.Web.Mvc.ViewPage<User>" %>
After they submit, in my controller, the code looks like this:
[HttpPost]
public ActionResult Edit(string id, FormCollection collection)
{
try
{
User myUser = db.Get<IUserRepository>().Get(id);
UpdateModel(myUser);
db.Update(myUser);
return RedirectToAction("Index", "Home");
}
catch
{
return View();
}
}
The values past into FormCollection have the values like:
[0] "FirstName" string
[1] "LastName" string
[2] "Email" string
Here is my UserModelBinder (took out some error checking code) which seems to be the source of the problem:
public class UserModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
IPrincipal p = controllerContext.HttpContext.User;
User u = db.Get(p.Identity.Name);
return u;
}
}
while the myUser I get from the database has all its original values, the UpdateModel for my controller never actually makes any changes. I have read problems people have with ViewModels and which prefix to use, but I am just passing in the regular database object.
The strange thing is that this User edit is for my "Public" area and I already have a User edit for the Admin area which lets the administrator change extra Properties. The "Admin" area User edit is working fine, but the "Public" area for User edit isn't even though the code is almost identical.
Update:
This turned out to be a custom ModelBinding problem and by changing my UserModelBinding to derive from DefaultModelBinder and adding into my BindModel method:
if (bindingContext.Model != null)
return base.BindModel(controllerContext, bindingContext);
Everything seems to work.
Try this instead
public ActionResult Edit(User thisUser)
Id will need to come from a hidden field maybe.
Also you will need to ensure your field names match the User property names.
You shouldn't need to do anything more as the object should have the values within it.
If this is not helpful then let me know and I'll remove this answer
edit
This is one of my update methods
[HttpPost]
public ActionResult EditCustomer(Customer customer)
{
//ensure that the model is valid and return the errors back to the view if not.
if (!ModelState.IsValid)
return View(customer);
//locate the customer in the database and update the model with the views model.
Customer thisCustomer = customerRepository.Single(x => x.CustomerID == customer.CustomerID);
if (TryUpdateModel<Customer>(thisCustomer))
customerRepository.SaveAll();
else
return View(customer);
//return to the index page if complete
return RedirectToAction("index");
}
edit 2
my custom model binder
public class CustomContactUsBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ContactFormViewModel contactFormViewModel = bindingContext.Model as ContactFormViewModel;
if (!String.IsNullOrEmpty(contactFormViewModel.Name))
if (contactFormViewModel.Name.Length > 10)
bindingContext.ModelState.AddModelError("Name", "Name is too long. Must be less than 10 characters.");
base.OnModelUpdated(controllerContext, bindingContext);
}
}
This blog post sounds like it solves your exact problem:
UpdateModel(user, "User");
Since it seems the data you want to bind to is prefixed by the viewmodel name.
Related
I have recently taken over support of an ASP.NET MVC project, and am trying to work through some of the errors, one in particular has me stumped though.
We have a 'New' page to add new items, with the following code running when the page is posted:
[HttpPost]
public ActionResult New(RecordView i)
{
if(ModelState.IsValid)
{
repository.AddRecord(i.DogIncident);
return View("Index");
}
return View("Index");
}
However, when it tries to load the Index view, I get the following error: "Object reference not set to an instance of an object." and it points to the following block of code at the top of a file called RecordsView.cshtml:
#for (var i = 0; i < Model.Records.Count; i++)
The record does add correctly though, it just doesn't load the listings page correctly, and since this is just a "nice to have" I thought I'd simplify things by changing it so that it either returns some text which generates an error as it's expecting a boolean returned.
Any ideas on how to fix this please? I'm stumped.
Thanks!
The flow of your code here doesn't look right:
[HttpPost]
public ActionResult New(RecordView i)
{
if(ModelState.IsValid)
{
repository.AddRecord(i.DogIncident);
return View("Index");
}
return View("Index");
}
From your description above, it sounds as though you're POSTing from your New view, to this New action, which should then redirect, when successful, to your Index action. Currently, the above code is not doing that, and it also fails to redisplay the form if the model isn't valid. It should probably look more like this:
[HttpPost]
public ActionResult New(RecordView i)
{
if(ModelState.IsValid)
{
repository.AddRecord(i.DogIncident);
return RedirectToAction("Index");
}
// Redisplay the `New` view, passing the model
// back to it in order to repopulate the form
return View(i);
}
The important distinction between return View(...) and return RedirectToAction(...) is that the latter will run the action and return the associated view. The former will simply return the view. That has implications in that if the Index action builds a model, and passes it to the Index view, none of that will happen if you simply return the view.
You could of course do something like:
return View("Index", new YourModelType());
but that isn't going to work if, as discussed above, your Index action performs some other data construction for your model, such as building drop down lists, which new YourModelType() wouldn't do.
Also, when data that is sent to a POST action is valid, you should be redirecting to another action (as I've done above), rather than simply returning a view, in order to conform with the Post-Redirect-Get pattern, which prevents some types of duplicate form submissions.
You display Index view, and seems that it requires some Model - Model.Records. And you don't pass it in this HttpPost action.
If you have action for that Index page, then you can just do
[HttpPost]
public ActionResult New(RecordView i)
{
if(ModelState.IsValid)
{
repository.AddRecord(i.DogIncident);
return RedirectToAction("Index");
}
return View(i);
}
It will just redirect a user to Index view, after creation of new RecordView item
Basically you may are trying to achieve PRG(Post/Redirect/Get) modal.
I guess, the problem is you are not sending the model for your GET request.
Post--> Save --> Redirect --> Load Data -->Assign to View in Index -->Access in view
//POST & REDIRECT
[HttpPost]
public ActionResult New(RecordView i)
{
if(ModelState.IsValid)
{
repository.AddRecord(i.DogIncident);
return RedirectToAction("Index");
}
}
//GET
public ActionResult Index()
{
var model=new MyViewModel();
model.Records=repository.GetRecords(i.DogIncident);
return View(model); //Assign to View in Index
}
Index.cshtml
#model MyViewModel
#for (var i = 0; i < Model.Records.Count; i++)
If Records is a list, make sure your ViewModel has a constructor:
public class RecordView
{
public List<Record> Records { get; set; }
public RecordView()
{
Records = new List<Record>();
}
}
You mentioned that the record adds correctly, so you must be passing a valid record model into your view from some other action than the one provided.
If #for (var i = 0; i < Model.Records.Count; i++) is the cause of this error, my guess is that the model exists, but the Records property has not been set. One immediate work around would be checking the existence of this property before accessing it. For example:
if (Model.Records != null) {
for (var i = 0; i < Model.Records.Count; i++) .....
}
i think you have collections are not instantiated, the error may be in models not in view models. this because when ever you have a collection you need to instantiate inside of constructor of that entity.
May be this is your answer...!Just look
[HttpPost]
public ActionResult New(RecordView)
{
if(ModelState.IsValid)
{
repositry.AddRecord(i.DogIncident);
return RedirectToAction("Index");
}
}
Is there any way of changing the binding prefix with a value which comes from the request parameters?
I have many nested search popups, and all of them shares the same ViewModel.
I can add a binding prefix to all the fields when requesting for the Search filters, but i don't know how can i make the [Bind(Prefix = "")] to work with values coming from the request parameters.
// get the search filters with the bindingPrefix we need
public ActionResult Search(string bindingPrefix)
{
ViewData.TemplateInfo.HtmlFieldPrefix = bindingPrefix;
SearchViewModel model = new SearchViewModel
{
BindingPrefix = bindingPrefix
};
return PartialView("_SearchFilters", model);
}
// post the search filters values
[HttpPost]
public ActionResult Search([Bind(Prefix = model.BindingPrefix)]SearchViewModel model)
{
}
I don't know why you would want to do this, but this should work.
In your form on the view, have a hidden value
#Html.Hidden("BindingPrefix", Model.BindingPrefix)
Modify your action to the following
[HttpPost]
public ActionResult Search(SearchViewModel model)
{
UpdateModel(model, model.BindingPrefix);
}
I have an index action on a controller as follows...
public ActionResult Index(string errorMsg = "")
{
//do stuff
ViewBag.ErrorMsg=erorMsg;
return View();
}
I have another action that is an http post for Index.
When there is something wrong I want to reload the Index page and show the error...
I have my view already conditionally showing errorMsg. But I cannot figure out how to call Index and pass in the error string?
Typically, you'd just share the view between the two actions. I'm guessing you have actions that look something like this (the more info you provide about what index does, the better my example will be):
public ActionResult Index()
{
return View();
}
[HttpPost, ActionName("Index")]
public ActionResult IndexPost()
{
if (!ModelState.IsValid)
{
ViewBag.ErrorMsg = "Your error message"; // i don't know what your error condition is, so I'm just using a typical example, where the model, which you didn't specify in your question, is valid.
}
return View("Index");
}
And Index.cshtml
#if(!string.IsNullOrEmpty(ViewBag.ErrorMsg))
{
#ViewBag.ErrorMsg
}
#using(Html.BeginForm())
{
<!-- your form here. I'll just scaffold the editor since I don't know what your view model is -->
#Html.EditorForModel()
<button type="Submit">Submit</button>
}
If I understand you correctly you just need to hit the url with the errorMsg in the query string:
/*controllername*/index?errorMsg=*errormessage*
However, when there is something wrong you don't necessarily need to reload the page. Seems like you might be approaching this in the wrong way..?
You can use RedirectToAction to redirect to the page, with a querystring for errorMsg value.
[HttpPost]
public ActionResult Index(YourViewModel model)
{
try
{
//try to save and then redirect (PRG pattern)
}
catch(Exception ex)
{
//Make sure you log the error message for future analysis
return RedirectToAction("Index",new { errorMs="something"}
}
}
RedirectToAction issues a GET request. So your form values will be gone, because HTTP is stateless. If you want to keep the form values as it is in the form, return the posted viewmodel object again. I would get rid of ViewBag and add a new property called ErrorMsg to my ViewModel and set the value of that.
[HttpPost]
public ActionResult Index(YourViewModel model)
{
try
{
//try to save and then redirect (PRG pattern)
}
catch(Exception ex)
{
//Make sure you log the error message for future analysis
model.ErrorMsg="some error";
return View(model);
}
}
and in the view you can check this model property and show the message to user.
Hi I have a question about model binding. Imagine you have an existing database entity displayed in a form and you'd like to edit some details, some properties eg createddate etc are not bound to the form, during model binding, these properties are not assigned to the model as they are not on the http post data or querystrong etc, hence their properties are null. In my controller method for update , Id just like to do
public ActionResult Update( Entity ent)
{
//Save changes to db
}
but as some properties are null in ent, they override the existing database fields which are not part of the form post data, What is the correct way to handle this? Ive tried hidden fields to hold the data, but model binding does not seem to assign hidden fields to the model. Any suggestions would be appreciated
You shouldn't be sending your entity to your view, you should send a slim version of it called a DTO (data transfer object) or ViewModel.
Only send the properties to the view (and hence the form that gets posted back) that you want the user to update with that action.
In your POST handler you can then validate and copy the properties across to your entity.
an AutoMapper can help if you have lots of properties
something like:
public class User
{
int id;
string name;
string email;
}
public class EditUserEmailDto
{
string email;
}
// get
public ActionResult EditEmail(int id,)
{
return View("EditEmail", new EditUserEmailDto());
}
// post
public ActionResult EditEmail(int id, EditUserEmailDto dto)
{
if(!ModelState.IsValid)
return View("EditEmail", dto);
var user = userRepo.Get(id);
user.email = dto.email;
userRepo.Save(user);
return;
}
Here's a simplification of my real models in ASP.NET MVC, that I think will help focus in on the problem:
Let's say I have these two domain objects:
public class ObjectA
{
public ObjectB ObjectB;
}
public class ObjectB
{
}
I also have a view that will allow me to create a new ObjectA and that includes selecting one ObjectB from a list of possible ObjectBs.
I have created a new class to decorate ObjectA with this list of possibilities, this is really my view model I guess.
public class ObjectAViewModel
{
public ObjectA ObjectA { get; private set; }
public SelectList PossibleSelectionsForObjectB { get; private set; }
public ObjectAViewModel(ObjectA objectA, IEnumerable<Location> possibleObjectBs)
{
ObjectA = objectA;
PossibleSelectionsForObjectB = new SelectList(possibleObjectBs, ObjectA.ObjectB);
}
}
Now, what is the best way to construct my view and controller to allow a user to select an ObjectB in the view, and then have the controller save ObjectA with that ObjectB selection (ObjectB already exists and is saved)?
I tried creating a strongly-typed view of type, ObjectAViewModel, and binding a Html.DropDownList to the Model.PossibleSelectionsForObjectB. This is fine, and the I can select the object just fine. But getting it back to the controller is where I am struggling.
Attempted solution 1:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectAViewModel objectAViewModel)
This problem here is that the objectAViewModel.ObjectA.ObjectB property is null. I was thinking the DropDownList which is bound to this property, would update the model when the user selected this in the view, but it's not for some reason.
Attempted solution 2:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectA objectA)
This problem here is that the ObjectA.ObjectB property is null. Again, I thought maybe the DropDownList selection would update this.
I have also tried using the UpdateModel method in each of the above solutions, with no luck. Does anyone have any ideas? I'm guessing I'm missing a binding or something somewhere...
Thanks!
I use code as follows:
[HttpPost]
public ActionResult Create([Bind(Exclude = "Id")]ObjectA objectAToCreate)
{
try
{
Repository.AddObjectA(objectAToCreate);
return RedirectToAction("Details", new { id = objectAToCreate.Id });
}
catch
{
return View();
}
}
With the following code in a Repository (Entity Framework specific):
public void AddObjectA(ObjectA objectAToAdd)
{
objectAToAdd.ObjectB = GetObjectB(objectAToAdd.ObjectB.Id);
_entities.AddToObjectAs(objectAToAdd);
_entities.SaveChanges();
}
public void GetObjectB(int id)
{
return _entities.ObjectBs.FirstOrDefault(m => m.id == id);
}
As per your commments, it is essentially reloading the object from the underlying data service, however I didn't find the need to use the ModelState to access the attempted value.
This is based on a view coded along these lines:
<p>
<%= Html.LabelFor( f => f.ObjectB.Id) %>
<%= Html.DropDownList("ObjectB.Id", new SelectList((IEnumerable)ViewData["ObjectBList"], "Id", "Descriptor"),"") %>
<%= Html.ValidationFor( f => f.ObjectB, "*") %>
</p>
Note that this could be improved to use a strongly typed ViewModel (which I believe you already do) and also to create a custom Editor Template for ObjectB such that the call could be made using:
<%= Html.EditorFor( f => f.ObjectB ) %>
After some more research it doesn't look like this is a case ASP.NET MVC will take care of for me. Perhaps there is a data service binding model I can use (so MVC would automatically grab the appropriate object out of memory, based on what was selected in the dropdown), but for now, I can fix this by handling it in the controller:
Get the selected item from the dropdown using Controller.ModelState
Reload that ObjectB from the underlying data service
Assign that ObjectB to ObjectA.ObjectB
Save ObjectA
So my controller method looks like this now:
Edited based on the comment from LukLed
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ObjectA objectA, string objectBStr)
{
ObjectB objectB = _objBService.Get(objectBStr);
objectA.ObjectB = objectB;
_objAService.Save(objectA);
return RedirectToAction("Details", new { id = objectA.Id });
}