Является ли хорошей практикой передача TempData и/или ViewData службам?

Пытаясь уменьшить размер моих контроллеров MVC, я реорганизую большую часть логики в сервисы (хотя эта же проблема возникла бы, если бы она учитывалась в моделях). Я часто обнаруживаю, что я напрямую устанавливал ViewData и/или TempData с информацией, которую я хотел отобразить пользователю, например:

var repoUser = new UserRepository();
var foundUser = repoUser.GetUser(userId);
if (foundUser == null) {
    ViewData["ErrorMessage"] = "Could not find user with ID {0}.".FormatWith(userId);
    return View("UsersRegister");
}

Конечно, как только вы перейдете в сервисный класс, вы потеряете прямой доступ к ViewData, TempData и таким методам, как View() или RedirectToAction(), поэтому я пытаюсь выяснить, как с этим справиться. На ум приходят два решения:

  1. Создайте класс, содержащий различную информацию о том, что должен делать контроллер, для передачи обратно от служб, например:

    public class ResponseInfo {
        public string Name { get; set; }
        public bool DoRedirect { get; set; }
        public object RedirectRouteValues { get; set; }
        public string InfoMessage { get; set; }
        public string ErrorMessage { get; set; }
    }
    
  2. Попробуйте и разрешите методам службы иметь прямой доступ к таким вещам, как ViewData и TempData, например:

    public ActionResult ToggleUserAttended(int eventId, int userId, HttpRequestBase request, TempDataDictionary tempData, ViewDataDictionary viewData, Func<string, object, ActionResult> redirectAction) {
        //...
        var repoUser = new UserRepository();
        var foundUser = repoUser.GetUser(userId);
        if (foundUser == null) {
            tempData["ErrorMessage"] = "Could not find user with ID {0}.".FormatWith(userId);
            return redirectAction("UsersRegister", new { id = eventId, returnUrl = request.QueryString["returnUrl"] });
        }
        //...
    }
    

    ... и затем в контроллере:

    return _svcEvent.ToggleUserAttended(123, 234, Request, TempData, ViewData, (name, routeVals) => RedirectToAction(name, routeVals));
    

Для номера 1 у контроллера будет больше логики, чтобы посмотреть на объект ResponseInfo и определить, следует ли перенаправить, отобразить представление, вставить сообщение об ошибке или информационное сообщение в TempData или ViewData и т. д. Номер 2 в значительной степени позволит одно- контроллер лайнера, но вы делаете службу очень осведомленной о вещах, специфичных для контроллера. Тем не менее, служба всегда будет тесно связана с контроллером, так что это проблема? Будет ли 1 или 2 лучшей практикой или что-то еще, чего я не указал?


person Jez    schedule 19.03.2013    source источник
comment
БОГ НЕТ. Переместите такие вещи в ActionFilters... Только не делайте этого, пожалуйста, не позволяйте своим службам получать доступ к интерфейсным концепциям, таким как ViewData и TempData..   -  person Simon Whitehead    schedule 19.03.2013
comment
@SimonWhitehead Так вы в основном говорите оставить это в контроллере? А сами сообщения об ошибках? Был бы признателен за более полный ответ.   -  person Jez    schedule 19.03.2013
comment
Есть ли причина, по которой вы не рассматривали возможность использования ModelState для отображения этих ошибок? У вас может быть карта уровня пользовательского интерфейса между списками ошибок уровня сервиса и файлом ModelStateDictionary. Это отделяет вашу службу от вашего пользовательского интерфейса. Вы даже можете написать свой собственный ActionResult, чтобы он возвращал все это красиво и значительно уменьшал размер ваших действий.. Сейчас у меня нет рядом ПК, чтобы напечатать пример.   -  person Simon Whitehead    schedule 19.03.2013
comment
Я никогда не слышал о ModelState и его документации кажется крайне неадекватным.   -  person Jez    schedule 19.03.2013
comment
Здесь: msdn .microsoft.com/en-au/library/. Прочитайте это .. посмотрите, что вы думаете. Если вы укажете пустой ключ для ModelStateDictionary, он не будет привязан к определенному свойству объекта... но все равно будет отображаться в ValidationSummary.   -  person Simon Whitehead    schedule 19.03.2013
comment
Все еще не так много общего описания использования ModelState.   -  person Jez    schedule 19.03.2013
comment
Одна проблема с ModelState заключается в том, что он, по-видимому, относится к состоянию модели, используемой визуализируемым «родительским» представлением. Я использую несколько форм в одном и том же представлении и визуализирую их, используя частичные, чтобы я мог передать другую модель для этой формы. Таким образом, ошибки для этих мини-форм, по-видимому, не подходят для ModelState для представления, содержащего партиалы.   -  person Jez    schedule 19.03.2013


