alexanderar / Mvc.CascadeDropDown

MIT License
26 stars 12 forks source link

Using Entity framework #13

Closed frellan closed 7 years ago

frellan commented 7 years ago

I'm trying to use this library with Entity framework model binding but I can't get the cascading dropdown to respond when I am selecting anything from the parent one, it's always disabled.

Controller.cs

// GET: Improvements/New
public ViewResult New()
{
    var dbAreas = _context.ImprovementAreas;
    var viewModel = new NewImprovementViewModel
    {
        Improvement = new Improvement(),
        Areas = dbAreas.Select
            (a => new SelectListItem { Text = a.Name, Value = a.Id.ToString() }).ToList()
    };
    return View(viewModel);
}
...
public JsonResult GetSubAreas(int areaId)
{
    var dbSubAreas = _context.ImprovementSubAreas.Where(
        sub => sub.ImprovementArea.Id == areaId).ToList();
    var subAreas = dbSubAreas.Select(
        sub => new SelectListItem { Text = sub.Name, Value = sub.Id.ToString() }).ToList();
    return Json(subAreas, JsonRequestBehavior.AllowGet);
}

New.cshtml

<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementAreaId)
    @Html.DropDownListFor(m => m.Improvement.ImprovementArea,
    Model.Areas, "Select area", new { @class = "form-control" })
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementSubAreaId)
    @Html.CascadingDropDownListFor(
    expression: m => m.Improvement.ImprovementSubArea,
    triggeredByProperty: m => m.Improvement.ImprovementAreaId,
    url: Url.Action("GetSubAreas", "Improvements"),
    ajaxActionParamName: "areaId",
    optionLabel: "Select subarea",
    disabledWhenParrentNotSelected: true,
    htmlAttributes: new { @class = "form-control" })
</div>

ViewModel

public Improvement.Improvement Improvement { get; set; }
public IEnumerable<SelectListItem> Areas { get; set; }

The other models have navigation properties tied to them so thats what I'm trying to set but it doesn't seem to work. Do I have to just use simple string properties in the view model like in the example and then bind everything in the POST request to the controller or is there a nice way to do this?

I also don't understand what the ajaxActionParamName refers to exactly?

alexanderar commented 7 years ago

You create parent dropdown for Improvement.ImprovementArea property and in cascading dropdown you specify that Improvement.ImprovementAreaId should trigger the data loading. Because of that cascading dropdown is newer filled with data. In addition to that dropdowns could be mapped only to simple properties (string, int and etc) and not to a complex objects because by the end of the day the value that is sent to server it is a value of selected

In your case, patent dropdown values will be Ids of objects from dbAreas set and dropdown name will be Improvement_ImprovementArea. When you post the form, default model binder will try to map this value to Model.Improvement.ImprovementArea property( ImprovementArea property of object that is stored as Improvement property of your model). So your model should look like this:

If ImprovementArea is a complex object, it will not work. If it is a simple type (such as string/int/long...) it will.

Instead of creating dropdowns for navigation properties you should create them for their Ids. Parent dropdown is crated for ImprovementAreaId and second dropdown for ImprovementSubAreaId:

<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementAreaId)
    @Html.DropDownListFor(m => m.Improvement.ImprovementAreaId, 
    Model.Areas, "Select area", new { @class = "form-control" })
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementSubAreaId)
    @Html.CascadingDropDownListFor(
    expression: m => m.Improvement.ImprovementSubAreaId,
    triggeredByProperty: m => m.Improvement.ImprovementAreaId,
    url: Url.Action("GetSubAreas", "Improvements"),
    ajaxActionParamName: "areaId",
    optionLabel: "Select subarea",
    disabledWhenParrentNotSelected: true,
    htmlAttributes: new { @class = "form-control" })
</div>

I also don't understand what the ajaxActionParamName refers to exactly?

It is the name of an argument of an action on the server that you use to get the data for the cascading drop down. In your code you use it correctly. You set ajaxActionParamName: "areaId" and the action on the server is public JsonResult GetSubAreas(int areaId)

frellan commented 7 years ago

Thank you for quick answer!

It all makes sense now but it still doens't work, nothing happens. I also get an error when I first load the page that the javascript code cant access a null value. It seems that it tries to add an observer to a null value. Do I have to instantiate the Model in the New action somehow?

Controller

var viewModel = new NewImprovementViewModel
{
    Improvement = new Improvement(), // Do i really need to set the Area too?
    Areas = dbAreas.Select(a => new SelectListItem { Text = a.Name, Value = a.Id.ToString() }).ToList()
};
return View(viewModel);

EDIT: It seems its trying to find an object with id ImprovementAreaId but it is actually _ImprovementImprovementAreaId, is this a bug?

I guess I can solve it by using simple attributes in the view model but chaining like this should work right?

alexanderar commented 7 years ago

Add 2 properties to your view model SelectedAreaId and SelectedSubAreaId:

public class NewImprovementViewModel
{
     public int SelectedAreaId {get;set;}
     public int SelectedSubAreaId{get;set;}
}

Create a dropdowns like this:


<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementAreaId)
    @Html.DropDownListFor(m => m.SelectedAreaId, 
    Model.Areas, "Select area", new { @class = "form-control" })
</div>
<div class="form-group">
    @Html.LabelFor(m => m.Improvement.ImprovementSubAreaId)
    @Html.CascadingDropDownListFor(
    expression: m => m.SelectedSubAreaId,
    triggeredByProperty: m => m.SelectedAreaId,
    url: Url.Action("GetSubAreas", "Improvements"),
    ajaxActionParamName: "areaId",
    optionLabel: "Select subarea",
    disabledWhenParrentNotSelected: true,
    htmlAttributes: new { @class = "form-control" })
</div>

Your post action should get the same NewImprovementViewModel ans an argument:

[HttpPost]
public ActionResult New(NewImprovementViewModel model)
{
    //model.SelectedSubAreaId will have an id of selected sub-area
    //model.SelectedAreaId will have an id of selected area
}
frellan commented 7 years ago

Yeah I did that, solved my problem. Thank you so much for your help!

Only thing that bugs me now is that the default "select subarea" can also be selected, is there a way to disable that option, or leave it out completely?

Thank you again.

alexanderar commented 7 years ago

"select subarea" is an option label and on not an option. You can modify it by setting optionLabel parameter of the CascadingDropDownListFor helper. In order to leave it empty simply pass an empty string: optionLabel: string.Empty,

I would also suggest you to add [Required] data annotation to the model properties to insure that user selects something:

public class NewImprovementViewModel
{
     [Required]
     public int SelectedAreaId {get;set;}

     [Required]
     public int SelectedSubAreaId{get;set;}
}

Don't forget this give us a star if you find this library useful