ViewModel Best Practices

238

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

В основном у меня были следующие вопросы:

  1. Мне обычно нравится иметь один класс / файл. Имеет ли это смысл с ViewModel, если он создается только для передачи данных из контроллера в представление?
  2. Если ViewModel принадлежит в своем собственном файле, и вы используете структуру каталогов / проектов, чтобы отделить вещи, к чему относится файл ViewModel ? В каталоге контроллеров ?

Это в основном это пока. У меня может появиться еще несколько вопросов, но это беспокоит меня в течение последнего часа или около того, и я могу найти последовательное руководство в другом месте.

РЕДАКТИРОВАТЬ: Глядя на пример приложения NerdDinner на CodePlex, кажется, что ViewModels являются частью контроллеров , но мне все еще неудобно, что они не находятся в своих собственных файлах.

jerhinesmith
источник
66
Я бы точно не назвал NerdDinner примером «Best Practices». Ваша интуиция служит вам хорошо. :)
Райан Монтгомери

Ответы:

211

Я создаю то, что я называю «ViewModel» для каждого представления. Я поместил их в папку ViewModels в моем веб-проекте MVC. Я называю их в честь контроллера и действия (или представления), которые они представляют. Поэтому, если мне нужно передать данные в представление SignUp на контроллере Membership, я создаю класс MembershipSignUpViewModel.cs и помещаю его в папку ViewModels.

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

Это также хорошо работает для составных моделей представления, которые содержат свойства, относящиеся к типу других моделей представления. Например, если у вас есть 5 виджетов на странице индекса в контроллере членства, и вы создали ViewModel для каждого частичного представления - как вы передаете данные из действия Index в партиалы? Вы добавляете свойство в MembershipIndexViewModel типа MyPartialViewModel и при рендеринге частичного вы передаете в Model.MyPartialViewModel.

Делая это таким образом, вы можете настроить частичные свойства ViewModel без необходимости вообще менять представление индекса. Он по-прежнему просто передается в Model.MyPartialViewModel, поэтому меньше шансов, что вам придется пройти через всю цепочку партиалов, чтобы что-то исправить, когда все, что вы делаете, это добавление свойства в частичную ViewModel.

Я также добавлю пространство имен «MyProject.Web.ViewModels» в web.config, чтобы позволить мне ссылаться на них в любом представлении, даже не добавляя явного оператора импорта в каждое представление. Просто делает его немного чище.

Райан Монтгомери
источник
3
Что если вы хотите выполнить POST из частичного представления и вернуть весь просмотр (в случае ошибки модели)? В частичном представлении у вас нет доступа к родительской модели.
Cosmo
5
@Cosmo: Затем POST к действию, которое может вернуть весь вид в случае ошибки модели. На стороне сервера у вас достаточно для воссоздания родительской модели.
Томас Ашан
А как насчет действий входа [POST] и входа [GET]? с разными моделями представления?
Барт Каликсто
Обычно, login [GET] не вызывает ViewModel, потому что не нужно загружать какие-либо данные.
Андре Фигейредо
Отличный совет. Куда должны идти доступ к данным, обработка и настройка свойств модели / ВМ? В моем случае у нас будут некоторые данные, поступающие из локальной базы данных CMS, а некоторые - из веб-сервисов, которые необходимо будет обработать / обработать перед установкой в ​​модель. Помещение всего этого в контроллер становится довольно грязным.
xr280xr
124

Разделять классы по категориям (Controllers, ViewModels, Filters и т. Д.) - нонсенс.

Если вы хотите написать код для раздела Home на своем веб-сайте (/), создайте папку с именем Home и поместите туда HomeController, IndexViewModel, AboutViewModel и т. Д. И все связанные классы, используемые действиями Home.

Если у вас есть общие классы, такие как ApplicationController, вы можете поместить их в корень вашего проекта.

Зачем разделять связанные вещи (HomeController, IndexViewModel) и хранить вещи, которые вообще не имеют отношения (HomeController, AccountController)?


