1

Summary:

I'm trying to use two DropDownList controls to filter the data that is currently being sorted and displayed in a view.

What we are going to learn Creating the ViewController for One to Many and Many-to-Many relationships that could Filter the data using DropDownList

Possible Causes If my DropdownList code is not terrible wrong, The ViewModel I'm using to display the data has no proper support for the DropDownList items.

In other words, the RazorView and my ViewModels are not compatible for what I'm trying to achieve. If I try to change my ViewModel or RazorView, I get an eldless loop of errors for my existing code.

OR The Linq Query needs an expert attention

Here is FilterViewModel.cs

    public IEnumerable <App>      Apps { get; set; }
    public IEnumerable <Language> Languages { get; set; }
    public IEnumerable <Platform> Platforms { get; set; }
    public IEnumerable <AgeGroup> AgeGroups { get; set; }
    public IEnumerable <ProductCode> ProductCodes { get; set; }

Here is AppsController.cs

 public ActionResult FilterApps(App app)
    {
        var apps = _context.Apps.ToList();
        var languages = _context.Languages.ToList();
        var productCodes = _context.ProductCodes.ToList();
        var platforms = _context.Platforms.ToList();
        var ageGroups = _context.AgeGroups.ToList();
        var viewModel = new FilterViewModel
        {
            AgeGroups = ageGroups,
            Languages = languages,
            Platforms = platforms,
            ProductCodes = productCodes,
            Apps = apps
            .Where(a => a.LanguageId == app.LanguageId && a.PlatformId == app.PlatformId)
     // I also tried all possible combinations :(a.Lanage.Id etc)
        };
        return View("FilterApps", viewModel);
    }

Here is the FilterApps.cshtml

