.NET MVC - Custom Edit form - c#

EDIT v2: - SOLVING STEP
Wrote custom ModelBinder and now it works. Additionally I tryed out and noticed, that it would work with the Default ModelBinder if the Boolean value is parsed into the hidden input as string ("boolean.toString()").
But with the Deafault ModelBinder I wouldn't have catched this.
EDIT v1:
I have might have moved on by adding hidden inputs of the remaining fields. Now it appears to not bind the form values with the model properly...
From what I understand the Default Binder, when I've got ALL field names named same as the form input name attributes (int season ....
I might need to write my own binder, but than that Model elegance, which I so much expect, goes out of the window.
Still there might be better for me so far unknown solution. Any suggestions and help appreciated.
ORIGINAL:
Got an issue regarding creation and being able to construct my own edit action in which I will change only few attributes (in this case the season and the episode fields) and I want it to work with the model, not its single fields.
I got this model...
public class Entry
{
public int entryID { get; set; }
[Required]
[Range(0, 99)]
public int season { get; set; }
[Required]
[Range(0, 99)]
public int episode { get; set; }
[Required]
public Boolean button_dark { get; set; }
[Required]
public int showID { get; set; }
public virtual Show show { get; set; }
public int? pictureID { get; set; }
public virtual Picture picture { get; set; }
public Entry () {
season = 0;
episode = 0;
}
}
... and I got classic GET and POST actions in the controller...
public ActionResult Index(int id)
{
Entry entry = db.Entries.Find(id);
if (entry == null)
{
return HttpNotFound();
}
return View(entry);
}
[HttpPost]
public ActionResult Index(Entry entry)
{
if (ModelState.IsValid)
{
db.Entry(entry).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Home");
}
return View(entry);
}
... and in the view I essentialy have.
<form action="/Entry/Index/#Model.entryID" method="post">
<input type="number" name="season" value="#Model.season" />
<input type="number" name="episode" value="#Model.episode"/>
<input class="button" type="submit" value="Submit" />
</form>
From what I have read about the Model Binder, it shoud chew it just ok. (I hope I did't missinterpreted or missunderstood anything)
When submiting the form i get this exception at the "db.SaveChanges();" statement.
System.Data.Entity.Infrastructure.DbUpdateConcurrencyException was unhandled by user code
HResult=-2146233087
Message=Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
I know what it says, but don't know why. And as a quite newbie in the .NET MVC I don't understand other forum posts to the exception itself.
Does anyone PLEASE know where and what should I do or read to help me with that?
Many thanks.

OK, now I will summarize this, for me a little unintuitive, solution to my problem...
You need to have all model fields in the form (even if they were
hidden)
Be sure to insert right data types in the input values,
otherwise the binder won't bind succesfully data to the model (for
example: Boolean must go there as String)

Related

Model dropping List<>s after DropDownList

I'm working on a search criteria building page. In addition to several string and numerical type fields, there are several "multiple choice" options.
I'm using the [Get] signature without parameters(pass the CriteriaModel to the view) >> [Post] signature with CriteriaModel parameter (redirect to searching controller)
I've built lightweight option classes (just value, name pairs) and am populating several List<> with the primitive options.
Using Html.DropDownListFor, I'm able to get them to display.
...but...
When I enter the [Post] version, the List<>s are all set to null and empty. Further, the other criteria fields supposed to be populated afterwards are also default and empty.
Technically, I don't need a whole list of values back - if I could even just have the index of the selected value - but I'm up against a wall here.
Pertinent model data:
public class CriteriaModel
{
[DisplayName("Owner Name")]
public string OwnerName { get; set; }
[DisplayName("Subdivision")]
public List<Subdivision> Subdivision { get; set; }
[DisplayName("PIN")]
public string PIN { get; set; }
}
public class Subdivision
{
public int ID { get; set; }
public string Name { get; set; }
}
Pertinent controller code:
[HttpGet]
public ActionResult Index()
{
CriteriaModel criteria = new CriteriaModel();
...fill in the Subdivisions...
View(criteria);
}
[HttpPost]
public ActionResult Index(CriteriaModel search_criteria)
{
return View("Search obtained" + search_criteria.Subdivision.First().Name);
}
And pertinent View markup:
#model REOModern.Models.CriteriaModel
...bunch of HTML...
#Html.LabelFor(model => model.Subdivision)
#Html.DropDownListFor(x => x.Subdivision, new SelectList(Model.Subdivision, "ID", "Name", Model.Subdivision.First().ID))
...other HTML...
<button type="submit" class="btn btn-primary" value="Index">Search</button>
I should clarify: I know that my 'return View("Search obtained" + ...' will fail, but it should show the piece of data that I need. The problem is it's a null reference exception. Until I can fix that, there's no point in building a user-friendly View for submitted search criteria.
MVC does not repopulate the List<> elements.
You would split the selected value out into another property of the model.
So in your model, include something like this
public int SelectedValue { get; set; }
Then for your Html.DropDownListFor helper you would use
Html.DropDownListFor(model => model.SelectedValue, Model.DropDownList, new { /* htmlAttributes */ });
Of course they're empty. The only data that exists in your post action is that which was posted via the form. Since the entire dropdown list, itself, was not posted, merely a selected item(s), the lists are empty. For anything like this, you need to rerun the same logic in your post action to populate them as you did in your get action. It's usually better to factor out this logic into a private method on your controller that both actions can use:
private void PopulateSomeDropDownList(SomeModel model)
{
// logic here to construct dropdown list
model.SomeDropDownList = dropdownlist;
}
Then in your actions:
PopulateSomeDropDownList(model);
return View(model);