Ответы (1)


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

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

Например, служба должна выдать исключение, если не может найти пользователя, а контроллер должен затем обработать эту ошибку с помощью соответствующего механизма пользовательского интерфейса: сообщения об ошибке, ошибки HTTP или чего-то еще, что подходит для вашего приложения. Точно так же, как служба может знать, куда перенаправлять? Он в курсе GUI? Как вы планируете использовать его в других контекстах (API, не-веб-интерфейс и т. д.)?

Что я обычно делаю, так это создаю команду /dto на основе формы/параметров и предоставляю ее сервису. Затем любой клиент службы может использовать ту же команду. Если мне нужно показать данные, я запрашиваю у сервисов/репозиториев все, что мне нужно, сопоставляю это с формой представления (если необходимо) и помещаю это в ViewData/TempData/Model/Whatever.

Вот несколько примеров (рассматривайте это как псевдокод):

[HttpPost]
public ActionResult ChangeUserInfo(UserInfoModel model)
{
  var cmd = new ChangeUserInfoCommand(CurrentUserId, model.FirstName, model.LastName);
  try {
      userSvc.UpdateUser(cmd); 
      return RedirectToAction(...);
  }
  catch(XxxxException e) { // e.g. something wrong (business logic)
      TempData["Error"] = "an error occurred: " + e.FriendlyError();
      return RedirectToAction(...); // error action
  }
  catch(SecurityException e) {
      throw new HttpException(404, "NotFound");
  }
}

public ActionResult ShowUsers()
{
  var userInfo = svc.GetUsers().Select(u => { Id = u.id, FullName = u.First + " " + u.Last);
  // or var userInfo = svc.GetUsers().Select(u => Mapper.To<UserDTO>(u));
  return View(userInfo);

  // or without model
  // ViewData["users"] = userInfo;
  // return View();
}

Обратите внимание, что это иллюстрация — я обычно делаю это совсем по-другому, но у меня есть много другой инфраструктуры (например, я не обрабатываю исключения, подобные этому, мои службы иногда имеют только один метод Execute(IEnumerable<Command> cmds), у меня есть хорошая поддержка для анонимных классов в качестве моделей просмотра и т. д.)

person Zdeslav Vojkovic    schedule 19.03.2013
comment
Не могли бы вы привести пример команды /dto на основе формы/параметров? - person Jez; 19.03.2013
comment
конечно, просто имейте в виду, что это иллюстрация — я обычно делаю это совсем по-другому, но у меня есть много другой инфраструктуры - person Zdeslav Vojkovic; 19.03.2013
comment
Хм, значит, вы на 100% полагаетесь на исключения, чтобы сообщить, что в службе произошла ошибка? Это тоже не очень хорошая практика; некоторые ошибки настолько распространены, что они достаточно предсказуемы, чтобы не нуждаться в исключении. - person Jez; 19.03.2013
comment
не уверен, что понял последний комментарий. Если нет исключения, как вы обозначаете ошибку? с кодом ошибки? Это предсказуемое звучит подозрительно похоже на ожидаемое событие и на самом деле не является ошибкой. - person Zdeslav Vojkovic; 19.03.2013
comment
Обычно я возвращаю bool, чтобы указать на ошибку, и беру параметр out string errorMsg. - person Jez; 19.03.2013
comment
Я тоже считаю, что это очень плохая идея. Еще несколько подробностей в этом ответе: stackoverflow.com/questions/15296993/. Эти ребята, похоже, согласны: msdn. microsoft.com/en-us/library/vstudio/ - person Zdeslav Vojkovic; 19.03.2013
comment
@ZdeslavVojkovic Обычно это приемлемо, если вы дважды проверяете те же бизнес-правила, что и аннотации данных внешнего интерфейса. Если проверка внешнего интерфейса что-то упускает, а сервисный уровень обнаруживает, что это недействительно, то это исключительно. Я бы не сказал, что несуществующий пользователь — это что-то исключительное. - person Simon Whitehead; 19.03.2013
comment
@SimonWhitehead, ну, отсутствие текущего пользователя — это исключение, IMO :) Поскольку служба не знает о текущих пользователях, она обрабатывает их всех одинаково, поэтому выбрасывает. Кроме того, используя любой обычный путь к приложению, вы не сможете обновить несуществующего пользователя (за исключением случаев одновременного удаления пользователя, где мне также нравится иметь исключение). Кроме того, я редко обрабатываю ValidationException, SecurityException и подобные распространенные ошибки явно, как это делается фильтрами/перехватчиками/чем угодно. Кроме того, что, если вы забудете проверить возвращаемое значение? - person Zdeslav Vojkovic; 19.03.2013