C# MVC ViewModel Returns Null - Postback - c#

I have a number of awards in my view and within each award there is a corresponding list of qualifications. I have created a ViewModel to display each award and with a click of a button a modal should appear with its relevant qualifications which can be marked as completed/updated by the user. However on the Post of the data it is not binding to my ViewModel in my controller method. The data is appearing in my view as expected with each Award only showing its relevant qualifications. I have used FormCollection to access some of the fields for testing purposes and the data is being posted back. Any help would be great!
ViewModel
public class CandidateExtended
{
public CandidateExtended()
{
this.Qualifications = new List<Qualification_Extended>();
}
public int AwardID { get; set; }
public int FrameworkID { get; set; }
public string ULN { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
public string TitleShort { get; set; }
public string TitleFull { get; set; }
public DateTime DOB { get; set; }
public string Award { get; set; }
public int AwardLevel { get; set; }
public string Status { get; set; }
public string Completion { get; set; }
public string SelectedRoute { get; set; }
public List<Qualification_Extended> Qualifications { get; set; }
public void addQualification(Qualification_Extended qualification)
{
Qualifications.Add(qualification);
}
}
Controller
[HttpGet]
public ActionResult Index()
{
var awardDetails = (from award in db.award
join candidate in db.candidate
on award.ULN equals candidate.ULN
join framework in db.framework
on award.QAN equals framework.QAN
where award.OrganisationIdentityID == organisationID
select new AwardDetails_Extended
{
AwardID = award.AwardID,
ULN = award.ULN,
AwardStatus = award.AwardStatus,
Forename = candidate.Forename,
Surname = candidate.Surname,
DOB = candidate.DOB,
FrameworkID = framework.FrameworkID,
TitleFull = framework.TitleFull,
TitleShort = framework.TitleShort,
AwardLevel = framework.AwardLevel,
Award = framework.Award,
Completion = framework.Completion
}).ToList();
var qualificationDetails = (from candidateQualification in db.candidateQualification
join qualification in db.qualification
on candidateQualification.QualificationID equals qualification.QualificationID
select new Qualification_Extended
{
ID = candidateQualification.ID,
QualificationID = candidateQualification.QualificationID,
ULN = candidateQualification.ULN,
FrameworkID = candidateQualification.FrameworkID,
Achieved = candidateQualification.Achieved,
DateAchieved = candidateQualification.DateAchieved
}).ToList();
List<CandidateExtended> candidateVM = new List<CandidateExtended>();
foreach (var item in awardDetails)
{
CandidateExtended vm = new CandidateExtended();
vm.AwardID = item.AwardID;
vm.FrameworkID = item.FrameworkID;
vm.ULN = item.ULN;
vm.Forename = item.Forename;
vm.Surname = item.Surname;
vm.DOB = item.DOB;
vm.TitleShort = item.TitleShort;
vm.TitleFull = item.TitleFull;
vm.Award = item.Award;
vm.AwardLevel = item.AwardLevel;
vm.Status = item.AwardStatus;
vm.Completion = item.Completion;
vm.SelectedRoute = item.SelectedRoute;
foreach (var qualification in qualificationDetails)
{
if (qualification.ULN == item.ULN && qualification.FrameworkID == item.FrameworkID)
{
vm.addQualification(qualification);
}
}
candidateVM.Add(vm);
}
return View(candidateVM);
}
View
#using (Html.BeginForm("UpdateAward", "Organisation", FormMethod.Post))
{
#Html.HiddenFor(a => award.AwardID)
<div class="row">
<div class="col-md-12">
<div class="row org-row-main">
<div class="col-md-7"><h4 class="org-type">Qualification</h4></div>
<div class="col-md-2"><h5 class="org-completed">Completed</h5></div>
<div class="col-md-3"><h5 class="org-date">Date</h5></div>
</div>
<hr class="org-hr"/>
#for (int i = 0; i < award.Qualifications.Count(); i++)
{
var qualification = award.Qualifications[i];
<div class="row org-row">
<div class="col-md-7">
#Html.HiddenFor(a => award.Qualifications[i].ID)
</div>
<div class="col-md-2">
#Html.CheckBoxFor(a => award.Qualifications[i].Achieved)
</div>
<div class="col-md-3">#Html.TextBoxFor(a => award.Qualifications[i].DateAchieved, "{0:dd/MM/yyyy}")
</div>
</div>
}
</div>
</div>
<button type="submit" class="btn admin-button" style="margin-top: 0;">Save</button>
}
UpdateAward
[HttpPost]
public ActionResult UpdateAward(CandidateExtended model, FormCollection collection)
{
return RedirectToAction("Index", "Login");
}

First (and you may already have this, but we can't see it): your View should start with a line containing #model List<CandidateExtended> (prefix the inner Type with the proper namespace).
Then in the View you should use Model, which is by definition of the exact type specified after the #model keyword.
We see that you are using award, we can't see where it comes from, presumably it is set using something like var award = Model[j] or foreach (var award in Model).
Never use such temporary or helper variables (for efficiency) in a View to render a Form; the View needs the fully qualified name of all objects, e.g. Model.Item[x].SubItem[y] in order to generate Form field names that can be used for Model Binding.
E.g. this : #Html.HiddenFor(a => award.Qualifications[i].ID)
should be: #Html.HiddenFor(a => Model[j].Qualifications[i].ID)
And make the same change in all other places.
Then do as was already suggested, use the List<...> in your Controller Post method.
Finally also please remove the FormCollection, it is not needed if you have everything set up as described here. Decent MVC code never uses FormCollection, ever.

Try calling the posted method on a separate button instead of BeginForm and it should work.

Related

Set razor view variable according to checkbox checked

I am working on an online library using ASP.NET MVC.
This is my view model for the library management page:
public class ManageViewModel
{
public IPagedList<ManageBookViewModel> WholeInventory;
public IPagedList<ManageBookViewModel> CurrentInventory;
public bool OldInventoryIsShown { get; set; } = false;
}
In the corresponding view I have a checkbox for whether or not to show the old inventory and a local variable modelList, which I would like to set to Model.WholeInventory if the checkbox is checked and to Model.CurrentInventory otherwise. I use modelList to display a table with all the books and I would need its value to be reset every time I (un)check the checkbox in order for the list to be properly displayed.
Is this possible? How would I go about doing this?
In my view I currently have:
<label class="switch">
<input id="OldInventoryIsShown" name="OldInventoryIsShown" type="checkbox" />
<span class="slider round"></span>
</label>
#{
var modelList = Model.OldInventoryIsShown ? Model.WholeInventory : Model.CurrentInventory;
}
#using (Html.BeginForm())
{
<table id="bookInventory" class="table table-hover">
<thead>
<tr>
<th>Author</th>
<th>Title</th>
....
</tr>
</thead>
#foreach (var entry in modelList)
{
<tr>
<td>#Html.DisplayFor(modelItem => entry.Author)</td>
<td>#Html.DisplayFor(modelItem => entry.Title)</td>
....
</tr>
}
</table>
<p>Page #(modelList.PageCount < modelList.PageNumber ? 0 : modelList.PageNumber) of #modelList.PageCount</p>
#Html.PagedListPager(modelList, page => Url.Action("Manage", page }))
}
The controller action:
public ActionResult Manage(int? page)
{
var wholeInventory = _bookService.GetBooksIncludingDisabled().Select(b => Mapper.Map<Book, ManageBookViewModel>(b));
var currentInventory = _bookService.GetBooks().Select(b => Mapper.Map<Book, ManageBookViewModel>(b));
int pageSize = 3;
int pageNumber = page ?? 1;
var model = new ManageViewModel
{
WholeInventory = wholeInventory.ToPagedList(pageNumber, pageSize),
CurrentInventory = currentInventory.ToPagedList(pageNumber, pageSize)
};
return View(model);
}
Models:
Book.cs
public class Book
{
public int BookId { get; set; }
[Required]
[MinLength(1)]
public string Title { get; set; }
[Required]
[MinLength(1)]
public string Author { get; set; }
....
public bool IsDisabled { get; set; } = false;
public virtual ICollection<UserBook> UserBooks { get; set; }
}
ManageBookViewModel.cs
public class ManageBookViewModel
{
public int BookId { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Enter the book title")]
public string Title { get; set; }
[Required(AllowEmptyStrings = false, ErrorMessage = "Enter the book author.")]
public string Author { get; set; }
....
public bool IsDisabled { get; set; }
}
Your ManageViewModel needs to include only one property for the paged list and it should be IPagedList<Book> (see explanation below)
public class ManageViewModel
{
public IPagedList<Book> Inventory;
[Display(Name = "Include old inventory")]
public bool OldInventoryIsShown { get; set; }
... // any other search/filter properties
}
and your view needs to include the checkbox inside the <form> element, and the form should be making a GET to your controller method. Then you also need to include the current value of OldInventoryIsShown as a route value in the #Html.PagedListPager() method so that the current filter is retained when paging.
#model ManageViewModel
...
#using (Html.BeginForm("Manage", "ControllerName", FormMethod.Get))
{
#Html.CheckBoxFor(m => m.OldInventoryIsShown)
#Html.LabelFor(m => m.OldInventoryIsShown)
... // any other search/filter properties
<input type="submit" value="search" />
}
<table id="bookInventory" class="table table-hover">
....
</table>
<p>Page #(modelList.PageCount < modelList.PageNumber ? 0 : modelList.PageNumber) of #modelList.PageCount</p>
#Html.PagedListPager(modelList, page =>
Url.Action("Manage", new { page = page, oldInventoryIsShown = Model.OldInventoryIsShown })) // plus any other search/filter properties
Finally in the controller method you need a parameter for the value of the bool property an modify your query based on that value.
public ActionResult Manage(int? page, bool oldInventoryIsShown)
{
int pageSize = 3;
int pageNumber = page ?? 1;
IQueryable<Book> inventory = db.Books;
if (!oldInventoryIsShown)
{
inventory = inventory.Where(x => !x.IsDisabled);
}
ManageViewModel model = new ManageViewModel
{
Inventory = inventory.ToPagedList(pageNumber, pageSize),
OldInventoryIsShown = oldInventoryIsShown
};
return View(model);
}
You current controller code is terribly inefficient. Lets assume your table has 10,000 Book records, and 5,000 of those are 'disabled' (archived). You current code first gets all 10,0000 records and adds them to memory. Then you map all then to a view model. Then you call another query to get another 5,0000 records (which are just duplicates of what you already have), which you add to memory and map to a view model. But all you want in the view is 3 records (the value of pageSize) so you have done thousands of times of extra unnecessary processing.
In your case, there is no need for a view model (although if you did need one, you would use the StaticPagedList methods - refer this answer for an example). Your query should be using your db context to generate an IQueryable<Book> so that only the results you need are returned from the database (internally the ToPagedList() method uses .Skip() and .Take() on IQueryable<T>)

MVC model binding to list - only works on first item in list

I have a page that contains multiple forms to edit questions for a single quiz, each question has its own list of answers. So for each question inside this quiz there is a form for which a user can edit the question (and answers), See below:
#model OLTINT.Areas.admin.ViewModels.OldQuizQAViewModel
<h1>Edit #Model.QuizTitle quiz</h1>
<hr />
<p class="breadcrumb">
#Html.ActionLink(HttpUtility.HtmlDecode("◄") + " Back to List", "Quizzes", new { id = Model.CourseID }, new { #class = "" })
</p>
#for (int j = 0; j < Model.OldQuizQuestions.Count(); j++)
{
using (Ajax.BeginForm("EditQuiz", "Course", null, new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "button"
}))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.QuizID)
#Html.HiddenFor(model => model.OldQuizQuestions[j].QuizQuestionID)
<p class="form_title">Question number #Model.OldQuizQuestions[j].Order</p>
<div class="resize_input">#Html.EditorFor(model => model.OldQuizQuestions[j].Question)</div>
<p class="form_title">#Html.LabelFor(model => model.OldQuizQuestions[j].Type)</p>
<div class="resize_input">#Html.DropDownListFor(model => model.OldQuizQuestions[j].Type, ViewBag.types, "Please choose...", new { #class = "chosen-select" })</div>
<p class="form_title">Choose correct answers</p>
Char x = 'a';
for (int i = 0; i < Model.OldQuizQuestions[j].OldQuizAnswers.Count(); i++)
{
x++;
if (i == 0)
{
x = 'a';
}
<div style="display:table; width:100%;">
<div class="divTableCell" style="padding:0 10px 10px 0; vertical-align:middle; min-width:6%;">
#Html.CheckBoxFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Correct, new { style = "" })
#Html.LabelFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Correct, "["+ x +"]")
</div>
<div class="divTableCell quiz_input">
#Html.HiddenFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].QuizAnsID)
#Html.EditorFor(model => model.OldQuizQuestions[j].OldQuizAnswers[i].Answer)
</div>
</div>
}
<div class="button_container">
<p id="button"></p>
#Html.ActionLink("Delete this question", "DeleteQuestion", new { id = Model.OldQuizQuestions[j].QuizQuestionID }, new { #class = "button button_red button_not_full_width" })
<input type="submit" value="Save" class="button button_orange button_not_full_width" />
</div>
<hr />
}
}
OldQuizQAViewModel:
public class OldQuizQAViewModel
{
public int CourseID { get; set; }
public int? QuizID { get; set; }
public string QuizTitle { get; set; }
public IList<OldQuizQuestions> OldQuizQuestions { get; set; }
}
OldQuizQuestions:
public class OldQuizQuestions
{
[Key]
public int QuizQuestionID { get; set; }
public int OldQuizID { get; set; }
[Required]
public string Question { get; set; }
[Required]
public int Order { get; set; }
[Required]
public int Type { get; set; }
public virtual IList<OldQuizAnswers> OldQuizAnswers { get; set; }
public virtual OldQuiz OldQuiz { get; set; }
}
OldQuizAnswers:
public class OldQuizAnswers
{
[Key]
public int QuizAnsID { get; set; }
public int QuizQuestionID { get; set; }
public string Answer { get; set; }
public int Order { get; set; }
public bool Correct { get; set; }
public bool Chosen { get; set; }
public virtual OldQuizQuestions OldQuizQuestions { get; set; }
}
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EditQuiz(OldQuizQAViewModel model)
{
var questiondata = model.OldQuizQuestions.Single();
if (ModelState.IsValid)
{
OldQuizQuestions updatequestion = db.OldQuizQuestions
.SingleOrDefault(x => x.QuizQuestionID == questiondata.QuizQuestionID);
updatequestion.Question = questiondata.Question;
updatequestion.Type = questiondata.Type;
db.Entry(updatequestion).State = EntityState.Modified;
db.SaveChanges();
foreach (var answer in questiondata.OldQuizAnswers)
{
var updateanswer = updatequestion.OldQuizAnswers
.First(x => x.QuizAnsID == answer.QuizAnsID);
updateanswer.Answer = answer.Answer;
updateanswer.Correct = answer.Correct;
db.Entry(updateanswer).State = EntityState.Modified;
db.SaveChanges();
}
return Content("<span style='font-weight:300; font-size:1.2em; color: green; '>Saved!</span>");
}
return Content("<span class='errortext'>Please correct the marked fields!</span>");
}
Now this works fine if I want to edit the first question but when I edit anything else my controller just says null but when I check the data that's being posted everything is there (for example when i try to edit question 2):
I've had a look around on here at the many queries about model binding to a list but none have helped. Can anyone see where i'm going wrong with this?
The issue you are facing is caused by a misunderstanding of how asp.net model binding works in relation to lists. For example looking at the view model used in your controller action EditQuiz.
public class OldQuizQAViewModel
{
public int CourseID { get; set; }
public int? QuizID { get; set; }
public string QuizTitle { get; set; }
public IList<OldQuizQuestions> OldQuizQuestions { get; set; }
}
In order for the model binding to work with a IList or any other collection, asp-net expects the form data you post to have sequential indexes. The form data you are sending over POST already has a working example of model binding with collections implemented. Looking at the form data:
The highlighted properties show how to correctly bind to a collection, in that you set the values for the property Correct in the OldQuizAnswers model for each index of IList in OldQuizQAViewModel and pass these all at once in a single request.
Whereas in the same request you only pass the data for OldQuizQuestions of specific index you wish these values to be bound to in the IList collection.
This is why the fist time you post, the model binding works successfully as you are referencing the first index ([0]), whereas on the second POST you reference the second index (1) but not the first, causing the model binding to fail.
See herefor more information on how model binding works.
Have you tried adding a #Html.HiddenFor(model => model.OldQuizQuestions[j]) ?
I read this "It creates a hidden input on the form for the field (from your model) that you pass it.
It is useful for fields in your Model/ViewModel that you need to persist on the page and have passed back when another call is made but shouldn't be seen by the user."
Answer comes from https://stackoverflow.com/a/3866720/8404545
Might be the problem here