Add a javascript array to Asp.Net MVC form's submit (replacing model List)

Let's imagine I have the following situation:
I have a question form and I want to add answer choices to it (my user should be allowed to add as many choices as he wants).
So I have this ViewModel (being sent to the view).
public class QuestionEdit
{
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Question { get; set; }
public List<Choice> Choices { get; set; }
}
public class Choice
{
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Choice { get; set; }
public bool Correct {get; set;^}
}
My Controller's Edit Post looks like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,Question,Choices")] QuestionEdit vm)
{
if (ModelState.IsValid)
{
/*Removed for clarity*/
}
return View(vm)
}
Is there any way of me getting the content from Choices and converting it to a javascript list, changing it as my user adds or removes items only by using javascript/ajax and when submitting the form, would it be a way for me to adding it back as if it were the Choices that my controller will be able to read?
I am aware that I could use the an custom EditorFor for this List, but everytime someone adds a new Choice I would have to post the entire form to add it and them get it back, so I wanted to change this only through javascript, as it requires only simple validations (not needing any server/database validation).
Thanks a lot.
You can use as much JavaScript and ajax as you need, as long as in your form you have inputs that are named in a way that the model binder expects.
What I mean by that is, you can create form inputs with a name attribute in the form of:
<input type="text" name="Choices[0].Id" />
<input type="text" name="Choices[0].Choice" />
<input type="hidden" name="Choices[0].Correct" />
<input type="text" name="Choices[1].Id" />
<input type="text" name="Choices[1].Choice" />
<input type="hidden" name="Choices[1].Correct" />
And so on. When you post your form, the model binder will attempt to take whatever values are attached and set them on your QuestionEdit object based on the name. So in the above case, vm.Choices in your action method will contain 2 items.
This link gives some pretty good examples that should point you in the right direction:
http://www.codeproject.com/Articles/551576/ASP-NET-MVC-Model-Binding-and-Data-Annotation
From there, it's up to your JavaScript to create input elements accordingly and append them to your form.
Hope this helps.

MVC5 Bind form post to collection - coming up null