Я написал сообщение в блоге на эту тему.

Макс Торо
источник
13
Если ты сделаешь это, все станет довольно быстро.
UpTheCreek,
14
Нет, беспорядочно размещать все контроллеры в одном пространстве имен / директорий. Если у вас есть 5 контроллеров, каждый из которых использует 5 моделей просмотра, то у вас есть 25 моделей просмотра. Пространства имен - это механизм организации кода, и здесь не должно быть никаких различий.
Макс Торо
41
@ Макс Торо: удивился, что тебя так сильно понизили. Спустя некоторое время, работая над ASP.Net MVC, я испытываю большую боль от того, что все ViewModel находятся в одном месте, все контроллеры в другом, а все View - в другом. MVC является трио связанных частей, они будут соединены - они поддерживают друг друга. Я чувствую, что решение может быть гораздо более организованным, если Controller, ViewModels и Views для данного раздела живут вместе в одном каталоге. MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml и т. Д.
quentin-starin
13
@RyanJMcGowan разделение интересов не является разделением классов.
Макс Торо
12
@RyanJMcGowan, независимо от того, как вы подходите к разработке, проблема в том, что вы получите, особенно для больших приложений. Находясь в режиме обслуживания, вы не думаете обо всех моделях, а о всех контроллерах, вы добавляете одну функцию за раз.
Макс Торо
21

Я храню свои классы приложений в подпапке «Core» (или отдельной библиотеке классов) и использую те же методы, что и в примере приложения KIGG, но с некоторыми небольшими изменениями, чтобы сделать мои приложения более СУХИМЫМИ.

Я создаю класс BaseViewData в / Core / ViewData /, где храню общие свойства для всего сайта.

После этого я также создаю все мои классы ViewData view в той же папке, которые затем наследуются от BaseViewData и имеют специфические свойства вида.

Затем я создаю ApplicationController, от которого происходят все мои контроллеры. ApplicationController имеет общий метод GetViewData следующим образом:

protected T GetViewData<T>() where T : BaseViewData, new()
    {
        var viewData = new T
        {
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        };
        return viewData;
    }

Наконец, в своем действии контроллера я делаю следующее, чтобы построить мою модель ViewData

public ActionResult Index(int? id)
    {
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    }

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

отметка
источник
13

Класс ViewModel предназначен для инкапсуляции нескольких фрагментов данных, представленных экземплярами классов, в один простой в управлении объект, который вы можете передать в свой View.

Было бы целесообразно иметь ваши классы ViewModel в своих собственных файлах, в своем собственном каталоге. В моих проектах у меня есть подпапка папки Models, которая называется ViewModels. Вот где живут мои ViewModels (например ProductViewModel.cs).

JMS
источник
13

Нет подходящего места для хранения ваших моделей. Вы можете хранить их в отдельной сборке, если проект большой и в нем много ViewModels (объектов передачи данных). Также вы можете хранить их в отдельной папке проекта сайта. Например, в Oxite они размещены в проекте Oxite, который также содержит множество различных классов. Контроллеры в Oxite перемещены в отдельный проект, а представления также находятся в отдельном проекте.
В CodeCampServer ViewModels имеют имя * Form и помещаются в проект пользовательского интерфейса в папке Models.
В проекте MvcPress они помещаются в проект Data, который также содержит весь код для работы с базой данных и немного больше (но я не рекомендовал этот подход, это только для примера)
Таким образом, вы можете видеть, что есть много точек зрения. Я обычно храню свои ViewModels (объекты DTO) в проекте сайта. Но когда у меня более 10 моделей, я предпочитаю перемещать их в отдельную сборку. Обычно в этом случае я перемещаю контроллеры в отдельную сборку.
Другой вопрос, как легко отобразить все данные из модели в вашу модель представления. Предлагаю взглянуть на библиотеку AutoMapper . Мне это очень нравится, это делает всю грязную работу за меня.
И я также предлагаю посмотреть на проект SharpArchitecture . Он обеспечивает очень хорошую архитектуру для проектов и содержит множество интересных фреймворков и руководств, а также большое сообщество.

