Построение списка выбора с группами OptGroup

Текущий проект:

  • ASP.NET 4.5.2
  • MVC 5

Я пытаюсь построить меню выбора с OptGroups из модели, но моя проблема в том, что я не могу построить OptGroups самих себя.

Моя модель:

[DisplayName("City")]
public string CityId { get; set; }
private IList<SelectListItem> _CityName;
public IList<SelectListItem> CityName {
  get {
    List<SelectListItem> list = new List<SelectListItem>();
    Dictionary<Guid, SelectListGroup> groups = new Dictionary<Guid, SelectListGroup>();
    List<Region> region = db.Region.Where(x => x.Active == true).OrderBy(x => x.RegionName).ToList();
    foreach(Region item in region) {
      groups.Add(item.RegionId, new SelectListGroup() { Name = item.RegionName });
    }
    List<City> city = db.City.Where(x => x.Active == true).ToList();
    foreach(City item in city) {
      list.Add(new SelectListItem() { Text = item.CityName, Value = item.CityId.ToString(), Group = groups[item.RegionId] });
    }
    return list;
  }
  set { _CityName = value; }
}

Каждый город может быть в регионе. Я хочу выбрать меню для группировки городов по регионам. Из всего, что я могу понять, код выше должен делать эту работу, но вместо этого я получаю раскрывающееся меню со всеми городами, сгруппированными под OptGroup с именем System.Web.Mvc.SelectListGroup

Ключевым моментом в приведенном выше коде является то, что я сначала перебираю регионы и помещаю их в словарь с RegionId установить, чтобы быть ключом, который возвращает RegionName (который сам отформатирован как SelectListGroup).

Затем я перебираю города и назначаю каждому городу группу, соответствующую городу. RegionId,

Я не видел ни одного примера в Интернете, который фактически извлекал бы контент из базы данных - 100% всех примеров используют жестко запрограммированные SelectListGroup а также SelectListItem ценности.

Мое мнение тоже правильно, AFAIK:

@Html.DropDownListFor(x => x.CityId, new SelectList(Model.CityName, "Value", "Text", "Group", 1), "« ‹ Select › »", htmlAttributes: new { @class = "form-control" })

Как видите, Группа должна быть введена в SelectList и DropDownList создается с OptGroups просто не правильные.

Полученное в результате выпадающее меню выглядит примерно так:

« ‹ Select › »
System.Web.Mvc.SelectListGroup
  City1
  City2
  ...
  LastCity

Когда это должно быть так:

« ‹ Select › »
Region1
  City2
  City4
  City5
Region2
  City3
  City1
  City6

Предложения?


Модифицированное решение: я следовал решению, предоставленному Stephen Muecke, но немного его изменил.

Одним из общих правил MVC является то, что у вас есть модель, которая тяжелее контроллера, и эта модель определяет вашу бизнес-логику. Стивен утверждает, что весь доступ к базе данных должен осуществляться в контроллере. Я пришел, чтобы согласиться с обоими.

Одна из моих самых больших "проблем" заключается в том, что любое создание выпадающего меню или любого другого предварительно заполненного элемента формы необходимо вызывать каждый раз, когда вызывается страница. Это означает, что для страницы создания или редактирования необходимо вызывать ее не только в методе [HttpGet], но также в методе [HttpPost], где модель отправляется обратно в представление, поскольку она не прошла надлежащую проверку. Это означает, что вы должны добавить код (традиционно через ViewBags) к каждому методу, просто чтобы предварительно заполнить элементы, такие как раскрывающиеся списки. Это называется дублированием кода и не является хорошей вещью. Должен быть лучший путь, и благодаря руководству Стивена я нашел его.

