Публикация коллекции и ModelState

У меня есть проблема в моем приложении MVC, которую я не знаю, как решить, или я делаю это неправильно.

У меня есть контроллер/представление, которое отображает список элементов в сетке с флажком, и когда элементы отправляются в мой контроллер, я хотел бы удалить строки из моей базы данных на основе переданных идентификаторов.

Вид выглядит примерно так:

@for(int index = 0; index < Model.Items.Length; index++)
{
    <td>
        @Html.HiddenFor(m => m[index].Id)
        @Html.CheckBoxFor(m => m[index].Delete)
    </td>
}

Мой контроллер принимает значения:

[HttpPost]
public ActionResult Delete(DeleteItemsModel model)
{
    if( !ModelState.IsValid )
    {
        // ...
    }

    foreach( var id in model.Items.Where(i => i.Delete))
        repo.Delete(id);
}

Этот сценарий работает нормально. Элементы публикуются правильно с идентификатором и флагом для удаления или нет, и они удаляются правильно. Проблема, с которой я сталкиваюсь, заключается в том, что моя страница не проходит проверку. Мне нужно снова получить элементы из базы данных и отправить данные обратно в представление:

if( !ModelState.IsValid )
{
    var order = repo.GetOrder(id);

    // map
    return View(Mapper.Map<Order, OrderModel>(order));
}

За время между получением пользователем списка элементов для удаления и нажатием кнопки «Отправить» возможно добавление новых элементов. Теперь, когда я извлекаю данные и отправляю их обратно в представление, в списке могут быть новые элементы.

Пример проблемы:
я выполняю HTTP-запрос GET на своей странице, и в моей сетке есть два элемента с идентификаторами 2 и 1. Я выбираю первую строку (идентификатор 2, отсортирован по самым последним), а затем нажимаю "Отправить". . Проверка на странице не удалась, и я возвращаю представление пользователю. Теперь в сетке три строки (3, 2, 1). MVC будет иметь флажок на ПЕРВОМ элементе (теперь с идентификатором 3). Если пользователь не проверит эти данные, он может удалить что-то не то.

Любые идеи о том, как исправить этот сценарий или что мне делать вместо этого? Кто-нибудь знает, как


person Dismissile    schedule 18.06.2012    source источник
comment
См. Stack Overflow не нуждается в ваших навыках SEO.   -  person John Saunders    schedule 18.06.2012
comment
@JohnSaunders Я запомню это на будущее.   -  person Dismissile    schedule 19.06.2012
comment
В подходе есть проблема, вы не должны получать элементы из базы данных при сбое проверки модели. Если вам как-то придется, то вам нужно перестроить свою модель представления.   -  person Yusubov    schedule 19.06.2012
comment
@ElYusubov Что делать, если данные устарели? Все равно обновить?   -  person Dismissile    schedule 19.06.2012
comment
Проблема с отсутствием получения элементов из базы данных при сбое проверки заключается в том, что я не отправляю каждый отдельный фрагмент данных в контроллер, поэтому мне пришлось бы либо начать отправку данных, что было бы неэффективно.   -  person Dismissile    schedule 19.06.2012
comment
Если вы публикуете частично (по частям), рассмотрите вызовы ajax и дочерние действия. Это должно помочь.   -  person Yusubov    schedule 19.06.2012


Ответы (2)


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

Предположим следующую модель:

public class MyViewModel
{
    public int Id { get; set; }
    public bool Delete { get; set; }
}

следующий контроллер:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        // Initially we have 2 items in the database
        var model = new[]
        {
            new MyViewModel { Id = 2 },
            new MyViewModel { Id = 1 }
        };
        return View(model);
    }

    [HttpDelete]
    public ActionResult Index(MyViewModel[] model)
    {
        // simulate a validation error
        ModelState.AddModelError("", "some error occured");

        if (!ModelState.IsValid)
        {
            // We refetch the items from the database except that
            // a new item was added in the beginning by some other user
            // in between
            var newModel = new[]
            {
                new MyViewModel { Id = 3 },
                new MyViewModel { Id = 2 },
                new MyViewModel { Id = 1 }
            };

            return View(newModel);
        }

        // TODO: here we do the actual delete

        return RedirectToAction("Index");
    }
}

