Pull to refresh

Comments 12

Сколько раз я сталкивался с проблемой доступа к отдельным свойствам модели, столько раз я видел решение на основе отображения полей.

А теперь несколько простых вопросов:
  1. Почему не учитываются права при Bindинге? — Мало не нарисовать контрол, нужно еще игнорировать данные, которые идут обратно.
  2. При платформенном биндинге будут срабатывать валидаторы, если модель биндится частично, то валидация превращается в нечто сверхсложное.
  3. Связаны ли отображение, биндинг и валидация? Или нужно поддерживать синхронное состояние этих частей в нескольких местах?
  4. Как проверять видимость/доступность полей исходя не только из прав, а например из этапа workflow?

Сколько у вас в среднем получилось вью на модель?
Да частичный биндинг не реализован, и поля, которые идут обратно не отбрасываются, и даже сохраняются в базу, все это конечно не правильно, и создает проблемы безопасности. Почему так? недостаток опыта и желание сделать побыстрее.
Валидация в основном клиентская и зависит в основном от типа полей, в тех местах, где валидация зависит от workflow приходится поддерживать в нескольких частях приложения.
Что касается четвертого пункта, то этап workflow на котором поле доступно для редактирования задается как раз аттрибутами, применение этих правил к конкретному этапу workflow идет в модели, в данном случае в базовой модели.
Спасибо за то, что указали на явные недостатки. Может быть, вы знакомы, с каким либо open source проектом, в котором все перечисленное красиво решено?
Валидация в основном клиентская
т.е. по факту ее нет, т.к. на сервере все проверки должны быть продублированы.
Признаюсь честно, думали о том что бы дублировать. Но наверное есть решение и получше, поэтому пока проект остается без серверной валидации.
Вот это уже звучит хреново совсем. Серверная валидация обязана присутствовать, в противном случае любой чуть знакомый с http поломает вам все, что только можно поломать.
Попробую кратко описать наш подход. Во-первых всегда нужно держать в голове полную цепочку:
  1. Пришел запрос на форму
  2. Сформировалась форма с учетом прав и WF и ушла пользователю
  3. Пользователь что-то сделал с формой (он может сделать что угодно — браузеры позволяют и поля добавить и скрипты отключить) и послал данные на сохранение
  4. Данные раскладываются с учетом прав и WF
  5. Происходит валидация и если все плохо, то нужно просто вернуть отрисованную форму обратно с сообщениями о проблемах
  6. Сохраняем модель и если все плохо, то нужно просто вернуть отрисованную форму обратно с сообщениями о проблемах
  7. Перенаправить пользователя на страницу исходя из сценария работы

На этапах 2, 4, 5 и 6 форма и правила обработки должны быть одинаковые.

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

Далее нужно разделить простейшую валидацию (формат даты, чисел, неотключаемая обязательность и т.п.) от комплексной валидации (когда правильность значения полей зависят друг от друга или иных внешних условий).

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

Такой единой точкой у нас сделана одна модель — коллекция элементов (назову ее FormModel, коротко — FM), которые представляют собой Expression к модели данных + атрибуты (наименование, параметры отрисовки и т.п.). Строится эта коллекция на основе информации в ModelMetadata и далее может быть модифицирована исходя из прав, WF или данных в моделе. Так же можно эту коллекцию собрать полностью руками.

Для этой модели есть один базовый шаблон, который понимает как разместить элементы коллекции (применяется размещение в сетке с соответствующими параметрами) + шаблоны на типы свойств (строка, число, дата, файл, HTML, enum и т.п.) — всего таких шаблонов чуть более 10. В особо тяжелых случаях можно нарисовать кастомный шаблон для этой коллекции, остальная кухня (видимость полей и т.п.) остается за пределами отрисовки HTML.

Есть несколько сценариев работы, применение конкретного сценария зависит от сложности задачи.

Первый — самый простой:
  1. В действии контроллера по его параметрам получаем модель данных
  2. Строим FM по ModelMetadata (элементы с видимостью, обязательностью, расположением и т.п. + кнопки, их может быть несколько)
  3. Возвращаем отрисованный стандартной view FM
  4. В действии сохранения контроллера по его параметрам получаем модель данных
  5. Строим FM по ModelMetadata
  6. Строим список полей, которые можно bindить
  7. Выполняем платформенный биндинг
  8. Есть ошибки? — добавляем сообщения и возвращаем отрисованный стандартной view FM
  9. Выполняем комплексную валидацию (у нас есть все данные — часть пришла при зачитке, часть из биндинга, но модель получилась полная)
  10. Есть ошибки? — добавляем сообщения и возвращаем отрисованный стандартной view FM
  11. Сохраняем модель данных
  12. Есть ошибки? — добавляем сообщения и возвращаем отрисованный стандартной view FM
  13. Редирект