Проблема с сохранением доступа к данным за пределами модели заключается в том, что вам необходимо заполнить модель данными. Проблема, заключающаяся в том, чтобы избежать повторного использования кода и избежать возможных ошибок, заключается в том, что вам не следует связывать данные с элементами контроллера. Это последнее действие является бизнес-логикой и по праву принадлежит модели. Бизнес-логика в моем случае заключается в том, что мне нужно ограничить ввод пользователя списком городов, сгруппированных по регионам, которые администратор может выбрать из раскрывающегося списка. Поэтому, хотя мы можем собирать данные в контроллере, мы привязываем их к модели в модели. Раньше моя ошибка заключалась в том, чтобы делать обе модели, что было совершенно неуместно.

Связывая данные с моделью в модели, мы избегаем необходимости связывать их дважды - по одному в каждом из методов [HttpGet] и [HttpPost] контроллера. Мы должны связать его только один раз в модели, которая обрабатывается обоими методами. И если у нас есть более общая модель, которая может быть разделена между функциями Create и Edit, мы можем сделать это связывание только в одном месте вместо четырех (но у меня нет такой степени универсальности, поэтому я не буду давать это как пример)

Итак, для начала мы фактически удалим всю сборку данных и поместим ее в свой собственный класс:

public class SelectLists {
  public static IEnumerable<SelectListItem> CityNameList() {
    ApplicationDbContext db = new ApplicationDbContext();
    List<City> items = db.City.Where(x => x.Active == true).OrderBy(x => x.Region.RegionName).ThenBy(x => x.CityName).ToList();
    return new SelectList(items, "CityId", "CityName", "Region.RegionName", 1);
  }
}

Это существует в пространстве имен, но под контроллером раздела, с которым мы имеем дело. Для ясности, я вставил его в самый конец файла, перед закрытием пространства имен.

Затем мы смотрим на модель для этой страницы:

public string CityId { get; set; }
private IEnumerable<SelectListItem> _CityName;
public IEnumerable<SelectListItem> CityName {
  get { return SelectLists.CityNameList(); }
  set { _CityName = value; }
}

Примечание: хотя CityId это Guid и поле БД является uniqueidentifier Я ввожу это значение в виде строки через представление, потому что проверка на стороне клиента сосет ослиные шары для Guids. Гораздо проще выполнить проверку на стороне клиента в раскрывающемся меню, если Value обрабатывается как строка вместо Guid. Вы просто конвертируете его обратно в Guid, прежде чем вставить его обратно в мастер-модель для этого стола. Кроме того, CityName не является действительным полем в таблице City - оно существует исключительно как заполнитель для самого раскрывающегося меню, поэтому оно существует в CreateClientViewModel для страницы создания. Таким образом, в представлении мы можем создать DropDownListFor это явно связывает CityId с выпадающим меню, фактически позволяя проверку на стороне клиента в первую очередь (направляющие - просто дополнительная головная боль).

Ключевым моментом является get {}, Как видите, больше нет обильного кода, который делает доступ к БД, просто SelectLists который нацелен на класс и вызов метода CityNameList(), Вы даже можете передавать переменные в метод, чтобы один и тот же метод мог возвращать различные варианты одного и того же выпадающего меню. Скажем, если вы хотите, чтобы в одном раскрывающемся списке на одной странице (Создать) его параметры были сгруппированы по OptGroups, а в другом раскрывающемся меню (Правка) не должно быть группировок параметров.

Фактическая модель оказывается еще проще, чем раньше:

@Html.DropDownListFor(x => x.CityId, Model.CityName, "« ‹ Select › »", htmlAttributes: new { @class = "form-control" })

Нет необходимости изменять элемент, который вводит данные раскрывающегося списка - вы просто вызываете его через Model.ElementName,

Надеюсь, это поможет.

2 ответа

Решение

Во-первых, просматриваемая модель не должна содержать код доступа к базе данных, чтобы заполнить ее свойства. Это ответственность контроллера, и вы сделали свой код невозможным для модульного тестирования. Начните с изменения модели на

public class CreateClientViewModel
{
    [DisplayName("City")]
    public string CityId { get; set; }
    public IList<SelectListItem> CityList { get; set; }
    ....
}

Затем в контроллере вы можете использовать одну из перегрузок SelectList который принимает groupName создать коллекцию

