I'm completely new to MVC, moving from Webforms which I know well.
I've got the basics and now trying to work with a database. I can send data to the view no problem but how do I get the edited back?
I'm seeing lots of examples like this:
public ActionResult Update(MyObject myObject)
{
//do database EFW here
}
I can't work out how you get the data from the web-form(view) into myObject. I'm not doing anything clever I have followed a few examples on line but none explain how this mechanism works.
For the form I'm using #Html.BeginForm and helper functions (Html.TextBoxFor) etc, to create my form fields. The problem is now I can't get the edited data back so I can update my data. I hit the right controller action.
The only thing that's different is my form a is a partial view inside a bootstrap dialog.
UPDATE
Well I finally have a object back in my action method, however it doesn't have the key field populated, only the things on the form so I still can't save it (as an update) because I don't know which one it is.
I get and send this object to the view
var diary = new mySQLEntities().Diaries.Single(d => d.DRefno == Id);
and all is good, then what I want to do is this, or something similar
public ActionResult DiaryItemUpdate(mymvc.Diary Diary)
{
mySQLEntities db = new mySQLEntities();
db.Diaries.Add(Diary);
db.SaveChanges();
return RedirectToAction("Index", "Home");
}
[Bind(Include = ... )] doesn't work, it just deletes ALL the properties, even the ones I was getting back.
All the examples I look at seem to do the same thing as I'm doing but it just doesn't work.
I thought this was going to be easy but it isn't. I looked at EF a while back and ditched it for this very reason. It says it makes development easy, it's making it a lot harder for me. I can see why I stuck with web-forms for so long.
Any properties you pass in need to be instantiated in the form if you want them passed back. You need to add the ID of the object as a hidden form element:
#Html.HiddenFor(model => model.ID)
This will ensure that the ID for the object you sent to the view gets posted back.
Related
Right, so a) I'm not sure if this is the right forum for this question, I failed to find a better suggestion, and b) I couldn't find an answer to a question I can't believe I haven't asked before:
So if I have a model, standard form to fill in and it errors so I throw it back to the user, how do I, or how SHOULD I, structure the code to make sure all the supporting properties are constructed?
Example:
public ActionResult DoThing()
{
var model = new DoThingModel();
model.Things = ThingService.FetchThings();
//Do Other Things
return View(model);
}
The fairly standard GET action
[HttpPost]
public ActionResult DoThing(DoThingModel model)
{
var valid = ThingService.ValidateThing(model);
if(!valid)
{
model.ValidationMessage = "Ohh no validation failed";
model.valid = false;
return View(model); //I have't rebuilt model.Things with ThingService.FetchThings()
}
else
{
return RedirectToAction("Success");
}
}
So if the thing is valid redirect to the success page, but otherwise return the form (contained within the model) to the user with a validation message. Problem is my form page has things on it and I haven't populated the model with it.
Now, this is a very simple example and I could just pass back things in the form, or duplicate the line of code that populates it. But imagine I have a much more complicated model, with breadcrumbs, multiple properties that need populating. Code duplication = bad, so how SHOULD I be making sure the model has everything I need before passing it back? I thought about creating a service but it's a service with one function. I thought about using a constructor, but that doesn't play well with model binding. I thought about creating a DoThingModel.Init() that does all the work, but putting code like that inside a model class seems dirty. I played around with a private function in the Controller that filled in all the required properties, but this seems dirty again.
I feel like there should be a standard code pattern that goes => Create New Model => Make sure Model has everything that supports the page => return Model => validate Model => re-apply any properties => return Model => repeat until valid.
Like I said in the intro, this must be a standard thing somewhere, but I can't find it.
I am using asp.net core 2.2 and trying to implement the pattern outlined at POST-REDIRECT-GET using TempData in ASP.NET Core. I have two actions as follows:
[ImportModelState]
public async Task<IActionResult> Upload(int id, CancellationToken ct)
{
var model = new MyModel();
// ...
return this.View(model);
}
[HttpPost]
[ExportModelState]
public async Task<IActionResult> Upload(int id, MyModel model, CancellationToken ct)
{
// ...
}
After accessing the get view, entering the form details and submitting, I set a break point in the post action to view the bound model data for the MyModel parameter and I see an array for a boolean Replace field which backs a checkbox in the views form: However the Replace field works and if the model state is valid, everything succeeds for both of the form checkbox states.
If the form is invalid and I serialize the modelstate to tempdata and merge it on a redirect to the get action, I get an exception System.InvalidOperationException: The parameter conversion from type 'Newtonsoft.Json.Linq.JArray' to type 'System.Boolean' failed because no type converter can convert between these types.
Why does the model binder create an array for the boolean field (is this due to the semantics for forms and check boxes?) which works on the post action, but the same data fails to bind against the same model type when repopulating the view?
The serialization to temp data uses the following logic:
var errorList = modelState
.Select(kvp => new ModelStateTransferValue
{
Key = kvp.Key,
AttemptedValue = kvp.Value.AttemptedValue,
RawValue = kvp.Value.RawValue,
ErrorMessages = kvp.Value.Errors
.Select(p => p.ErrorMessage)
.ToList(),
});
The deserialization and merging uses the following logic:
var errorList = JsonConvert.DeserializeObject<List<ModelStateTransferValue>>(serialized);
var modelState = new ModelStateDictionary();
foreach (var item in errorList)
{
modelState.SetModelValue(item.Key, item.RawValue, item.AttemptedValue);
foreach (var error in item.ErrorMessages)
{
modelState.AddModelError(item.Key, error);
}
}
filterContext.ModelState.Merge(modelState);
The issue is that you aren't actually following post-redirect-get. The redirect happens on success. If there's validation errors, you just return the view again. No need to persist anything in TempData. Even the author of the linked article points out:
As PRG is primarily intended to prevent double form submissions, it does not necessarily follow that you should REDIRECT a user if the form is invalid. In that case, the request should not be modifying state, and so it is valid to submit the form again.
Where he goes astray is in the whole use TempData for invalid submissions and still redirect thing. That's just patently wrong, and I actually find it a little surprising that that advice is coming from Andrew Lock. I've referenced his articles quite often, but apparently never noticed this one.
In software development, there's an unspoken golden rule: don't be a unicorn. Doing things differently than virtually every other developer on the planet doesn't make you unique and special; it makes you an idiot.
UPDATE
Wow. Just reading through the comments and came across one from Andrew where he says:
Having said that, I haven't actually personally used this approach - as you say it adds a lot of complexity
That's a done deal right there. I mean it's an interesting thought experiment, but if it's not good enough for the guy who wrote it, it's not good enough for anyone else either.
All this does is get rid of the "Confirm form resubmission" dialog when a user refreshes a page that was submitted with invalid data. The case where the post is successful is already covered by PRG in general. Given the complexity, and all the extra requests, it's just not even remotely worth it for something that might not even ever happen. Personally, I've found users to be afraid to refresh the page, fearing that they'll have to re-enter all the fields again (which ironically this solution would actually cause, whereas they'd have been just fine refreshing otherwise).
So this is strange. I have a model composed of person and List<person>. when the user submits the view back to the controller, person is add to the List<person> and then we clear out all the person fields by the means of a person=new Person();.
I would expect that when return to the view, all the person fields are cleared out and the view is a "fresh" start. However, for some strage reason I cannot figure out, most of the fields for person are still filled out with the previous values (even after the person=new Person();).
The model is a complex model, composed of several "objects of objects", and some of the objects inherit from other objects. Still, I can't understand why the view still shows values from previous posts.
EDIT !!!!!
I am posting via a regular form post (HTML.BeginForm()). So here is my controller:
[HttpPost]
[ValidateAntiForgeryToken()]
public ActionResult sendInscriptorRequest(inscriptionModel _model)
{
var _umbracoModel = Umbraco.TypedContentAtRoot().FirstOrDefault();
_model = bllInscripcion.fillModel(_model);
_model = _model.Map(_umbracoModel);
if (_model.formAction == "addParticipants")
{
_model.participants.Add(_model.newParticipant);
_model.newParticipant = new participantModel();
_model.ui.participants.btnTotalParticipantsNumber += 1;
return View("addParticipants", _model);
}
else
{
_model.newParticipant = bllInscripcion.preFillParticipantContactWithInscriptorContact(_model);
return View("formularioInscripcion2", _model);
}
}
You trying to return the view with the model from the POST action method. That won't work because the intended behavior is to return the model state that was posted so the user can make corrections.
If you're posting data and then want to return an empty view you should redirect to the GET action method that returns the initial state of the view. It's known as a Post-Redirect-Get pattern.
http://en.wikipedia.org/wiki/Post/Redirect/Get
Following this pattern will also solve the problem of the user refreshing the page following the POST and getting that annoying dialog that says "You're about to resubmit data. Are you sure?" to which the user always responds "Yes" and then your system has to deal with the potential duplication.
Explanation of the problem:
This is because of ModelState. By default, MVC will retain the model state within a post action if you're returning the same view. To prevent this, simply clear it before returning the view:
ModelState.Clear();
This will mean that whatever model you pass back to the view will be used, not the one stored in model state.
DISCLAIMER:
You should use ModelState.Clear only when needed. Judging by your code, you don't need to use it. Simply redirecting to another page after you have performed the necessary logic is the better thing to do here (as per the PRG pattern).
I'm relatively new to view models and I'm running into a few problems with using them. Here's one situation where I'm wondering what the best practice is...
I'm putting all the information a view needs into the view model. Here's an example -- please forgive any errors, this is coded off the top of my head.
public ActionResult Edit(int id)
{
var project = ProjectService.GetProject(id);
if (project == null)
// Something about not found, possibly a redirect to 404.
var model = new ProjectEdit();
model.MapFrom(project); // Extension method using AutoMapper.
return View(model);
}
If the screen only allows editing of one or two fields, when the view model comes back it's missing quite a bit of data (as it should be).
[HttpPost]
public ActionResult Edit(int id, ProjectEdit model)
{
var project = ProjectService.GetProject(id);
if (project == null)
// Something about not found, possibly a redirect to 404.
try
{
if (!ModelState.IsValid)
return View(model) // Won't work, view model is incomplete.
model.MapTo(project); // Extension method using AutoMapper.
ProjectService.UpdateProject(project);
// Add a message for the user to temp data.
return RedirectToAction("details", new { project.Id });
}
catch (Exception exception)
{
// Add a message for the user to temp data.
return View(model) // Won't work, view model is incomplete.
}
}
My temporary solution is to recreate the view model from scratch, repopulate it from the domain model, reapply the form data to it, then proceed as normal. But this makes the view model parameter somewhat pointless.
[HttpPost]
public ActionResult Edit(int id, ProjectEdit model)
{
var project = ProjectService.GetProject(id);
if (project == null)
// Something about not found, possibly a redirect to 404.
// Recreate the view model from scratch.
model = new ProjectEdit();
model.MapFrom(project); // Extension method using AutoMapper.
try
{
TryUpdateModel(model); // Reapply the form data.
if (!ModelState.IsValid)
return View(model) // View model is complete this time.
model.MapTo(project); // Extension method using AutoMapper.
ProjectService.UpdateProject(project);
// Add a message for the user to temp data.
return RedirectToAction("details", new { project.Id });
}
catch (Exception exception)
{
// Add a message for the user to temp data.
return View(model) // View model is complete this time.
}
}
Is there a more elegant way?
EDIT
Both answers are correct so I would award them both if I could. The nod goes to MJ however since after trial and error I find his solution to be the leanest.
I'm still able to use the helpers too, Jimmy. If I add what I need to be displayed to the view bag (or view data), like so...
ViewBag.Project= project;
I can then do the following...
#Html.LabelFor(model => ((Project)ViewData["Project"]).Name)
#Html.DisplayFor(model => ((Project)ViewData["Project"]).Name)
A bit of a hack, and it requires the domain model to be decorated with System.ComponentModel.DisplayNameAttribute in some cases, but I already do that.
I'd love to call...
#Html.LabelFor(model => ViewBag.Project.Name)
But dynamic causes a problem in expressions.
After some trial-and-error (aka code it, then hate it) learning, my currently preferred approach is:
I use view-models only to bind input fields. So in your case, if your view is only editing two fields, then your view-model will only have two properties. For the data required to populate the view (drop-down lists, labels, etc), I use the dynamic ViewBag.
I believe that displaying the view (i.e. populating anything the view needs to display), and capturing the posted form values (binding, validation, etc) are two separate concerns. And I find that mixing the data required to populate the view with that which is posted back from the view gets messy, and creates exactly your situation more often than not. I dislike partially populated objects being passed around.
I’m not sure how this plays out with Automapper (for mapping the domain object to the dynamic ViewBag) though, as I haven’t used it. I believe it has a DynamicMap method that may work? You shouldn’t have any issues auto-mapping the posted strongly-typed ViewModel onto the Domain object.
If I understand correctly, your viewmodel probably looks very similar to your domain entity. You mentioned that the viewmodel can come back mostly empty because only certain fields were editable.
Assuming you have a view where only a few fields are available for edit (or display), these are the only fields you should make available in your viewmodel. I usually create one viewmodel per view, and let either the controller or a service handle the user's input and map it back up with the domain entity after performing some validation.
Here's a thread concerning best-practices for viewmodels that you might find useful.
Edit: You can also accept a different view model in your Edit/POST action than your Edit/GET action serves up. I believe this should work as long as the model binder can figure it out.
Hello I have recently began work on a largely JQuery/JQueryUI based ASP .Net website. The idea was to have only one page and to have the rest of the content be dynamic, etc loaded in through dialogs and ajax.
The problem is however when a Create & a Edit form for the same model are open in dialogs at the same time some JQueryUI widgets such as the DatePicker stop working as the forms cause the DOM to have duplicate id's on the fields which are present in both.
So I tried using this code on the controller:
ViewData.TemplateInfo.HtmlFieldPrefix = "Create"; // or Edit etc
This worked to fix the DatePicker problem, but the fields no longer mapped to the model when they were posted back to the controller.
Does anyone know how to fix this?
You could try specifying the same prefix when binding back:
[HttpPost]
public ActionResult Create([Bind(Prefix = "Create")] CreateViewModel model)
{
...
}
For anyone having the same issue, you could also just rename your model to "create" as shown below:
[HttpPost]
public ActionResult Create(CreateViewModel create)
{
...
}
Which I think looks nicer, but it's a bit risky. If another developer, or you, decides to change the parameter name later the form will break which is not ideal.
I figured this out as one of my forms was binding correctly and the other wasn't. One had the prefix as the parameter name and one was just "model".