Unbreakable Unbreakable - 2 months ago 18
Ajax Question

How to make an ajax dropdown strongly bound to the ViewModel in ASP.Net MVC 5

So, I have a referral form for job application. Now when a job seeker selects a company from a company dropdown, I want to display the dropdown of coverletters just for that company (coverletters are already uploaded by candidate from another tab). Now for getting referred for Microsoft a person might have 3 coverletters.

Issue:



I want to know how to can I display the dropdown from ajax call and still keep the coverletter dropdown strongly bound to the Model.

Below is Non AJAX strongly Bound: shows all the coverletters irrespective of company selected)

View:



@model Bridge.ViewModels.ReferralViewModel
@using (Html.BeginForm())
{
<div class="form-group">
@Html.LabelFor(model => model.CompanyId, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(m => m.CompanyId, Model.Companies, new { @class = "form-control js-change", onchange = "companyChanged()" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.CoverLetterId, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(m => m.CoverLetterId, new SelectList(Model.CoverLetters, "CoverLetterId", "CoverLetterName", Model.CoverLetters), new { @class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}


Model:



public class ReferralViewModel
{
public int ReferralViewModelId { get; set; }
public int CompanyId { get; set; }
public IEnumerable<SelectListItem> Companies { get; set; }
public int CoverLetterId { get; set; }
public IEnumerable<CoverLetter> CoverLetters { get; set; }
}


Controller:



public ActionResult Create()
{
var viewModel = new ReferralViewModel
{
var candidateId = User.Identity.GetUserId();
Companies = _context.Companies.Select(x => new SelectListItem
{
Text = x.CompanyName,
Value = x.CompanyId.ToString()

}),
CoverLetters = _context.CoverLetters.Where(r => r.CandidateId == candidateId).ToList()
};
return View(viewModel);
}


Now AJAX Attempt:



Controller Action

public JsonResult ListOfCoverLetterByCompanyId(int companyId)
{
var coverletters = _context.CoverLetters
.Where(c => c.CompanyId == companyId)
.ToList();

var dropdown = new List<SelectListItem>();
foreach (var cl in coverletters)
{
dropdown.Add(new SelectListItem { Text = cl.CoverLetterName, Value = cl.CoverLetterId.ToString() });
}
return Json(dropdown, JsonRequestBehavior.AllowGet);
}


New View

@model Bridge.ViewModels.ReferralViewModel
@using (Html.BeginForm())
{
@Html.LabelFor(model => model.CompanyId, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownListFor(m => m.CompanyId, Model.Companies, new { @class = "form-control js-change", onchange = "companyChanged()" })
</div>
</div>
@* Dropdown will appear here*@
<select id="CoverLetterId" name="CoverLetterId"></select>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}

@section scripts{
<script>
function companyChanged() {
var companyId = $(".js-change").val();
$.ajax({
url: "/Referral/ListOfCoverLetterByCompanyId",
data: { companyId: companyId },
contentType: "application/json; charset-utf-8",
success: function (datas) {
$("#CoverLetterId").html("");
$.each(datas, function (i, data) {
$("#CoverLetterId ").append('<option value="' + data.Value + '">' + data.Text + '</option>');
});
},
error: function () {}
});
}
</script>
<script>
companyChanged();
</script>
}


New View Model

public class ReferralViewModel
{
public int ReferralViewModelId { get; set; }
public int CompanyId { get; set; }
public IEnumerable<SelectListItem> Companies { get; set; }
public int CoverLetterId { get; set; }
}


I think by following current code I lost the beauty of strongly bound Razor view. Dropdown is not bound to any property. Can I somehow make it strongly bound and use @Html.DropdownListFor

Answer Source

Your original view model is correct although it would be better for your CoverLetters property to be public IEnumerable<SelectListItem> CoverLetters { get; set; }, and both the CompanyId and CoverLetterId properties should be int? (nullable)

and in the view, strongly bind to your model using

@Html.DropDownListFor(m => m.CoverLetterId, Model.CoverLetters, "Please select", new { @class = "form-control" })

In your controller, add a private method that initializes both SelectLists. If CompanyId has a value, then your populate CoverLetters from the database, otherwise you initialize it to an empty collection. You can then call that method in both the GET method, and in the POST method if ModelState is invalid

private void ConfigureViewModel(ReferralViewModel model)
{
    // Populate companies always
    model.Companies = _context.Companies.Select(x => new SelectListItem
    {
        Text = x.CompanyName,
        Value = x.CompanyId.ToString()
    });
    // Populate cover letters only if a company has been selected
    // i.e. if your editing and existing Referral, or if you return the view in the POST method
    if (model.CompanyId.HasValue)
    {
        model.CoverLetters = _context.CoverLetters.Where(x => x.CompanyId == companyId).Select(x => new SelectListItem
        {
            Value = x.CoverLetterId.ToString(),
            Text = x.CoverLetterName
        });
    }
    else
    {
        model.CoverLetters  = new SelectList(Enumerable.Empty<SelectListItem>());
    }
}

public ActionResult Create()
{
    ReferralViewModel model = new ReferralViewModel();
    ConfigureViewModel(model);
    return View(model);
}

public ActionResult Create(ReferralViewModel model)
{
    if (!ModelState.IsValid())
    {
        ConfigureViewModel(model);
        return View(model);
    }
    // Initialize data model, map properties from view model, save and redirect
}