var cities = var cities = db.City.Include(x => x.Region).Where(x => x.Active == true)
    .OrderBy(x => x.Region.RegionName).ThenBy(x => x.CityName);

var model = new CreateClientViewModel()
{
    CityList = new SelectList(cities, "CityId", "CityName", "Region.RegionName", null, null)
};
return View(model);

И в представлении

@Html.DropDownListFor(x => x.CityId, Model.CityList , "« ‹ Select › »", new { @class = "form-control" })

В качестве альтернативы, вы также можете сделать это, используя Group собственностью SelectListItem

var model = new CreateClientViewModel()
{
    CityList = new List<SelectListItem> // or initialize this in the constructor
};
var cities = var cities = db.City.Include(x => x.Region).Where(x => x.Active == true).GroupBy(x => x.Region.RegionName);
foreach(var regionGroup in cities)
{
    var optionGroup = new SelectListGroup() { Name = regionGroup.Key };
    foreach(var city in regionGroup)
    {
        model.CityList.Add(new SelectListItem() { Value = city.CityId.ToString(), Text = city.CityName, Group = optionGroup });
    }
}
return View(model);

Я использовал эту блестящую библиотеку: DropDownList Optgroup MVC 1.0.0

Nuget link

Вот как это использовать:

  1. Подготовьте свой список:

    var ethnicityData = new List<GroupedSelectListItem> {
            new GroupedSelectListItem() { Value="British", Text ="British", GroupName = "White", GroupKey="1"},
            new GroupedSelectListItem() { Value="Irish", Text ="Irish", GroupName = "White", GroupKey="1"},
            new GroupedSelectListItem() { Value="White Other", Text ="White Other", GroupName = "White", GroupKey="1"},
            new GroupedSelectListItem() { Value="Other Ethin Group", Text ="Other Ethin Group", GroupName = "Other Ethnic Group", GroupKey="2"},
            new GroupedSelectListItem() { Value="Chinese", Text ="Chinese", GroupName = "Other Ethnic Group", GroupKey="2"},
            new GroupedSelectListItem() { Value="Other mixed background", Text ="Other mixed background", GroupName = "Mixed", GroupKey="4"},
            new GroupedSelectListItem() { Value="White and Asian", Text ="White and Asian", GroupName = "Mixed", GroupKey="4"},
            new GroupedSelectListItem() { Value="White and Black African", Text ="White and Black African", GroupName = "Mixed", GroupKey="4"},
            new GroupedSelectListItem() { Value="White and Black Caribbean", Text ="White and Black Caribbean", GroupName = "Mixed", GroupKey="4"},
            new GroupedSelectListItem() { Value="Other Black background", Text ="Other Black background", GroupName = "Black or Black British", GroupKey="5"},
            new GroupedSelectListItem() { Value="Caribbean", Text ="Caribbean", GroupName = "Black or Black British", GroupKey="5"},
            new GroupedSelectListItem() { Value="African", Text ="African", GroupName = "Black or Black British", GroupKey="5"},
            new GroupedSelectListItem() { Value="Bangladeshi", Text ="Bangladeshi", GroupName = "Asian or Asian British", GroupKey="6"},
            new GroupedSelectListItem() { Value="Other Asian background", Text ="Other Asian background", GroupName = "Asian or Asian British", GroupKey="6"},
            new GroupedSelectListItem() { Value="Indian", Text ="Indian", GroupName = "Asian or Asian British", GroupKey="6"},
            new GroupedSelectListItem() { Value="Pakistani", Text ="Pakistani", GroupName = "Asian or Asian British", GroupKey="6"},
            new GroupedSelectListItem() { Value="Not Stated", Text ="Not Stated", GroupName = "Not Stated", GroupKey="3"}
    

    };

  2. Используйте это так:

    @ Html.DropDownGroupListFor (model => model.Ethnicity, ethityData, "", new { @class = "form-control" })

Другие вопросы по тегам