Orchard - save list of elements on form submit

Basically i have a editor view where i have a few fields and a list, located in admin -> teacher -> edit
File.cshtml
<fieldset>
<legend>#T("Details")</legend>
#Html.LabelFor(m => m.FirstName, T("First name"))
#Html.TextBoxFor(m => m.FirstName)
#Html.LabelFor(m => m.LastName, T("Last name"))
#Html.TextBoxFor(m => m.LastName)
#Html.LabelFor(m => m.Gender, T("Gender"))
#Html.DropDownListFor(m => m.Gender, genderSelectListItems, "Select gender...")
</fieldset>
<fieldset>
#{
var i = 0;
#Html.LabelFor(m => m.Availability)
#Html.DropDownListFor(m => m.Availability, daysSelectList, "Select day to add...")
<input type="button" id="addDay" value="Add" />
<ul id="daysList">
#foreach (var day in Model.Availability)
{
<li>
#Html.TextBoxFor(m => m.Availability[i].Interval)
</li>
i++;
}
</ul>
}
</fieldset>
Whenever i submit the data, only the fields get saved. The list is never saved, even though fiddler shows that the data is sent back to the server
My models are:
TeacherPartRecord
public class TeacherPartRecord : ContentPartRecord
{
public TeacherPartRecord()
{
TeacherData = new List<TeacherDataRecord>();
}
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual Gender Gender { get; set; }
public virtual IList<TeacherDataRecord> TeacherData { get; set; }
}
TeacherPart - this file is almost the same as TeacherPartRecord so i truncated it, showing only the difference
public class TeacherPart : ContentPart<TeacherPartRecord>
{
................
public IList<AvailabilityRecord> Availability
{
get { return Record.TeacherData.Select(r => r.AvailabilityRecord).ToList(); }
}
}
TeacherDataRecord
public class TeacherDataRecord
{
public virtual int Id { get; set; }
public virtual TeacherPartRecord TeacherPartRecord { get; set; }
public virtual AvailabilityRecord AvailabilityRecord { get; set; }
}
AvailabilityRecord
public class AvailabilityRecord
{
public virtual int Id { get; set; }
public virtual Day Day { get; set; }
public virtual string Interval { get; set; }
}
and TeacherPartDriver
public class TeacherPartDriver : ContentPartDriver<TeacherPart>
{
//GET
protected override DriverResult Editor(TeacherPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Teacher_Edit", () =>
shapeHelper.EditorTemplate(TemplateName: "Parts/Teacher", Model: BuildEditorViewModel(part), Prefix: Prefix));
}
//POST
protected override DriverResult Editor(TeacherPart part, IUpdateModel updater, dynamic shapeHelper)
{
var viewModel = new TeacherEditViewModel();
updater.TryUpdateModel(viewModel, Prefix, null, null);
_teacherService.UpdateTeacher(viewModel, part);
return Editor(part, shapeHelper);
}
}
BuildEditorViewModel only maps properties from TeacherPart to TeacherEditViewModel, which is identical with TeacherPart
The POST Editor method never receives the data from Availability (part.Availability is the same as previously, new data is not received)
I inserted a few entries in the database manually because i can't add a list of type Availability from the edit page, even though the other content (first name, last name, gender) is created
I would like to know how/when does the binding occur, or maybe some suggestions on how i could solve this issue.
I should mention that my c#/MVC experience is 6 months and my orchard experience is about 2 weeks so i'm still learning.
All i hope is that i stated my problem clearly and there is someone that can help.
Thank you for your time
EDIT
As i can see, the values are bound
<input id="Teacher_Availability_0__Interval" name="Teacher.Availability[0].Interval" type="text" value="14-20">
<input id="Teacher_Availability_1__Interval" name="Teacher.Availability[1].Interval" type="text" value="12-18">
and fiddler shows the data is getting sent
Fiddler
so i guess that something wrong happens right after the data is sent and before being received by the server or maybe my models are wrong