Посложнее:
На втором шаге FM получаем из фабрики, при этом там уже задана функция комплексной проверки, остальное аналогично.

Самый сложный:
На втором шаге полностью готовую FM получаем из WF, который смотрит на модель данных и ее состояние + пользователя, который хочет что-то сделать.
Для сохранения на шаге 11 опять же передаем обработанную bindингом модель WF для решения, что же с ней делать.

Примерный код контроллера в простейшем случае:
private _dmf - Фабрика моделей данных, получаем из IoC

[HttpGet]
public ActionResult Default(...параметры...)
{
    return StandardView(_getFM(...параметры...));
}

[HttpPost]
public ActionResult Default(...параметры.., FormCollection form)
{
    var fm = _getFM(...параметры...);
    if (fm.Bind(ControllerContext, form)) // разложит данные, разложит ошибки по элементам, добавит сообщения
    {
        if(_dmf.TryPut(..., fm.Model, fm.Messages) // попытка сохранить, если есть ошибки, то будут положены в fm.Messages
        {
            return RedirectToAction(...); // Если все Ок
        }
    }
    return StandardView(fm); // Все плохо - выводим все, что есть и смогли разложить
}

private FormModel<TDataModel> _getFM(...параметры...)
{
    var dm = _dmf.Get(User, ...параметры...);
    var fm = new FormModel<TDataModel>();
    fm.Model = dm;
    fm.Validate = () => {....}; // <-- комплексная проверка dm
    fm.Buttons.Add(...);

    // Тут можно изменить элементы fm.Elements[..] - скрыть, показать, проставить обязательность, изменить расположение и т.п.

    return fm;
}


В сложных случаях метод _getFM(… параметры...) выносится в фабрику или в WF.

В ходе разработки было сказано очень много «тёплых» и «ласковых» слов в адрес разработчиков платформенных ModelMetadata и Bind — пришлось искать обходные пути для нормальной работы.

Как результат: для over 2000 классов моделей данных и 3000+ действий контроллеров сделано всего чуть более 10 базовых шаблонов и пара десятков кастомных. + хитрые пользователи не смогут сохранить то, что запрещено сохранять + контроллеры просты, основная логика в сервисах и WF, view не перегружены.
Так же такая схема позволила добавлять не только элементы — свойства модели, но и статику (просто HTML) и ActionResult в единое отображение.
Только ради вашего комментария, уже стоило опубликовать заметку
Сейчас не могу проверить работает ли это (вроде должно), но вместо вот этой жести
result = MvcHtmlString.Create(result.ToString().Replace("/>", "readonly = \"readonly\" />"));

напишите
result = html.EditorFor(expression,new { htmlAttributes = new { @readonly = "readonly" }});
Спасибо, поменял в статье, в моем случае рабочим вариантом оказался
html.TextBoxFor(expression, new { @readonly = "readonly" });
Погодите, погодите. Мы не может привязываться к textbox, а вдруг у нас radiobutton или checkbox или area. Нужен editor. Плюс мы еще забыли про htmlAttributes, которые идут параметром в функцию, если юзер их указывал в разметке, а мы на них забили? например
 @Html.RegistratorEditorFor(model => model.Department, new { @class = "form-control" } )


надо что-то вроде:
            var member = (expression.Body as MemberExpression).Member;
            if (html.ViewData.Model is BaseDocumentModel)
            {
                if (IsReadOnly(member as PropertyInfo, html.ViewData.Model as BaseDocumentModel))
                {
                    if (htmlAttributes == null)
                        htmlAttributes = new Dictionary<string, object>();
                    htmlAttributes.Add("readonly","readonly");
                    return html.EditorFor(expression,new { htmlAttributes = htmlAttributes });
                }
            }
            return html.EditorFor(expression,htmlAttributes);

у меня работает
Вам надо познакомиться с паттерном проектирования — «Авторизация на основе ролей» (RBAC). Вы просто неверно реализуете систему разграничения прав доступа — отсюда все ваши проблемы с «говнокодом».
Ну и, конечно, валидация на сервере должна быть.
А подскажите какие есть подходы или фреймворки RBAC для .NET MVC. Все что смотрел — не совсем универсальные. Хотелось, что бы авторизация была динамической.
Sign up to leave a comment.

Articles