For loop post to db works fine. Retrieving value from bool property saved in model works as well. However, when user goes to edit, the view is not showing saved values in the array. So the issue is binding the saved boolean values to the array.
#for (var i = 0; i < Model.Questions.Count; i++)
{
#Html.HiddenFor(m => m.Answers[i].QuestionID, new { id = Model.Questions[i].QuestionID })
#Html.HiddenFor(m => m.Answers[i].ApplicationID, new { id = Model.ApplicationID })
#Html.HiddenFor(m => m.Answers[i].QuestionText, new { id = Model.Questions[i].QuestionText })
#Html.HiddenFor(m => m.Answers[i].QuestionCategoryID, new { id = Model.Questions[i].QuestionCategoryID })
#Html.RadioButtonFor(model => model.Answers[i].ResponseBool, true) Yes
#Html.RadioButtonFor(model => model.Answers[i].ResponseBool, false) No
#Html.DisplayFor(model => model.Questions[i].QuestionText, new { #class = "col-md-2" })
}
I am accessing the questions and answer repos through my viewmodel:
Questions = new QuestionRepository().GetStepOneQuestions();
Answers = new ResponseRepository().GetStepOneResponses(stepOneSaved.ApplicationID)
.Select(k => new Response()
{
ApplicationID = stepOneSaved.ApplicationID,
QuestionID = k.QuestionID,
QuestionCategoryID = k.QuestionCategoryID,
QuestionText = k.QuestionText,
ResponseBool = k.ResponseBool
})
.ToList();
Controller:
public ActionResult StepOne(int id)
{
var application = _applicationData.GetByID(id);
var form = _eduInfoData.GetByEdID(id);
var vm = new StepOneViewModel(application, form);
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult StepOne( StepOneViewModel form)
{
try
{
var item = form.toEduInfo();
if (ModelState.IsValid && _eduInfoData.TrySave(item))
{
for (int i = 0; i < form.Answers.Count; i++)
{
_responseData.TrySave(form.ApplicationID, form.Answers[i]);
}
return RedirectToAction("StepTwo", new { id = item.ApplicationID });
}
}
catch (DataException /* dex */)
{
ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
}
return View(form);
}
Solved! problem was I not pulling back the responses correctly.To fix I passed in the appId and questionId into my response model,and set the values there.
still needs some re-factoring but here's the gist of it:
public Response(int id, int qID)
{
this.ResponseBool = new ResponseRepository().GetResponse(id).Where(m => m.QuestionID == qID).Select(m => m.ResponseBool).FirstOrDefault();
}
Answers = Questions
.Select(k => new Response(stepOneSaved.ApplicationID, k.QuestionID)
{
ApplicationID = stepOneSaved.ApplicationID,
QuestionID = k.QuestionID,
QuestionCategoryID = k.QuestionCategoryID,
QuestionText = k.QuestionText,
})
.ToList();
Related
I am trying to use an ActionLink to pass an instance of a ViewModel from my view to a controller. However, the ViewModel is always null when it reaches the controller.
ActionLink:
#Html.ActionLink("Begin Test", "TakeTest", new { vm = Model }, new { #class = "btn btn-default" })
Controller:
public ActionResult TakeTest(QuizViewModel vm) {
ViewBag.Title = vm.vmTest.TestName;
string userId = User.Identity.GetUserId();
vm.Student = db.Users.SingleOrDefault(s => s.Id == userId);
vm.Attempt = db.Attempts.SingleOrDefault(a => a.Student == vm.Student && a.Test == vm.vmTest);
if (vm.vmTest.AttemptsAllowed > 0 && vm.Attempt.Count >= vm.vmTest.AttemptsAllowed) {
return View();
}
vm.Attempt.Count++;
if (vm.QuestionList == null) {
vm.QuestionList = db.Questions.Where(q => q.TID == vm.vmTest.ID).ToList();
}
Random r = new Random();
int randQid = r.Next(0, vm.QuestionList.Count - 1);
vm.ShowQuestion = vm.QuestionList[randQid];
vm.QuestionList.Remove(vm.ShowQuestion);
vm.Asets = db.AnswerSets.Where(a => a.Question == vm.ShowQuestion).ToList();
return View(vm);
}
I currently have one view that is used to create and edit my model. For the edit action the fields on the page are automatically populated with the data from the model and can be edited and then saved. I'd like a similar action from my controller that populates the fields with the data for a sort of "View" page. I read that I can add the htmlAttribute #readonly="readonly" to my textboxes but is there a way that I can append this attribute to the textboxes (or something easier) only when the view is called from this action? I'm not sure what code is relevant here, more of a conceptual question, but here's some code just in case:
The view that I want to use for all three functions (create, edit, and view) is called "Create.cshtml" and is full of:
#Html.LabelFor(model => model.Project.SubmissionNumber)
#Html.EditorFor(model => model.Project.SubmissionNumber)
#Html.ValidationMessageFor(model => model.Project.SubmissionNumber, "", new { #class = "text-danger" })
and a few dropdown menus:
#Html.LabelFor(model => model.Project.Status)
#Html.DropDownListFor(model => model.Project.Status,
Model.Statuses,
"--")
#Html.ValidationMessageFor(model => model.Project.Status, "", new { #class = "text-danger" })
I won't include the whole view because it's very repetitive. Picture a bunch of textboxes and dropdown menus just like that.
My Create action:
public ActionResult Create()
{
var model = new ProjectClearanceApp.Models.ProjectViewModel();
var statuses = GetAllStatuses();
var divisions = GetAllDivisions();
var branches = GetAllBranches();
var project_types = GetAllProjectTypes();
var states = GetAllStates();
model.Statuses = GetSelectListItems(statuses);
model.Divisions = GetSelectListItems(divisions);
model.Branches = GetSelectListItems(branches);
model.ProjectTypes = GetSelectListItems(project_types);
model.States = GetSelectListItems(states);
return View(model);
}
// POST: Projects/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ProjectName,ProjectDescription,DateReceived,EffectiveDate,ExpirationDate,GeneralContractor,ProjectTerm,ProjectType,SubmissionNumber,PolicyNumber,Status,Underwriter,Division,BrokerCity,TAName,Branch,FirstNamedInsuredAddress,FirstNamedInsured,ProjectAddress")] Project project)
{
if (ModelState.IsValid)
{
if (project.ProjectID <= 0)
{
db.Projects.Add(project);
}
else
{
db.Entry(project).State = EntityState.Modified;
}
db.SaveChanges();
return RedirectToAction("Index");
}
var model = new ProjectViewModel();
model.Project = db.Projects.Find(project.ProjectID);
var states = GetAllStates();
var branches = GetAllBranches();
var divisions = GetAllDivisions();
var project_types = GetAllProjectTypes();
var statuses = GetAllStatuses();
model.States = GetSelectListItems(states);
model.Branches = GetSelectListItems(branches);
model.Divisions = GetSelectListItems(divisions);
model.ProjectTypes = GetSelectListItems(project_types);
model.Statuses = GetSelectListItems(statuses);
return View(model);
}
All of those GetAll_____s() and GetSelectListItems(_____) are to fill the enums that populate the dropdown menus.
My Edit action (shockingly similar):
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var model = new ProjectViewModel();
model.Project = db.Projects.Find(id);
if (model.Project == null)
{
return HttpNotFound();
}
var states = GetAllStates();
var branches = GetAllBranches();
var divisions = GetAllDivisions();
var project_types = GetAllProjectTypes();
var statuses = GetAllStatuses();
model.States = GetSelectListItems(states);
model.Branches = GetSelectListItems(branches);
model.Divisions = GetSelectListItems(divisions);
model.ProjectTypes = GetSelectListItems(project_types);
model.Statuses = GetSelectListItems(statuses);
return View("Create", model);
}
// POST: Projects/Edit/5
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "ProjectID,ProjectName,ProjectDescription,DateReceived,EffectiveDate,ExpirationDate,GeneralContractor,ProjectTerm,ProjectType,SubmissionNumber,PolicyNumber,Status,Underwriter,Division,BrokerCity,TAName,Branch,FirstNamedInsuredAddress,FirstNamedInsured,ProjectAddress")] Project project)
{
if (ModelState.IsValid)
{
if (project.ProjectID <= 0)
{
db.Projects.Add(project);
} else
{
db.Entry(project).State = EntityState.Modified;
}
db.SaveChanges();
return RedirectToAction("Index");
}
var model = new ProjectViewModel();
model.Project = db.Projects.Find(project.ProjectID);
var states = GetAllStates();
var branches = GetAllBranches();
var divisions = GetAllDivisions();
var project_types = GetAllProjectTypes();
var statuses = GetAllStatuses();
model.States = GetSelectListItems(states);
model.Branches = GetSelectListItems(branches);
model.Divisions = GetSelectListItems(divisions);
model.ProjectTypes = GetSelectListItems(project_types);
model.Statuses = GetSelectListItems(statuses);
return View("Create", model);
}
I'm assuming the "View" action that I want to be read-only should be similar to the "GET" Edit/Create actions, but I don't know what I need to add to make those textboxes read-only.
What I did to achieve what I actually wanted was I added a bool ReadOnly to my ViewModel and wrapped all of my textboxes and dropdownlists in an if/else statement, like this:
#if (Model.ReadOnly)
{
#Html.EditorFor(model => model.Project.SubmissionNumber, new { htmlAttributes = new { #readonly = "readonly" } })
}
else
{
#Html.EditorFor(model => model.Project.SubmissionNumber)
}
and this:
#if (Model.ReadOnly)
{
#Html.DropDownListFor(model => model.Project.Status,
Model.Statuses,
"--",
new { #disabled = "disabled" })
}
else
{
#Html.DropDownListFor(model => model.Project.Status,
Model.Statuses,
"--")
}
and now I can reuse the exact same view for all three methods without needing to copy paste code.
I did check all issues about that but I couldn't figure out how to solve it. I need to your help guys.
Here is my code about dropdownlist. When I post the page I got the error message which I mentioned above.
.cshtml
<div class="editor-label">
#Html.LabelFor(model => model.Department, new { #style = " margin-top:12px;display:block;text-align:center" })
</div>
<div class="editor-field">
#Html.DropDownList("Department",(SelectList) ViewBag.Department, new { id = "Department", #class = "form-control", #style = "width:250px; margin-top:5px;margin-left: auto;margin-right:auto; text-align:center;" })
#Html.ValidationMessageFor(model => model.Department.Name)
</div>
model
public ActionResult Index()
{
IEnumerable<SelectListItem> items = db.Department
.Select(c => new SelectListItem
{
Value = c.ID.ToString(),
Text = c.Name
});
var selectList = new SelectList(items,"Value","Text");
ViewBag.Department = selectList;
return View();
}
[HttpPost]
public ActionResult Index(Person model, HttpPostedFileBase photo)
{
if (ModelState.IsValid)
{
Person Person = new Person();
Person.DeparmentID = model.Department.ID;
Person.FirmID = model.Firm.ID;
Person.GraduationDate = model.GraduationDate;
Person.HomeTel = model.HomeTel;
Person.MobileTel = model.MobileTel;
Person.Mail = model.Mail;
Person.Name = model.Name;
Person.Surname = model.Surname;
Person.Position = model.Position;
Person.WorkingSituation = model.WorkingSituation;
if (photo != null && photo.ContentLength > 0)
{
if (photo.ContentLength > 10240)
{
ModelState.AddModelError("photo", "Resim boyutu 10 KB'ı aşamaz.");
return View();
}
var supportedTypes = new[] { "jpg", "jpeg", "png" };
var fileExt = System.IO.Path.GetExtension(photo.FileName).Substring(1);
if (!supportedTypes.Contains(fileExt))
{
ModelState.AddModelError("photo", "Yalnızca jpg, jpeg, png veri tipleri desteklenmektedir.");
return View();
}
var fileName = Path.GetFileName(photo.FileName);
photo.SaveAs(Server.MapPath("~/Upload/") + photo.FileName);
Person.Img = fileName;
}
db.Person.Add(Person);
db.SaveChanges();
ViewBag.ShowConfirmation = "The item was created.";
return RedirectToAction("Index");
}
else
{
return View(model);
}
}
Maybe if you use DropDownListFor instead of DropDownList? You would need to include the listitems in your viewmodel instead of in a viewbag.
Along these lines
#Html.DropDownListFor(m => m.Department, new SelectList(Model.Departments, "Value", "Text", Model.Department), new { #style = "..." })
The second and third parameter of the SelectList constructor define the names of the Value and Text parameters in your SelectListItem list.
This is how i am loading on page load state and city dropdown:
My Controller method:
This is the first method which is calling when page is loaded.
public ActionResult Index()
{
var states = GetStates();
var cities = Enumerable.Empty<SelectListItem>();
ViewBag.States = states;
ViewBag.Cities = cities;
}
private IEnumerable<SelectListItem> GetStates()
{
using (var db = new DataEntities())
{
return db.States.Select(d => new SelectListItem { Text = d.StateName, Value =d.Id.ToString() });
}
}
[HttpGet]
public ActionResult GetCities(int id)
{
using (var db = new DataEntities())
{
var data = db.Cities.Where(d=>d.StateId==id).Select(d => new { Text = d.CityName, Value = d.Id }).ToList();
return Json(data, JsonRequestBehavior.AllowGet);
}
}
My View:
IEnumerable<SelectListItem> States = ViewBag.States;
IEnumerable<SelectListItem> Cities = ViewBag.Cities;
#Html.DropDownList("State", States, "Select State", new { onchange="loadCities(this)"})
#Html.DropDownListFor(m => m.CityId, Cities, "Select City", new { id="ddlCity"})
function loadCities(obj) {
$.ajax({
url: "/Home/GetCities",
data: { id: $(obj).val() },
contentType:"application/json",
success:function(responce){
var html = '<option value="0">Select City</option>';
$(responce).each(function () {
html += '<option value="'+this.Value+'">'+this.Text+'</option>'
});
$("#ddlCity").html(html);
}
});
}
Any better way then this to load state and city dropdown?
public class HomeController : Controller
{
public ActionResult Index(int id=0)
{
Person model = null;
var states = GetStates().ToList();
var cities = Enumerable.Empty<SelectListItem>();
if (id > 0)
{
using (var db = new DataEntities())
{
model = db.People.Include("City").FirstOrDefault(d => d.Id == id);
if (model == null)
model = new Person();
else
{
states.First(d => d.Value == model.City.StateId.ToString()).Selected = true;
cities = db.Cities.Where(d => d.StateId == model.City.StateId).ToList().Select(d => new SelectListItem { Text = d.CityName,Value=d.Id.ToString(),Selected=d.Id==model.CityId });
}
}
}
else
{
model = new Person();
}
ViewBag.States = states;
ViewBag.Cities = cities;
ViewBag.Persons = GetPersons();
return View(model);
}
[HttpGet]
public ActionResult GetCities(int id)
{
using (var db = new DataEntities())
{
var data = db.Cities.Where(d=>d.StateId==id).Select(d => new { Text = d.CityName, Value = d.Id }).ToList();
return Json(data, JsonRequestBehavior.AllowGet);
}
}
public ActionResult SavePersonDetail([Bind(Exclude = "Id")] Person model)
{
// var employeeDal= new Emploee();
//employee.firstname=model.
if (ModelState.IsValid)
{
var Id = model.Id;
int.TryParse(Request["Id"], out Id);
using (var db = new DataEntities())
{
if (Id > 0)
{
var person = db.People.FirstOrDefault(d => d.Id == Id);
if (person != null)
{
model.Id = Id;
db.People.ApplyCurrentValues(model);
}
}
else
{
db.People.AddObject(model);
}
db.SaveChanges();
}
}
if (!Request.IsAjaxRequest())
{
ViewBag.States = GetStates();
ViewBag.Persons = GetPersons();
ViewBag.Cities = Enumerable.Empty<SelectListItem>();
return View("Index");
}
else
{
return PartialView("_personDetail",GetPersons());
}
}
public ActionResult Delete(int id)
{
using (var db = new DataEntities())
{
var model = db.People.FirstOrDefault(d => d.Id == id);
if (model != null)
{
db.People.DeleteObject(model);
db.SaveChanges();
}
}
if (Request.IsAjaxRequest())
{
return Content(id.ToString());
}
else
{
ViewBag.States = GetStates();
ViewBag.Persons = GetPersons();
ViewBag.Cities = Enumerable.Empty<SelectListItem>();
return View("Index");
}
}
private IEnumerable<SelectListItem> GetStates()
{
using (var db = new DataEntities())
{
return db.States.ToList().Select(d => new SelectListItem { Text = d.StateName, Value =d.Id.ToString() });
}
}
private IEnumerable<Person> GetPersons()
{
using (var db = new DataEntities())
{
return db.People.Include("City").Include("City.State").ToList();
}
}
public ActionResult HomeAjax()
{
ViewBag.States = GetStates();
ViewBag.Cities = Enumerable.Empty<SelectListItem>();
using (var db = new DataEntities())
{
var data = db.States.Include("Cities").Select(d => new { Id = d.Id, Name = d.StateName, Cities = d.Cities.Select(x => new { Id=x.Id,Name=x.CityName}) }).ToList();
ViewBag.CityStateJson = new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(data);
}
ViewBag.Persons = GetPersons();
return View();
}
}
#model IEnumerable<Person>
<div>
<table>
<tr>
<th>
First Name
</th>
<th>
Last Name
</th>
<th>
Email
</th>
<th>
City
</th>
<th>
State
</th>
<th>
Edit
</th>
</tr>
#if (Model.Count() == 0)
{
<tr>
<td colspan="6">
<h3>No data available</h3>
</td>
</tr>
}
else {
foreach (var item in Model) {
<tr data-id="#item.Id">
<td data-id="fn">#item.FirstName</td>
<td data-id="ln">#item.LastName</td>
<td data-id="email">#item.Email</td>
<td data-id="cn">#item.CityName<input type="hidden" value="#item.CityId" /></td>
<td>#item.StateName</td>
<td>
#if (ViewBag.Title == "Home Ajax" || Request.IsAjaxRequest())
{
Update
<span>#Ajax.ActionLink("Delete", "Delete", new { id = item.Id }, new AjaxOptions {OnSuccess="deleteSuccess",OnBegin="showLoader",OnComplete="hideLoader" })</span>
}
else {
<span>#Html.ActionLink("Update", "Index", new { id = item.Id })</span>
<span>#Html.ActionLink("Delete", "Delete", new { id = item.Id })</span>
}
</td>
</tr>
}
}
</table>
</div>
#model Person
#{
ViewBag.Title = "Home Ajax";
IEnumerable<Person> persons = ViewBag.Persons;
IEnumerable<SelectListItem> States = ViewBag.States;
IEnumerable<SelectListItem> Cities = ViewBag.Cities;
IEnumerable<State> fullStates=ViewBag.CityStates;
}
#section featured {
<section class="featured">
<div class="content-wrapper">
<hgroup class="title">
<h1>#ViewBag.Title.</h1>
</hgroup>
</div>
</section>
}
#section styles{
<style type="text/css">
td,th {
border:1px solid;
padding:5px 10px;
}
select {
padding:5px 2px;
width:310px;
font-size:16px;
}
</style>
}
#section scripts{
#Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
var jsonArray = #Html.Raw(ViewBag.CityStateJson)
function clearValues() {
$("input[type='text'],select").val('');
$("input[type='hidden'][name='Id']").val(0);
}
function loadCities(obj) {
for (var i = 0; i < jsonArray.length; i++) {
if (jsonArray[i].Id == parseInt($(obj).val())) {
fillCity(jsonArray[i].Cities);
break;
}
}
}
function Edit(obj, Id) {
// alert("hi")
$("input[type='hidden'][name='Id']").val(Id);
var tr = $(obj).closest("tr");
$("#txtfirstName").val($("td[data-id='fn']", tr).text().trim());
$("#txtlastName").val($("td[data-id='ln']", tr).text().trim());
$("#txtemail").val($("td[data-id='email']", tr).text().trim());
var city = $("td[data-id='cn'] input[type='hidden']", tr).val();
var state;
for (var i = 0; i < jsonArray.length; i++) {
for (var j = 0; j < jsonArray[i].Cities.length; j++) {
if (jsonArray[i].Cities[j].Id == parseInt(city)) {
state = jsonArray[i].Id;
break;
}
}
if (state) {
fillCity(jsonArray[i].Cities);
break;
}
}
$("#ddlState").val(state);
$("#ddlCity").val(city);
}
function fillCity(obj) {
var html = '<option value="0">Select City</option>';
$(obj).each(function () {
html += '<option value="' + this.Id + '">' + this.Name + '</option>'
});
$("#ddlCity").html(html);
}
function deleteSuccess(responce) {
alert("record deleted successfully");
$("tr[data-id='" + responce + "']").remove();
}
function insertSuccess() {
alert("Record saved successfully");
clearValues();
}
function showLoader() {
$("#overlay").show();
}
function hideLoader() {
$("#overlay").hide();
}
</script>
}
<h3>Add Personal Detail</h3>
#using (Ajax.BeginForm("SavePersonDetail", "Home", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "personList" ,OnSuccess="insertSuccess",OnBegin="showLoader",OnComplete="hideLoader"}))
{
#Html.HiddenFor(m => m.Id);
<ol class="round">
<li>
#Html.LabelFor(m => m.FirstName)
#Html.TextBoxFor(m => m.FirstName, new { id = "txtfirstName" })
#Html.ValidationMessageFor(m => m.FirstName)
</li>
<li>
#Html.LabelFor(m => m.LastName)
#Html.TextBoxFor(m => m.LastName, new { id = "txtlastName" })
#Html.ValidationMessageFor(m => m.LastName)
</li>
<li>
#Html.LabelFor(m => m.Email)
#Html.TextBoxFor(m => m.Email, new { id = "txtemail" })
#Html.ValidationMessageFor(m => m.Email)
</li>
<li>
#Html.Label("State")
#Html.DropDownList("State", States, "Select State", new { onchange = "loadCities(this)", id = "ddlState" })
</li>
<li>
#Html.LabelFor(m => m.CityId)
#Html.DropDownListFor(m => m.CityId, Cities, "Select City", new { id = "ddlCity" })
#Html.ValidationMessageFor(m => m.CityId)
</li>
</ol>
<input type="submit" value="Save" />
<input type="button" value="Cancel" onclick="clearValues();"/>
}
<h2>
Person List
</h2>
<div style="position:fixed;text-align:center;top:0;bottom:0;left:0;right:0;z-index:10;background-color:black;opacity:0.6;display:none;" id="overlay">
<img style="position:relative;top:370px" src="~/Images/ajax-loader.gif" />
</div>
<div id="personList">
#Html.Partial("_personDetail", persons)
</div>
You approach using ajax is fine although I would recommend a few better practices including using a view model with properties for StateID, CityID StateList and CityList, and using Unobtrusive JavaScript rather than polluting you markup with behavior, and generating the first ("please select") option with a null value rather than 0 so it can be used with the [Required] attribute
HTML
#Html.DropDownList(m => m.StateID, States, "Select State") // remove the onchange
#Html.DropDownListFor(m => m.CityID, Cities, "Select City") // why change the default ID?
SCRIPT
var url = '#Url.Action("GetCities", "Home")'; // use the helper (dont hard code)
var cities = $('#CityID'); // cache the element
$('#StateID').change(function() {
$.getJSON(url, { id: $(this).val() }, function(response) {
// clear and add default (null) option
cities.empty().append($('<option></option>').val('').text('Please select'));
$.each(response, function(index, item) {
cities.append($('<option></option>').val(item.Value).text(item.Text));
});
});
});
If you were rendering multiple items (say you were asking the user to select their last 10 cities they visited), you can cache the result of the first call to avoid repeated calls where their selections may include cities from the same state.
var cache = {};
$('#StateID').change(function() {
var selectedState = $(this).val();
if (cache[selectedState]) {
// render the options from the cache
} else {
$.getJSON(url, { id: selectedState }, function(response) {
// add to cache
cache[selectedState] = response;
.....
});
}
});
Finally, in response to your comments regarding doing it without ajax, you can pass all the cities to the view and assign them to a javascript array. I would only recommend this if you have a few countries, each with a few cities. Its a matter of balancing the slight extra initial load time vs the slight delay in making the ajax call.
In the controller
model.CityList = db.Cities.Select(d => new { City = d.CountryID, Text = d.CityName, Value = d.Id }).ToList();
In the view (script)
// assign all cities to javascript array
var allCities= JSON.parse('#Html.Raw(Json.Encode(Model.CityList))');
$('#StateID').change(function() {
var selectedState = $(this).val();
var cities = $.grep(allCities, function(item, index) {
return item.CountryID == selectedState;
});
// build options based on value of cities
});
This is a correct approach, but you can simplify your javascript:
function loadCities(obj) {
$.getJSON("/Home/GetCities", function (data) {
var html = '<option value="0">Select City</option>';
$(data).each(function () {
html += '<option value="'+this.Value+'">'+this.Text+'</option>'
});
$("#ddlCity").html(html);
});
}
Further possible simplification:
Add the default item (Select City) server-side, so your javascript will be smaller.
Here's how I'd do it without the page refresh, assuming the list of cities isn't too long.
I'm assuming you can create a GetStatesAndCities method to return a Dictionary.
public ActionResult Index()
{
Dictionary<string, List<String>> statesAndCities = GetStatesAndCities();
ViewBag.StatesAndCities = Json(statesAndCities);
}
Then in the view:
var states = JSON.parse(#ViewBag.StatesAndCities);
function loadCities(obj) {
var cities = states[$(obj).val()];
var html = '<option value="0">Select City</option>';
$(cities).each(function () {
html += '<option value="'+this.Value+'">'+this.Text+'</option>'
});
$("#ddlCity").html(html);
}
This way when the state is changed the cities field with update immediately with no need for callback.
disclaimer: This is not a code answer, there are plenty other answers.
I think best way to keep yourself happy to seperate UI pages from data => turn them into API calls:
/GetCities
/GetStates
Now you can simply leave the select's empty on Razor rendering the page. And use a Jquery/Bootstrap plugin to create an AJAX select box.
This way when the user stops typing his search, this search string can than be send with the AJAX call (eg: /GetStates?search=test) and then a small result set can be send back to the website.
This gives:
Better separation in serveside code
Better User eXperience.
Smaller page loads (since you no longer send all the options to user when he requests the page, only when he opens the select box).
How about using Knockout?
Knockout is a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model
You have to use ajax for your cities. But with knockout you dont need to write
var html = '<option value="0">Select City</option>';
$(responce).each(function () {
html += '<option value="'+this.Value+'">'+this.Text+'</option>'});
$("#ddlCity").html(html);
in your javascript.Knockout makes it simple.
You can simply write:
function CityModel() {
var self = this; // that means this CityModel
self.cities = ko.observableArray([]);
self.getCities = function () {
$.ajax({
url: "/Home/GetCities",
data: { id: $(obj).val() },
contentType: "application/json",
success: self.cities
});
}
}
ko.applyBindings(new CityModel());
thats all. But you have to bind your data into html elements.
Instead of using :
#Html.DropDownListFor(m => m.CityId, Cities, "Select City", new { id="ddlCity"})
You can use:
<select data-bind="options:cities,optionsValue:"Id",optionsText:"CityName",optionsCaption:"Select City""></select>
or you can mix razor and knockout:
#Html.DropDownListFor(m => m.CityId, Cities, "Select City", new { id="ddlCity",data_bind:"options:cities,optionsValue:\"Id\",optionsText:\"CityName\""})
One more thing you have to call GetCities when State changes, you can :
#Html.DropDownList("State", States, "Select State", new {data_bind:"event:\"change\":\"$root.GetCities\""})
Dont be scare with \"\" things this because " is an escape character and we have to say to razor i want to use " by using \ before it.
You can find more info about knockout :Knockout
And mixing with razor: Razor and Knockout
Ps: yes using knockout is suspend us from Razor and Mvc. You have to write another ViewModel . But like this situations ko is helpful. Mixing razor and knockout is another option for you.
In my project I have one view that populates dynamically, creating either a single or multiple forms on one screen / page. I have managed to dynamically give each form a unique id, however when there are multiple forms on one page, an array of all forms is returned when only one is submitted and I do not know how to identify the actual instance that is being submitted from the array.
Here is my View;
#using Microsoft.AspNet.Identity
#model IEnumerable<Template.Models.GetQuestionViewModel>
#{
ViewBag.Title = "View question";
ViewBag.Page = Model.First().PageNumber;
}
#Html.AntiForgeryToken();
#foreach (var q in Model.OrderBy(o => o.QuestionRanking))
{
Html.BeginForm("ViewQuestion", "Question", FormMethod.Post, new { #class = "form-horizontal", id = #q.formNumber, role = "form" });
var qtype = q.QuestionTypeId;
<div>
<h1>#q.QuestionRanking. #q.Question1</h1>
</div><div class="form-group">
#switch (qtype)
{
case 1:
// Textbox
#Html.TextArea("Answer", q.Answer, new { #class = "form-control", rows="4", cols="10" })
break;
case 2:
// Dropdown
<select class="form-control" id="Answer" name="Answer">
#foreach (var item in q.QuestionOptions.OrderBy(o => o.QuestionOptionRanking))
{
if (q.Answer == item.QuestionOption1)
{
<option value="#item.QuestionOption1" selected>#item.QuestionOption1</option>
}
else
{
<option value="#item.QuestionOption1">#item.QuestionOption1</option>
}
}
</select>
break;
...
}
</div>
<input type="hidden" name="QuestionId" value="#q.Id" /
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" name="#q.formNumber" class="btn btn-primary" value="Save answer" />
</div>
</div>
<br />
<hr />
**Html.EndForm();**
}
And here is my controller;
using System.Web.Mvc;
using Template.Models;
using Microsoft.AspNet.Identity;
using System.Linq;
using System.Collections.Generic;
using System;
namespace Template.Controllers
{
public class QuestionController : Controller
{
private WebTemplateEntities db = new WebTemplateEntities();
// GET: /Questions/ViewQuestion/5
[HttpGet]
public ActionResult ViewQuestion(int page = 1)
{
// If user is not logged in, push them back to Login
var userId = User.Identity.GetUserId();
if (userId == null)
{
return RedirectToAction("Login", "Account");
}
// Get the list of Questions and Response options from the database
List<GetQuestionViewModel> quest = new List<GetQuestionViewModel>();
var i = 1; // for form array
foreach (var item in db.Questions.Where(q => q.PageNumber == page))
{
//Check if user has submitted a response
var reply = db.Responses.Where(r => r.QuestionId == item.Id && r.UserId == userId).Select(r => r.Answer).ToList();
//If no input, do not look for answers
if (reply.Count == 0)
{
quest.Add(new GetQuestionViewModel()
{
Id = item.Id,
QuestionOptions = db.QuestionOptions
.Where(k => k.QuestionId == item.Id)
.ToList(),
PageNumber = item.PageNumber,
Question1 = item.Question1,
QuestionRanking = item.QuestionRanking,
QuestionTypeId = item.QuestionTypeId,
formNumber = "form" + i // for form array
});
}
else //If user has input answers find them
{
quest.Add(new GetQuestionViewModel()
{
Id = item.Id,
QuestionOptions = db.QuestionOptions
.Where(k => k.QuestionId == item.Id)
.ToList(),
PageNumber = item.PageNumber,
Question1 = item.Question1,
QuestionRanking = item.QuestionRanking,
QuestionTypeId = item.QuestionTypeId,
formNumber = "form" + i, // for form array
Answer = reply.First()
});
}
i++; // for form array
}
return View(quest);
}
// POST: /Questions/ViewQuestion/5
[HttpPost]
public ActionResult ViewQuestion([Bind(Include = "QuestionId, Answer, UserId")] ResponseViewModel responseViewModel)
{
// Get page and question id
var page = System.Web.HttpContext.Current.Request["page"]; //NOTE: Problem with binding on multi form pages
var qId = System.Web.HttpContext.Current.Request["QuestionId"]; //NOTE: Problem with binding on multi form pages
var newAnswer = System.Web.HttpContext.Current.Request["Answer"];
int a = Convert.ToInt32(qId); //NOTE: Problem with binding on multi form pages
//Check if user has previously answerer
List<Response> reply = new List<Response>();
reply = (from r in db.Responses
where r.QuestionId == a && r.UserId == responseViewModel.UserId
select r ).ToList();
// If this is the first time the user has answered, add the response
if (reply.Count() == 0)
{
Response re = new Models.Response();
re.Answer = responseViewModel.Answer;
re.UserId = responseViewModel.UserId;
re.QuestionId = responseViewModel.QuestionId;
re.Source = "Web";
re.Status = "New";
re.DateStamp = System.DateTime.Now;
db.Responses.Add(re);
db.SaveChanges();
}
else // If the user has already answered move the original responses to AuditTrail
{
var ans = reply.Select(r => r.Id).First();
var dos = reply.Select(r => r.Answer).First();
AuditTrail au = new Models.AuditTrail();
au.ResponseId = ans;
au.OriginalData = dos;
au.DateChanged = System.DateTime.Now;
db.AuditTrails.Add(au);
db.SaveChanges();
// Update user's response
var stat = "Update, 1"; // NOTE: Work in progress; create array of updates to track number of updates
var updateAnswer = db.Responses.Single(q => q.Id == ans);
updateAnswer.Answer = newAnswer;
updateAnswer.Status = stat;
db.SaveChanges();
}
return RedirectToAction("ViewQuestion/" + page);
}
}
}
There are more question type cases but I left them out for clarity.
Where there is more than one question and response per screen the post action returns an array of all the forms on the page. From my initial investigations I cannot see how to determine which specific form is submitted therefore I do not know how to pull the correct response.
Is there a way?
There is concept called PartialView in MVC. You can use this to can render multiple forms in single page.
How utterly stupid I feel!! I didn't close the form!
For those of you struggling with the same issue, simply close with Html.EndForm();
I placed it right after the last line in my view and now it returns only the values from the submitted form.
And my boss spotted it..... Not a good day!