I am just attempting to bind a collection from a form post. My collection in the model is a different name than what I'm trying to bind to, which my be the cause of the issue. I had this working, and then it stopped. I cannot find what changed to make it stop.
I thought my question was similar to this one:
ASP.NET MVC5: Want to update several items in Collection with model binding
And I have read this a few times (Scott Hanselman can be counted on to give a clear picture of such things):
http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx
Here's some code:
public class AttestModel
{
public Account Account { get; set; }
public List<AccountSimpleDetail> AccountSimpleDetails { set; get; }
}
public class AccountSimpleDetResponseModel
{
public int AccountSimpleDetailId { get; set; }
public int AnswerId { get; set; }
}
public partial class AccountSimpleDetail :
{
public int AccountSimpleDetailId { get; set; }
public int? SaqSimpleQuestionId { get; set; }
public int? SaqAnswerId { get; set; }
...More fields go on here
}
In the View I have:
#for (var i = 0; i < Model.AccountSimpleDetails.Count(); i++)
{
#Html.HiddenFor(x => x.AccountSimpleDetails[i].AccountSimpleDetailId)
#Html.HiddenFor(x => x.AccountSimpleDetails[i].AnswerId)
}
I have some Javascript to handle clicking on a bunch of buttons in the browser which sets the AnswerId in the hidden fields. This is working fine.
The markup produced looks like this:
<input data-val="true" data-val-number="The field AccountSimpleDetailId must be a number." data-val-required="The AccountSimpleDetailId field is required." id="AccountSimpleDetails_1__AccountSimpleDetailId" name="AccountSimpleDetails[1].AccountSimpleDetailId" type="hidden" value="71" />
<input data-val="true" data-val-number="The field AnswerId must be a number." id="AccountSimpleDetails_1__AnswerId" name="AccountSimpleDetails[1].AnswerId" type="hidden" value="1" />
The method signature looks like this:
public ActionResult Attest(int id, List<AccountSimpleDetResponseModel> AccountSimpleDetails)
What I used previously (which stopped working) is:
public ActionResult Attest(int id, [Bind(Prefix = "AccountSimpleDetails")] List<AccountSimpleDetResponseModel> simpleDetails)
The result of the bind is always null.
I know when I get to the point where I try something, it doesn't work, I try something else... it simply means I am missing some fundamental point. Thank you for your time.
EDIT
Here is the post request (Raw from Watchlist):
AccountSimpleDetails%5b1%5d.AccountSimpleDetailId=165&AccountSimpleDetails%5b1%5d.AnswerId=1&AccountSimpleDetails%5b2%5d.AccountSimpleDetailId=166&AccountSimpleDetails%5b2%5d.AnswerId=1&AccountSimpleDetails%5b3%5d.AccountSimpleDetailId=167&AccountSimpleDetails%5b3%5d.AnswerId=1&AccountSimpleDetails%5b4%5d.AccountSimpleDetailId=168&AccountSimpleDetails%5b4%5d.AnswerId=1&DXScript=1_171%2c1_94%2c1_93%2c17_33%2c17_2%2c1_152%2c1_164%2c1_91%2c1_156%2c1_101%2c17_7%2c1_154%2c1_103%2c1_102%2c17_8%2c1_114%2c1_121%2c1_169%2c1_138%2c1_170%2c1_124%2c17_9%2c1_163%2c1_162%2c1_147%2c17_32%2c1_157%2c1_98%2c1_125%2c1_104%2c1_166%2c1_139%2c17_13%2c1_97%2c1_141%2c1_142%2c17_15%2c1_155%2c1_143%2c1_144%2c17_16%2c17_17%2c1_126%2c17_11%2c1_146%2c1_149%2c17_20%2c1_160%2c17_22%2c1_158%2c1_153%2c1_161%2c17_25%2c1_165%2c17_28%2c17_31%2c1_100%2c5_5%2c5_4%2c4_11%2c4_10%2c4_6%2c4_7%2c4_9%2c17_14%2c4_12%2c1_113%2c1_116%2c4_13%2c4_14%2c1_110%2c1_112%2c1_137%2c17_12%2c1_159%2c7_49%2c7_47%2c7_51%2c17_21%2c1_105%2c1_108%2c1_117%2c17_0%2c1_120%2c1_106%2c17_1%2c1_107%2c17_3%2c1_109%2c1_122%2c17_5%2c1_145%2c1_119%2c17_18%2c17_19%2c1_118%2c17_29%2c1_123%2c10_2%2c10_1%2c10_3%2c10_4%2c17_4%2c9_23%2c9_22%2c9_24%2c17_24%2c9_13%2c9_10%2c9_8%2c17_23%2c9_12%2c9_9%2c9_15%2c9_11%2c1_96%2c8_10%2c8_17%2c8_24%2c8_26%2c8_9%2c8_12%2c8_13%2c8_18%2c17_26%2c8_21%2c8_23%2c8_22%2c8_16%2c8_19%2c8_20%2c8_14%2c8_15%2c8_25%2c8_11%2c6_12%2c17_30%2c16_16%2c16_18%2c16_14%2c16_11%2c16_19%2c16_6%2c16_15%2c16_8%2c16_12%2c16_13%2c16_7%2c17_27%2c16_17&DXCss=http%3a%2f%2ffonts.googleapis.com%2fcss%3ffamily%3dOpen%2bSans%3a300%2c400%2c600%2c700%2c800%257CShadows%2bInto%2bLight%2c%2fContent%2fbootstrap.min.css%2c%2fContent%2fMainSite.css%2c%2fContent%2fSiteAdjustments.css%2c%2fContent%2ffont-awesome.min.css%2c1_12%2c1_14%2c0_863%2c0_859%2c1_10%2c0_695%2c1_5%2c0_697%2c0_703%2c0_706%2c0_823%2c0_813%2c0_861%2c0_709%2c4_2%2c0_711%2c5_1%2c0_794%2c0_776%2c0_778%2c7_1%2c7_0%2c1_1%2c0_671%2c9_18%2c9_19%2c9_21%2c9_20%2c0_888%2c9_17%2c0_890%2c0_790%2c8_2%2c0_792%2c8_0%2c0_810%2c6_2%2c0_812%2c0_796%2c16_2%2c0_798
You binding your controls to properties of AttestModel so just post back the model
public ActionResult Attest(AttestModel model)
and the AccountSimpleDetails property will be correctly bound to the collection. However looking at your edited post request, it appears that the indexers are starting at 1, not 0 (i.e. the first one is AccountSimpleDetails[1].AccountSimpleDetailId=165.... Do you have any javascript that deletes items in the collection? If so it will not bind the collection because indexers are zero based (and must be consecutive) so you will need to add an additional control for the index property.
I have this:
#for (int x = 0; x < Model.AccountSimpleDetails.Count(); x++)
{
<span>#x</span>
}
And it returns:
1,2, 3
That is the reason for the parsing issues as Stephen pointed out. So that's the real issue.
Thanks

Is my view model too "complex" to allow binding on post?

Using my model displaying a page works fine but the post does not return the bound model.
My classes:
public class ContactManager
{
public Contact Contact { get; set; }
public SelectList SalutationList { get; set; }
}
public class Contact
{
public int Id{get;set;}
public string FirstName{get; set;}
public SalutationType SalutationType{get; set;}
}
public class SalutationType
{
public int Id { get; set; }
public string Name { get; set; }
}
My View:
#model ViewModels.ContactManager
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Contact.Id)
#Html.DropDownListFor(model => model.Contact.SalutationType.Id, Model.SalutationList)
#Html.EditorFor(model => model.Contact.FirstName)
<input type="submit" value="Save" />
}
The issue seems to be in the DropDownListFor. The dropdown list displays correctly with the proper value but when I post this page the complete model is blank. If I simplify the DropDownListFor like this the values are posted as expected.
#html.DroDownListFor(model=>model.MyPlaceHolderProp, Model.SalutationList)
Is my model too complex? Am I not doing something correctly?
The models are based off of several tables using EF that I have created in a separate project. I am trying to avoid creating more classes/models then I have to.
You should post your controller action as well, as your model coming back as blank really has nothing to do with this. Changing the DropDownListFor definition one way or another should not effect the posting of any other values.
That said, you will run into another issue eventually here, so you need to regroup, anyways. You can't just post back the id value of a related item. Entity Framework will either complain that there's already an object with that id, or worse, if the object attaches, it will update the row with that id with the new posted value for Name, which in this case, is nothing, so it'll just clear it out.
When you create a relationship with a single item (a foreign key basically), if you don't specify a property to hold that foreign key value, Entity Framework creates one for you behind the scenes to track the relationship. In your case here, that means your Contacts table has a column named SalutationType_Id. However, there's no way from your class to directly access this value. This is why I recommend that you always provide an explicit property to handle the relationship:
[ForeignKey("SalutationType")]
public int SalutationTypeId { get; set; }
public SalutationType SalutationType { get; set; }
If you do that, then you can directly stuff the posted id there and Entity Framework will create the relationship.
#Html.DropDownListFor(m => m.Contact.SalutationTypeId, Model.SalutationList);
If you insist on keeping the key implicit, then you must create the relationship yourself, by creating a field on your view model to hold the posted value, then using that value to look up the SalutationType instance from the database, and then finally adding that to the Contact instance.
Add to your view model
public int SalutationTypeId { get; set; }
In your view
#Html.DropDownListFor(m => m.SalutationTypeId, Model.SalutationList)
In your POST action
var salutationType = db.SalutationTypes.Find(model.SalutationTypeId);
contact.SalutationType = salutationType;
You could do it this way. This may be the more "MVC best practice" way to handle it. Everything stays neatly in their models, and no manual IDs are required. The views are intended to be representations of the underlying models they are built on. If you are creating a view that has a form, then create a model that represents the form and use it in the view.
Revise your models like:
public class PostModel
{
public int ContactID { get; set; }
public int SalutationID { get; set; }
public string FirstName { get; set; }
}
public class PostView
{
public ContactManager contact { get; set; }
public PostModel post { get; set; }
}
Then create the PostView in the controller:
public ActionResult Index()
{
//create the PostView model
var pv = new PostView();
pv.ContactManager = contactManager;
pv.post = new PostView()
{
ContactID = contactManager.Contact.Id,
SalutationID = contactManager.SalutationType.Id,
FirstName = contactManager.Contact.FirstName
};
return View(pv);
}
Then the view could be like:
#model ViewModels.PostView
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.post.ContactID)
#Html.DropDownListFor(model => model.post.SalutationID, model.contact.SalutationList)
#Html.EditorFor(model => model.post.FirstName)
<input type="submit" value="Save" />
}
Then the post action in the controller:
[HttpPost]
public ActionResult Index(PostView pv)
{
//post code
//the posted data will be in pv.post
}
Have you considered using a custom model binder? Custom model binding isn't all that complicated for models that are still relatively simple, and you can handle the serialization/deserialization however you need to.
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://ivonna.biz/blog/2012/2/2/custom-aspnet-model-binders-series,-part-3-subclassing-your-models.aspx
http://forums.asp.net/t/1944696.aspx?what+is+custom+model+binding+in+mvc
I am not sure this will help you... I wsa having a similar issue but I was using ajax to post back... anyway, I had forgotten to mark my binding class with the [Serializable] attribute.
so you might try
[Serializable]
public class Contract {
...
}
Again, I am using Json to post back to my controller so may not be related or help you. But, I guess could be worth a try.