@model Marketing.ViewModels.FilterViewModel
<h2>FilterApps</h2>
@using (Html.BeginForm("FilterApps", "Apps", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="form-group">

    @Html.DropDownListFor( m => m.Languages,
      new SelectList(Model.Languages, "Id", "Name"),"Select Language",
      new { @class = "form-control", @id = "dropDown" })

    @Html.DropDownListFor(m => m.Platforms, 
      new SelectList(Model.Platforms, "Id", "Name"), "Select Platform",
      new { @onchange = "this.form.submit();",
      @class = "form-control", @id = "dropDown" })

</div>
}

//The existing code below is working fine so far.
@foreach (var group in Model.AgeGroups)
  {
   <h4>@group.Name</h4>

     @foreach (var app in Model.Apps.OrderBy(a => a.AppOrder))
            {
                if (app.AgeGroupId == group.Id)
                {
                 @app.ProductCode.Name
                 @app.Name
                 @app.Platform.Name
                }
             }
   }

Probably unnecessary but I hope the additional information will help.

Additional Information

The App.cs is referencing all other tables e.g.

public Language Language { get; set; }
public int LanguageId { get; set; }

public Platform Platform { get; set; }
public int PlatformId { get; set; } 
and so on... 

What I have already tried Several breakpoints and Logs to track the data, I also tried to use the following but it ruins my existing sorting and grouping.

public App App { get; set; } //Instead of the IEnumerable<App> 
12
  • 1
    You cannot bind a <select> to a collection of complex objects (which is what Languages and Platforms are). You view model needs additional properties say, int SelectedLanguage and int SelectedPlatform to bind your dropdownlists to. Are you wanting to display Apps based on the value of those 2 dropdownlists Commented Nov 17, 2017 at 6:03
  • And how are you using the other 4 properties in your view (are Languages and Platforms just for the dropdownlists - and if so they should be IEnumerable<SelectListItem> properties)? Commented Nov 17, 2017 at 6:05
  • @ToughGuy Form submit on dropdown selection, seriously!!?? Commented Nov 17, 2017 at 6:05
  • 1
    Agree with @JibinBalachandran. Do NOT do that (and do not give your dropdowns the same id attribute which is invalid html) Commented Nov 17, 2017 at 6:08
  • Your also posting back a completely different model so nothing would bind anyway. Its not really clear what you wanting to do with this, but certainly your view model is not correct. Commented Nov 17, 2017 at 6:09

1 Answer 1

2
+50

There are multiple issues with your code.

First you cannot bind a <select> element to a collection of complex objects. A <select> posts back the value of its selected option (which will be an int assuming the Id property of Language is int).

Next the view in the model is FilterViewModel (and your generating form controls with name attributes based on those properties), but your posting back to a different model (App) which does not contain those properties so nothing would bind anyway.

Your adding a null label option ("Select Language") and if that were selected, it would post a null value which would cause your query to fail.

There are also some bad practices which I have noted below.

Your view model should be

public class AppsFilterVM
{
    public int? Language { get; set; }
    public int? Platform { get; set; }
    public IEnumerable<SelectListItem> LanguageOptions { get; set; }
    public IEnumerable<SelectListItem> PlatformOptions { get; set; }
    ...
    public IEnumerable <App> Apps { get; set; }
}

Its not clear what AgeGroups and ProductCodes are for so I have omitted them in the code above, and from your comments, I have assumed that the user can filter by either Language or Platform or both

The controller code would be

public ActionResult FilterApps(AppsFilterVM model)
{
    var apps = _context.Apps;
    if (model.Language.HasValue)
    {
        apps = apps.Where(x => x.LanguageId == model.Language.Value);
    }
    if (model.Platform.HasValue)
    {
        apps = apps.Where(x => x.PlatformId == model.Platform.Value);
    }
    model.Apps = apps;
    ConfigureViewModel(model);
    return View(model);
}

private void ConfigureViewModel(AppsFilterVM model)
{
    // populate the selectlists
    var languages = _context.Languages;
    var platforms = _context.Platforms
    model.LanguageOptions = new SelectList(languages, "Id", "Name");
    model.PlatformOptions = new SelectList(platforms , "Id", "Name");
}

Then in the view (note its making a GET, not a POST)

@model.AppsFilterVM
....
@using (Html.BeginForm("FilterApps", "Apps", FormMethod.Get))
{
    @Html.LabelFor(m => m.Language)
    @Html.DropdownListFor(m => m.Language, Model.LanguageOptions, "No filter")
    @Html.ValidationMessageFor(m => m.Language) 
    @Html.LabelFor(m => m.Platform)
    @Html.DropdownListFor(m => m.Platform, Model.PlatformOptions, "No filter")
    @Html.ValidationMessageFor(m => m.Platform) 
    <input type="submit" value="Filter" />
}
@foreach (var group in Model.AgeGroups)
{
    ....

There a a few other thing you should not be doing. Your giving both <select> elements the same id attribute which is invalid html (the DropDownListFor() method already generates a unique id based on the property name).

You should not submit a form based on the change event of a <select> Not only is it unexpected behavior, if a user uses the keyboard to navigate through options (e.g. using the arrow keys, or typing a character to go to the first option starting with that letter, then the form will be immediately submitted. In addition, the user might select an option from the 2nd dropdownlist first, which would immediately post before they have a chance to select the option in the first one. Allow the user to make their selections, check them, and then submit the form when they choose to.

Your view should not contain linq queries, and your grouping and ordering should be done in the controller before you pass the model to the view. Your Apps property should in fact be a view model containing a property for the group name, and a collection property for the Apps, (similar to the view models in your previous question) so that the view is simply

@foreach(var group in Model.AgeGroups)
{
    @group.Name
    foreach (var app in group.Apps)
    {
        @app.ProductCode
        @app.Name
        @app.Platform
    }
}

You should also consider using ajax to submit your form, which would call separate server method that returns a partial view of just the Apps, and update the DOM in the success callback, which would improve performance. For an example, refer Rendering partial view on button click in ASP.NET MVC.

Sign up to request clarification or add additional context in comments.

10 Comments

Thanks a lot for your detailed explanation. However I'm having issues on few places. It's giving me (explicit conversion: missing cast error) apps = apps.Where(x => x.LanguageId == model.Language)
That make no sense - are you sure you used var apps = _context.Apps; previously (you do not add .ToList() as per the code in your question - the query returns a IQueryable<App>)
I tried with .ToList() and with heck of more options but for some reason, it is not working. pasteboard.co/GU33GVg.png
No, do use .ToList() What are the exact details of the error message?
You may need to use == model.Language.Value)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.