Failed DropDownListFor() object reference in Editor Template using ViewModel

I've been all over SO and the interwebs looking for a resolution to this, and while I've found several posts that get me close, they all end up failing with an "Object reference not set to an instance of an object" error.
I am attempting to create a variant of the "Nested Collection Models in MVC to Add Multiple Phone Numbers" from Itorian.com, but mine involves a little more complexity.
Basically, I've got an MVC 5 EF 6 app which is set up to allow an unlimited number of phone numbers to be associated with any given contact. I'm using a links table to associate Contact ID with Phone ID. The intent is to allow multiple contacts (i.e., family members) to share a single phone number while also allowing one contact to have multiple phone numbers. Phonetype (home, work, cell, etc.) is set from a third table, allowing for dropdown selection. The phonelink table has columns for phonetype_id, phone_id, and contact_id.
I should also probably note that Client extends my base Contact. Phone numbers are associated to Contact, but I'm actually creating a Client to generate the extended fields at the same time. This works perfectly if I don't enter Phones into the mix.
My problem is twofold:
First:
I need to figure out what is causing the Object Reference Not Set error on my DropDownListFor
Second:
Figuring out how to correctly add the new fields to the view when the Add Phone Number link is clicked. More details on this below.
My model classes, generated from DB by EF:
Phone.cs
public partial class phone
{
public phone()
{
this.phonelinks = new HashSet<phonelink>();
}
public int id { get; set; }
public string phone_number { get; set; }
public string phone_extension { get; set; }
public virtual ICollection<phonelink> phonelinks { get; set; }
}
Phonetype.cs
public partial class phonetype
{
public phonetype()
{
this.phonelinks = new HashSet<phonelink>();
}
public int id { get; set; }
public string phone_type { get; set; }
public virtual ICollection<phonelink> phonelinks { get; set; }
}
Phonelink.cs
public partial class phonelink
{
public int id { get; set; }
public int contact_id { get; set; }
public int phone_id { get; set; }
public int phonetype_id { get; set; }
public virtual contact contact { get; set; }
public virtual phone phone { get; set; }
public virtual phonetype phonetype { get; set; }
}
I have also extended phonelink to include a Remove From View helper property (which works as expected via HTML Helpers and JS):
public partial class phonelink
{
public bool remove_from_view { get; set; }
}
My Controller for a new Contact (just the GET, as my error occurs before attempting POST)
ClientsController.cs
// GET: Clients/Create
public ActionResult Create()
{
var viewModel = new ClientCreateViewModel();
ConfigureCreateViewModel(viewModel);
return View(viewModel);
}
private void ConfigureCreateViewModel(ClientCreateViewModel model)
{
model.AllEthnicities = new SelectList(db.ethnicities, "id", "ethnicity1");
model.AllGenders = new SelectList(db.genders, "id", "gender1");
model.AllPrefixes = new SelectList(db.prefixes, "id", "prefix1");
model.AllSuffixes = new SelectList(db.suffixes, "id", "suffix1");
model.PhoneLinkVM = new PhoneLinkViewModel()
{
AllPhoneTypes = new SelectList(db.phonetypes, "id", "phone_type")
};
}
My ViewModel (simplified)
ClientViewModel.cs
public class ClientCreateViewModel
{
public client Clients { get; set; }
public PhoneLinkViewModel PhoneLinkVM { get; set; }
// There are other declarations here for Ethnicities, Gender, etc.
// They all work for displaying dropdowns, so have been removed for simplicity
}
public class PhoneLinkViewModel
{
public IEnumerable<phonelink> Phonelinks { get; set; }
public IEnumerable<SelectListItem> AllPhoneTypes { get; set; }
public int SelectedPhoneType { get; set; }
public phonelink phonelink { get; set; }
}
And in my View below I call phonelink from an editor template.
Create.cshtml
#* Div for Phone Numbers *#
<div id="phoneNumbers">
#Html.EditorFor(model => model.PhoneLinkVM.phonelink)
</div>
<div class="row top-space">
<div class="form-group col-md-4 col-xs-12">
#Html.AddLink("Add Phone Number", "#phoneNumbers", ".phoneNumber", "Phonelinks", typeof(CreateClientViewModel))
</div>
</div>
Phonelink.cshtml
#model TheraLogic.Models.ClientCreateViewModel
#using TheraLogic.Models
<div class="phoneNumber">
<div class="row top-space">
#* Phone Number Inputs *#
<div class="form-group col-md-2">
<label class="control-label">Phone Number</label>
#Html.EditorFor(model => model.PhoneLinkVM.phonelink.phone.phone_number, new { htmlAttributes = new { #class = "form-control" } })
#Html.HiddenFor(model => model.PhoneLinkVM.phonelink.remove_from_view, new { #class = "mark-for-removal" })
#Html.RemoveLink("Remove", "div.phoneNumber", "input.mark-for-removal")
</div>
<div class="form-group col-md-1">
<label class="control-label">Extension</label>
#Html.EditorFor(model => model.PhoneLinkVM.phonelink.phone.phone_extension, new { htmlAttributes = new { #class = "form-control" } })
</div>
<div class="form-group col-md-1">
<label class="control-label">Type</label>
#Html.DropDownListFor(model => model.PhoneLinkVM.SelectedPhoneType, Model.PhoneLinkVM.AllPhoneTypes, htmlAttributes: new { #class = "form-control" })
</div>
</div>
</div>
If I comment out the #Html.DropDownListFor in the bottom of the editor template, I can view the Create view without error. But with that in place, it produces the Object Reference Not Set error.
The only way I've been able to make it work at all is to add:
ViewBag.ThesePhoneTypes = model.PhoneLinkVM.AllPhoneTypes;
to the bottom of ConfigureCreateViewModel, and pull it into the view within the editor template with
#Html.DropDownList("ThesePhoneTypes")
which is more of a workaround than a true solution.
Regarding adding/removing phone numbers when clicking the link, if I use type(phonelink) in my #Html.AddLink helper in Create.cshtml, I get an error stating:
The model item passed into the dictionary is of type
'phonelink', but this dictionary requires a model item of
type 'Models.ClientCreateViewModel'
But if I use type(ClientCreateViewModel then rather than getting the content in my Phonelink.cshtml editor template, I just get a set of inputs for the integers declared in my ClientCreateViewModel for SelectedGender, SelectedEthnicity, etc. (omitted in ViewModel code above since they work). I know I need to dig deeper into binding the nested PhoneLinkViewModel, but I'm not sure how to go about it.
HtmlHelpers.cs
(taken nearly verbatim from the Itorian.com link above)
public static class HtmlHelpers
{
public static IHtmlString RemoveLink(this HtmlHelper htmlHelper, string linkText, string container, string deleteElement)
{
var js = string.Format("javascript:removeNestedForm(this,'{0}','{1}'); return false;", container, deleteElement);
TagBuilder tb = new TagBuilder("a");
tb.Attributes.Add("href", "#");
tb.Attributes.Add("onclick", js);
tb.InnerHtml = linkText;
var tag = tb.ToString(TagRenderMode.Normal);
return MvcHtmlString.Create(tag);
}
public static IHtmlString AddLink<TModel>(this HtmlHelper<TModel> htmlHelper, string linkText, string container, string counter, string collectionProperty, Type nestedType)
{
var ticks = DateTime.UtcNow.Ticks;
var nestedObject = Activator.CreateInstance(nestedType);
var partial = htmlHelper.EditorFor(x => nestedObject).ToHtmlString().JsEncode();
partial = partial.Replace("id=\\\"nestedObject", "id=\\\"" + collectionProperty + "_" + ticks + "_");
partial = partial.Replace("name=\\\"nestedObject", "name=\\\"" + collectionProperty + "[" + ticks + "]");
var js = string.Format("javascript:addNestedForm('{0}','{1}','{2}','{3}');return false;", container, counter, ticks, partial);
TagBuilder tb = new TagBuilder("a");
tb.Attributes.Add("href", "#");
tb.Attributes.Add("onclick", js);
tb.InnerHtml = linkText;
var tag = tb.ToString(TagRenderMode.Normal);
return MvcHtmlString.Create(tag);
}
private static string JsEncode(this string s)
{
if (string.IsNullOrEmpty(s)) return "";
int i;
int len = s.Length;
StringBuilder sb = new StringBuilder(len + 4);
string t;
for (i = 0; i < len; i += 1)
{
char c = s[i];
switch (c)
{
case '>':
case '"':
case '\\':
sb.Append('\\');
sb.Append(c);
break;
case '\b':
sb.Append("\\b");
break;
case '\t':
sb.Append("\\t");
break;
case '\n':
break;
case '\f':
sb.Append("\\f");
break;
case '\r':
break;
default:
if (c < ' ')
{
string tmp = new string(c, 1);
t = "000" + int.Parse(tmp, System.Globalization.NumberStyles.HexNumber);
sb.Append("\\u" + t.Substring(t.Length - 4));
}
else
{
sb.Append(c);
}
break;
}
}
return sb.ToString();
}
}
I can add the Javascript also, if needed, but I think it's ok, as it does add DIVs with fields to the view, just not for type(phonelink) as I need it to.
You need to initialize PhoneLinkVM. This doesn't cause a problem with calls to other HtmlHelpers because the expression you pass as the first param is not actually evaluated. However, the select list parameter passed to DropDownListFor is a straight-up variable reference, and by default PhoneLinkVM is null.
To fix it you just need to add a constructor like the following to your view model:
public class ClientCreateViewModel
{
public ClientCreateViewModel()
{
PhoneLinkVM = new PhoneLinkViewModel();
}
...
}
Although it doesn't really answer the question of why the dropdownlistfor was failing from within the editor template, I have achieved a solution for my project.
I ditched the editor template altogether, and am now rendering the PhoneLink div as a partial instead. I just decided to use an alternative method for adding additional phone links, which negates the need for a phonelink editor template in my case.
When I changed #Html.EditorFor(model => model.PhoneLinkVM.phonelink) to #Html.Partial("_PhonelinkPartial", Model.PhoneLinkPartial) (along with moving/renaming a few related files) my view rendered as anticipated.
I will handle the "Add Phone Number" functionality with JQuery and modal popups instead.

textbox data writes back to database, but textboxfor will not

I'm picking up that it's a best practice to use the strongly-typed textboxfor helper method rather than simple textbox. I'm all for that. But as I made the switch in my project, the data stopped getting written back to the database. I'm having trouble figuring out why.
Here's my model.
public class MasterModel
{
[Key]
public int mandatoryKey { get; set; }
public List<tblAddress> Address { get; set; }
public List<tblPrimaryCaregiverdata> Primary { get; set; }
public List<tblPhone> Phone { get; set; }
public List<tblEmail> Email { get; set; }
public List<tblRelatedCaregiver> Related { get; set; }
public List<tblTrainingHistoryMain> TrainingHistory { get; set; }
public List<tblInquiryReferralStatu> InquiryReferral { get; set; }
}
Then the controller
public ActionResult Create(MasterModel masterModel)
{
if (ModelState.IsValid)
{
db.tblPrimaryCaregiverdatas.Add(masterModel.Primary[0]);
db.SaveChanges();
int newCareGiverID = db.tblPrimaryCaregiverdatas.OrderByDescending(p => p.CareGiverID)
.FirstOrDefault().CareGiverID;
foreach (var ph in masterModel.Phone)
{
ph.CareGiverID = newCareGiverID;
db.tblPhones.Add(ph);
}
db.SaveChanges();
return RedirectToAction("Index");
}
Now, in the view (which invokes several partial views, one for each table in the master model), when I created the fields like this:
SelectList phonetypes = ViewBag.PhoneType;
<div>
<label class="label-fixed-width">Phone:</label>
#Html.TextBox("masterModel.Phone[0].phone", null, new { style = "width: 600px" })
#Html.DropDownList("masterModel.Phone[0].PhoneType",phonetypes, null, new
{
#class = "form-control-inline dropdown",
style = "width: 100px"
})
...I was able to write data back into the database. But when I switched over to textboxfor like this:
SelectList phonetypes = ViewBag.PhoneType;
<div>
<label class="label-fixed-width">Phone:</label>
#Html.TextBoxFor(x => x.Phone[0].Phone, null, new { style = "width: 600px" })
#Html.DropDownListFor(x => x.Phone[0].PhoneType, phonetypes,"--------", new
{
#class = "form-control-inline dropdown",
style = "width: 100px"
})
The Phone section of the MasterModel is empty when I post back. Can someone help me understand what's going on here?
When you post a model back to an ActionResult and return the same View, the values for the model objects are contained in the ModelState. The ModelState is what contains information about valid/invalid fields as well as the actual POSTed values. If you want to update a model value, you can do this:
foreach (var ph in masterModel.Phone)
{
ModelState.Clear()//added here
ph.CareGiverID = newCareGiverID;
db.tblPhones.Add(ph);
}

Categories

Resources