и вид:

@model MyViewModel[]

@Html.ValidationSummary()

@using (Html.BeginForm())
{
    @Html.HttpMethodOverride(HttpVerbs.Delete)
    for (int i = 0; i < Model.Length; i++)
    {
        <div>
            @Html.HiddenFor(m => m[i].Id)
            @Html.CheckBoxFor(m => m[i].Delete)
            @Model[i].Id
        </div>
    }
    <button type="submit">Delete</button>
}

Вот что произойдет:

Пользователь переходит к действию Index, выбирает первый элемент для удаления и нажимает кнопку «Удалить». Вот как выглядит представление перед отправкой формы:

введите здесь описание изображения

Вызывается действие «Удалить», и когда представление отображается еще раз (из-за ошибки проверки), пользователю предоставляется следующее:

введите здесь описание изображения

Видите, как предварительно выбран неправильный элемент?

Почему это происходит? Поскольку помощники HTML используют значение ModelState в приоритете при привязке вместо значения модели, и это предусмотрено дизайном.

Итак, как решить эту проблему? Прочитав следующий пост в блоге Фила Хаака: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx

В своем блоге он рассказывает о непоследовательных индексах и приводит следующий пример:

<form method="post" action="/Home/Create">

    <input type="hidden" name="products.Index" value="cold" />
    <input type="text" name="products[cold].Name" value="Beer" />
    <input type="text" name="products[cold].Price" value="7.32" />

    <input type="hidden" name="products.Index" value="123" />
    <input type="text" name="products[123].Name" value="Chips" />
    <input type="text" name="products[123].Price" value="2.23" />

    <input type="hidden" name="products.Index" value="caliente" />
    <input type="text" name="products[caliente].Name" value="Salsa" />
    <input type="text" name="products[caliente].Price" value="1.23" />

    <input type="submit" />
</form>

Видите, как мы больше не используем инкрементные индексы для имен кнопок ввода?

Как мы применим это к нашему примеру?

Как это:

@model MyViewModel[]
@Html.ValidationSummary()
@using (Html.BeginForm())
{
    @Html.HttpMethodOverride(HttpVerbs.Delete)
    for (int i = 0; i < Model.Length; i++)
    {
        <div>
            @Html.Hidden("index", Model[i].Id)
            @Html.Hidden("[" + Model[i].Id + "].Id", Model[i].Id)
            @Html.CheckBox("[" + Model[i].Id + "].Delete", Model[i].Delete)
            @Model[i].Id
        </div>
    }
    <button type="submit">Delete</button>
}

Теперь проблема устранена. Или это? Вы видели тот ужасный беспорядок, который сейчас представляет вид? Мы исправили одну проблему, но добавили в представление нечто совершенно отвратительное. Не знаю, как вас, а когда я смотрю на это, меня тошнит.

Так что же можно было сделать? Мы должны прочитать сообщение в блоге Стивена Сандерсона: http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/, в котором он представляет очень интересный пользовательский помощник Html.BeginCollectionItem, который используется следующим образом:

<div class="editorRow">
    <% using(Html.BeginCollectionItem("gifts")) { %>
        Item: <%= Html.TextBoxFor(x => x.Name) %>
        Value: $<%= Html.TextBoxFor(x => x.Price, new { size = 4 }) %>
    <% } %>
</div>

Заметили, как элементы формы упакованы в этот помощник?

Что делает этот помощник? Он заменяет последовательные индексы, сгенерированные строго типизированными помощниками, на Guids и использует дополнительное скрытое поле для установки этого индекса на каждой итерации.