zihotki
источник
8
ViewModels! = DTO
Барт Каликсто
6

Вот фрагмент кода из моих лучших практик:

    public class UserController : Controller
    {
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        {
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        }

        public ActionResult Index(int? page)
        {
            return View(userService.GetPage(page ?? 1, 5));
        }

        public ActionResult Create()
        {
            return View(createBuilder.BuildInput(new User()));
        }

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        {
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }

        public ActionResult Edit(long id)
        {
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        }

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
        {           
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }
}
Ома
источник
5

Мы добавляем все наши ViewModel в папку Models (вся наша бизнес-логика находится в отдельном проекте ServiceLayer)

Beep Beep
источник
4

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

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

Прит Сангха
источник
2

В нашем случае у нас есть модели вместе с контроллерами в проекте, отдельном от представлений.

Как правило, мы пытались перемещать и избегать большинства вещей ViewData ["..."] в ViewModel, поэтому мы избегаем приведений и магических строк, что хорошо.

ViewModel также содержит некоторые общие свойства, такие как информация о разбиении на страницы для списков или информация заголовка страницы для рисования панировочных сухарей и заголовков. На данный момент, по моему мнению, базовый класс содержит слишком много информации, и мы можем разделить ее на три части: самую основную и необходимую информацию для 99% страниц в модели базового представления, а затем модель для списков и модель. для форм, которые содержат конкретные данные для этого сценария и наследуются от базового.

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

Марк Климент
источник
0

код в контроллере:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        {
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        }

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    {
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        {
            if (EntryManager.Update(entry) == 1)
            {
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            }
            else
            {
                return RedirectToAction("EntryEditFailed", "Dictionary");
            }
        }
        else
        {
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        }

        #endregion
    }

модель с учетом кода:

public class EntryEditViewModel
    {
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        {
            get { return _entry; }
            set { _entry = value; }
        }

        public StatusList StatusList
        {
            get { return _statusList; }
        }

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        {
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        }

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        {
            this.Entry = entry;
        }

        #endregion       
    }

проекты:

  • DevJet.Web (веб-проект ASP.NET MVC)

  • DevJet.Web.App.Dictionary (отдельный проект библиотеки классов)

    В этом проекте я сделал несколько папок, таких как: DAL, BLL, BO, VM (папка для просмотра моделей)

Коннектикут
источник
Привет, вы можете поделиться, какова структура класса Entry?
Динис Круз
0

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

class ViewModelBase 
{
  public bool HasError {get;set;} 
  public string ErrorMessage {get;set;}
  public List<string> UserRoles{get;set;}
}

В базовом классе контроллера есть метод наподобие PopulateViewModelBase (), этот метод заполнит контекстные данные и пользовательские роли. HasError и ErrorMessage, установите эти свойства, если есть исключение при извлечении данных из service / db. Привязать эти свойства к представлению, чтобы показать ошибку. Пользовательские роли могут использоваться для отображения скрытого раздела в представлении на основе ролей.

Чтобы заполнить модели представления различными действиями get, это можно сделать согласованным с помощью базового контроллера с абстрактным методом FillModel.

class BaseController :BaseController 
{
   public PopulateViewModelBase(ViewModelBase model) 
{
   //fill up common data. 
}
abstract ViewModelBase FillModel();
}

В контроллерах

class MyController :Controller 
{

 public ActionResult Index() 
{
   return View(FillModel()); 
}

ViewModelBase FillModel() 
{ 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
{ 
   switch(currentAction) 
{  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 
}
}
catch(Exception ex) 
{
   model.HasError=true;
   model.ErrorMessage=ex.Message;
}
//fill common properties 
base.PopulateViewModelBase(model);
return model;
}
}
Аджай Келкар
источник