ViewModel loses data on post

I'm trying to create a complex ViewModel that has both an Index Page and Create Page of Company Notes all within the Details Page of a Company, and would like some guidance as to whether I'm doing this properly.
My problem at the moment is that when I create a new Company Note, it doesn't have any information in the object beyond the EditorFor fields I include in my cshtml - it loses all the data in the ViewModel.
I have a Company model and CompanyController, and in my Details action, I populate all the notes that are relevant to the company, and a form to allow users to add a new note.
My Company and CompanyNotes model are very simple:
public class Company
{
public int CompanyID { get; set; }
// bunch of fields related to the company
public virtual ICollection<CompanyNote> CompanyNotes { get; set; }
}
public class CompanyNote
{
public int CompanyNoteID { get; set; }
public DateTime Date { get; set; }
public string Note { get; set; }
public Company Company { get; set; }
}
I have a ViewModel that looks like this:
public class CompanyViewModel
{
public Company Company { get; set; }
// List of all notes associated with this company
public IEnumerable<CompanyNote> CompanyNotes { get; set; }
// A CompanyNote object to allow me to create a new note:
public CompanyNote CompanyNote { get; set; }
}
This is my Details action, which populates the company record, gets a list of related notes, and displays a create form with a new, empty object:
public ActionResult Details(int id = 0)
{
var viewModel = new CompanyViewModel();
viewModel.Company = db.Companies.Find(id);
if (viewModel.Company == null)
{
return HttpNotFound();
}
viewModel.CompanyNotes = (from a in db.CompanyNotes
where a.Company.CompanyID.Equals(id)
select a).OrderBy(x => x.Date);
viewModel.CompanyNote = new CompanyNote
{
Date = System.DateTime.Now,
Company = viewModel.Company
};
return View(viewModel);
}
This is my CreateNote action in my CompanyController. (Should I split this out into a separate partial view? What would be the benefit?)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateNote(CompanyViewModel companyViewModel)
{
CompanyNote companyNote = companyViewModel.CompanyNote;
if (ModelState.IsValid)
{
db.CompanyNotes.Add(companyNote);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(companyViewModel);
}
Finally, here's a simplified version of detail.cshtml:
#model Project.ViewModels.CompanyViewModel
// My company detail display is here, removed for sake of berevity
#using (Html.BeginForm("CreateNote", "Company"))
{
#Html.EditorFor(model => model.CompanyNote.Date)
#Html.TextAreaFor(model => model.CompanyNote.Note})
<input type="submit" value="Create" />
}
When I post, my CreateNote action has a companyViewModel that is basically empty, with the exception of companyViewModel.CompanyNote.Date and companyViewModel.CompanyNote.Note, which are the fields in my form - all the other data in the ViewModel is null, so I'm not sure how to even include a reference back to the parent company.
Am I even on the right path here?
Thanks,
Robbie
When I post, my CreateNote action has a companyViewModel that is
basically empty, with the exception of
companyViewModel.CompanyNote.Date and
companyViewModel.CompanyNote.Note, which are the fields in my form -
all the other data in the ViewModel is null, so I'm not sure how to
even include a reference back to the parent company.
That's perfectly normal behavior. Only information that is included in your form as input fields is sent to the server when you submit the form and this is the only information you could ever hope the model binder be able to retrieve.
If you need the CompanyNotes collection in your HttpPost action simply query your backend, the same way you did in your GET action. You could do this by passing the company ID as a hidden field:
#Html.HiddenFor(model => model.Company.CompanyID)
So the idea is to only include as input fields in your form information that the user is supposed to somehow modify. For all the other information, well, you've already have it in your backend so all you have to do is hit it to get it.
Contrary to classic WebForms, there's no longer any notion of ViewState in ASP.NET MVC. It is much closer to the stateless nature of the HTTP protocol.

Categories