При этом проблема проявляется только в том случае, если вам нужно получить свежие данные из вашей базы данных в действии «Удалить». Если вы полагаетесь на связыватель модели для регидратации, вообще не будет никаких проблем (за исключением того, что если есть ошибка модели, вы покажете представление со старыми данными -> что, вероятно, не так уж проблематично):

[HttpDelete]
public ActionResult Index(MyViewModel[] model)
{
    // simulate a validation error
    ModelState.AddModelError("", "some error occured");

    if (!ModelState.IsValid)
    {

        return View(model);
    }

    // TODO: here we do the actual delete

    return RedirectToAction("Index");
}
person Darin Dimitrov    schedule 19.06.2012
comment
Благодарю вас! Вы точно поняли мою проблему, и это отличное решение. Я думал, что объяснил это достаточно, но мне, вероятно, нужно поработать над формулировкой моих вопросов немного лучше. - person Dismissile; 20.06.2012

Распространенным решением этой проблемы является использование шаблона Post-Redirect-Get.

Вы можете найти объяснение с примером кода для MVC здесь (вместе с кучей других полезных советов по MVC). Прокрутите вниз до пункта 13 в списке для объяснения PRG.

person Morten Mertner    schedule 18.06.2012
comment
Это хорошо, но не решает проблему полностью. Если добавляется новый элемент, он будет вверху списка. ModelState для элемента с индексом 0 был проверен, и теперь будет проверен новый элемент. - person Dismissile; 19.06.2012
comment
Я не уверен, что понимаю. Если вы не вносили никаких изменений, перенаправьте и повторно отобразите неизмененную форму. Если вы вносите изменения, перенаправьте и отобразите измененные данные. Судя по вашему комментарию, вы сохраняете некоторое состояние после последнего запроса на проверку элементов, а не извлекаете информацию из своего хранилища данных или серверной части, что вам, вероятно, следует делать. Если это не поможет, рассмотрите возможность публикации нового вопроса с кодом, чтобы показать, что вы делаете сейчас. - person Morten Mertner; 19.06.2012
comment
Только что наткнулся на этот пост - думаю, он поможет вам решить проблему. См. jefclaes.be/2012/06/persisting. -модель-состояние-при-использовании-prg.html - person Morten Mertner; 19.06.2012
comment
Сохраняющееся состояние модели является проблемой. Прочитайте вопрос еще раз, пожалуйста. У меня есть две строки в моей таблице. Я нажимаю первый флажок в первой строке. Проверка не проходит. Я перенаправляю обратно на GET и сохраняю состояние модели в моем TempData. База данных извлекает данные, и теперь есть 3 элемента вместо двух... и последний добавленный элемент в базу данных теперь находится в первой строке. Первый элемент в таблице теперь имеет флажок вместо правильного. - person Dismissile; 19.06.2012
comment
Проблема вот в чем: ModelState будет иметь примерно такую ​​запись: Items[0].Delete=true. 0 - это индекс. Если страница перезагружается и ДРУГОЙ элемент теперь находится в индексе 0, то это ModelState теперь в основном неверно, потому что оно ссылается на другой элемент, чем тот, который был отправлен в первый раз. - person Dismissile; 19.06.2012
comment
Остановитесь и задумайтесь на мгновение. Если индекс не является постоянным для элементов (которые вы определили), то, возможно, вам не следует его использовать? У предметов есть что-то еще, что вы могли бы использовать? Я вижу свойство Id; может быть, вам следует использовать это для поиска элементов вместо индекса? Пауза и размышление помогают, попробуйте ;) - person Morten Mertner; 19.06.2012
comment
Я не использую индексы, чтобы определить, что есть что. ASP.net MVC требует, чтобы имена переменных формы были в определенном формате для публикации коллекции. Проблема в том, что когда я повторно отображаю представление для пользователя, порядок вещей меняется, и MVC использует состояние модели для построения представления. - person Dismissile; 19.06.2012