ViewModel Najlepsze praktyki


238

Z tego pytania wynika, że ​​sensowne jest, aby kontroler utworzył model ViewModel, który dokładniej odzwierciedla model, który widok próbuje wyświetlić, ale jestem ciekawy niektórych konwencji (jestem nowy we wzorcu MVC , jeśli nie było to już oczywiste).

Zasadniczo miałem następujące pytania:

  1. Zwykle lubię mieć jedną klasę / plik. Czy ma to sens w przypadku ViewModel, jeśli jest on tworzony tylko w celu przekazywania danych z kontrolera do widoku?
  2. Jeśli ViewModel należy do własnego pliku, a ty używasz struktury katalogu / projektu do oddzielenia rzeczy, to gdzie należy plik ViewModel ? W katalogu kontrolerów ?

Na razie tyle. Mogę mieć jeszcze kilka pytań, ale przeszkadza mi to przez ostatnią godzinę i wydaje mi się, że mogę znaleźć spójne wskazówki gdzie indziej.

EDYCJA: Patrząc na przykładową aplikację NerdDinner na CodePlex, wygląda na to, że ViewModels są częścią Kontrolerów , ale nadal sprawia mi to dyskomfort, że nie znajdują się we własnych plikach.


66
Nie nazwałbym NerdDinner przykładem „najlepszych praktyk”. Twoja intuicja dobrze ci służy. :)
Ryan Montgomery,

Odpowiedzi:


211

Dla każdego widoku tworzę coś, co nazywam „ViewModel”. Umieszczam je w folderze o nazwie ViewModels w moim projekcie MVC Web. Nazywam je po kontrolerze i akcji (lub widoku), które reprezentują. Więc jeśli muszę przekazać dane do widoku SignUp na kontrolerze członkostwa, tworzę klasę MembershipSignUpViewModel.cs i umieszczam ją w folderze ViewModels.

Następnie dodaję niezbędne właściwości i metody, aby ułatwić transfer danych z kontrolera do widoku. Korzystam z Automappera, aby przejść z mojego ViewModel do modelu domeny i wrócić, jeśli to konieczne.

Działa to również dobrze w przypadku złożonych modeli ViewModels, które zawierają właściwości podobne do innych modeli ViewModels. Na przykład, jeśli masz 5 widgetów na stronie indeksu w kontrolerze członkostwa i utworzyłeś ViewModel dla każdego widoku częściowego - w jaki sposób przekazujesz dane z akcji Index do części? Dodajesz właściwość do MembershipIndexViewModel typu MyPartialViewModel, a podczas renderowania częściowej przekazujesz w Model.MyPartialViewModel.

Wykonanie tego w ten sposób pozwala dostosować częściowe właściwości ViewModel bez konieczności zmiany widoku indeksu. Nadal po prostu przechodzi w Model.MyPartialViewModel, więc jest mniejsza szansa, że ​​będziesz musiał przejść przez cały łańcuch części, aby coś naprawić, gdy wszystko, co robisz, to dodawanie właściwości do częściowego ViewModel.

Dodam również przestrzeń nazw „MyProject.Web.ViewModels” do web.config, aby umożliwić mi odwoływanie się do nich w dowolnym widoku bez dodawania wyraźnej instrukcji importu do każdego widoku. Po prostu sprawia, że ​​jest trochę czystszy.


3
Co zrobić, jeśli chcesz wykonać POST z widoku częściowego i zwrócić cały widok (w przypadku błędu modelu)? W widoku częściowym nie masz dostępu do modelu nadrzędnego.
Cosmo,

5
@Cosmo: Następnie POST do działania, które może zwrócić cały widok w przypadku błędu modelu. Po stronie serwera wystarczy odtworzyć model nadrzędny.
Tomas Aschan,

Co z logowaniem [POST] i logowaniem [GET]? z różnymi modelami widoków?
Bart Calixto

Zazwyczaj login [GET] nie wywołuje ViewModel, ponieważ nie trzeba ładować żadnych danych.
Andre Figueiredo

Dobra rada. Gdzie powinien iść dostęp do danych, przetwarzanie i ustawianie właściwości modelu / maszyny wirtualnej? W moim przypadku będziemy mieć pewne dane pochodzące z lokalnej bazy danych CMS i niektóre pochodzące z usług internetowych, które będą musiały zostać przetworzone / zmanipulowane przed ustawieniem w modelu. Umieszczenie tego wszystkiego w kontrolerze staje się dość nieuporządkowane.
xr280xr

124

Rozdzielanie klas według kategorii (kontrolery, modele ViewModels, filtry itp.) To nonsens.

Jeśli chcesz napisać kod dla sekcji Home swojej witryny (/), utwórz folder o nazwie Home i umieść tam HomeController, IndexViewModel, AboutViewModel itp. Oraz wszystkie powiązane klasy używane przez akcje Home.

Jeśli masz wspólne klasy, takie jak ApplicationController, możesz umieścić je w katalogu głównym projektu.

Po co rozdzielać rzeczy powiązane (HomeController, IndexViewModel) i utrzymywać razem rzeczy, które w ogóle nie mają związku (HomeController, AccountController)?


Napisałem post na blogu na ten temat.


13
Jeśli to zrobisz, sprawy staną się dość chaotyczne.
UpTheCreek

14
Nie, niechlujne jest umieszczenie wszystkich kontrolerów w jednym katalogu / namespace. Jeśli masz 5 kontrolerów, z których każdy używa 5 modeli, masz 25 modeli. Przestrzenie nazw to mechanizm porządkowania kodu i tutaj nie powinno być inaczej.
Max Toro,

41
@ Max Toro: zaskoczony, że tak bardzo cię nie doceniono. Po pewnym czasie pracy nad ASP.Net MVC odczuwam duży ból z powodu posiadania wszystkich modeli ViewModels w jednym miejscu, wszystkich kontrolerów w innym i wszystkich widoków w innym. MVC to trio powiązanych ze sobą elementów, są one połączone - wspierają się nawzajem. Wydaje mi się, że rozwiązanie może mnie znacznie lepiej zorganizować, jeśli kontroler, ViewModels i widoki dla danej sekcji mieszkają razem w tym samym katalogu. MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml itp.
quentin-starin

13
@RyanJMcGowan separacja obaw nie jest separacją klas.
Max Toro,

12
@RyanJMcGowan bez względu na to, jak podejdziesz do programowania, problemem jest to, co się kończy, szczególnie w przypadku dużych aplikacji. W trybie konserwacji nie myślisz o wszystkich modelach, a nie o wszystkich kontrolerach, dodajesz jedną funkcję na raz.
Max Toro,

21

Trzymam swoje klasy aplikacji w podfolderze o nazwie „Core” (lub oddzielnej bibliotece klas) i używam tych samych metod, co KIGG przykładowa aplikacja , ale z pewnymi zmianami, aby moje aplikacje były bardziej SUCHE.

Tworzę klasę BaseViewData w / Core / ViewData /, gdzie przechowuję wspólne właściwości dla całej witryny.

Następnie tworzę również wszystkie moje klasy ViewData widoku w tym samym folderze, które następnie pochodzą z BaseViewData i mają określone właściwości widoku.

Następnie tworzę ApplicationController, z którego pochodzą wszystkie moje kontrolery. ApplicationController ma ogólną metodę GetViewData w następujący sposób:

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

Wreszcie w mojej akcji kontrolera wykonuję następujące czynności, aby zbudować mój model ViewData

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

Myślę, że to działa naprawdę dobrze i utrzymuje twoje widoki w porządku i twoje kontrolery są chude.


13

Klasa ViewModel służy do kapsułkowania wielu elementów danych reprezentowanych przez instancje klas w jednym łatwym do zarządzania obiekcie, który można przekazać do swojego widoku.

Sensowne byłoby posiadanie klas ViewModel we własnych plikach, we własnym katalogu. W moich projektach mam podfolder folderu Modele o nazwie ViewModels. Tam ProductViewModel.csmieszkają moje modele ViewModels (np. ).


13

Nie ma dobrego miejsca na przechowywanie modeli. Możesz przechowywać je w osobnym zestawie, jeśli projekt jest duży i istnieje wiele ViewModels (obiektów transferu danych). Możesz także przechowywać je w osobnym folderze projektu witryny. Na przykład w Oxite są one umieszczane w projekcie Oxite, który zawiera również wiele różnych klas. Kontrolery w Oxite są przenoszone do osobnego projektu, a widoki są również w osobnym projekcie.
W CodeCampServer ViewModels mają nazwę * Form i są umieszczane w projekcie interfejsu użytkownika w folderze Modele.
W projekcie MvcPress są one umieszczane w projekcie Data, który zawiera również cały kod do pracy z bazą danych i trochę więcej (ale nie poleciłem tego podejścia, to tylko przykład)
Możesz więc zobaczyć, że jest wiele punktów widzenia. Zazwyczaj trzymam moje ViewModels (obiekty DTO) w projekcie witryny. Ale kiedy mam więcej niż 10 modeli, wolę przenieść je do oddzielnego zestawu. Zwykle w tym przypadku przenoszę również kontrolery do oddzielnego zestawu.
Kolejne pytanie dotyczy łatwego mapowania wszystkich danych z modelu na model ViewModel. Sugeruję zajrzeć do biblioteki AutoMapper . Bardzo mi się podoba, to dla mnie cała brudna robota.
Proponuję także przyjrzeć się projektowi SharpArchitecture . Zapewnia bardzo dobrą architekturę dla projektów i zawiera wiele fajnych ram i wskazówek oraz wspaniałą społeczność.


8
ViewModels! = DTO
Bart Calixto

6

Oto fragment kodu z moich najlepszych praktyk:

    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

Wrzucamy wszystkie nasze ViewModels do folderu Modele (cała nasza logika biznesowa znajduje się w osobnym projekcie ServiceLayer)


4

Osobiście sugerowałbym, że jeśli ViewModel nie jest trywialny, użyj osobnej klasy.

Jeśli masz więcej niż jeden model widoku, sugeruję, aby sensowne było podzielenie go na przynajmniej katalog. jeśli model widoku zostanie później udostępniony, wówczas przestrzeń nazw sugerowana w katalogu ułatwia przejście do nowego zestawu.


2

W naszym przypadku mamy modele wraz ze sterownikami w projekcie oddzielnym od widoków.

Zasadniczo próbowaliśmy przenieść i uniknąć większości elementów ViewData [„...”] do ViewModel, dlatego unikamy rzutów i magicznych ciągów, co jest dobrą rzeczą.

ViewModel posiada również pewne wspólne właściwości, takie jak informacje o paginacji dla list lub informacje nagłówkowe strony, aby narysować okruszki i tytuły. W tej chwili klasa podstawowa zawiera moim zdaniem zbyt dużo informacji i możemy podzielić ją na trzy części, najbardziej podstawowe i niezbędne informacje dla 99% stron w modelu widoku podstawowego, a następnie model dla list i model dla formularzy, które przechowują określone dane dla tych scenariuszy i dziedziczą po podstawowym.

Wreszcie, wdrażamy model widoku dla każdego podmiotu, aby zająć się konkretnymi informacjami.


0

kod w kontrolerze:

    [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
    }

kod w widoku modelu:

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       
    }

projektowanie:

  • DevJet.Web (projekt internetowy ASP.NET MVC)

  • DevJet.Web.App.Dictionary (oddzielny projekt biblioteki klas)

    w tym projekcie stworzyłem kilka folderów takich jak: DAL, BLL, BO, VM (folder do przeglądania modeli)


Cześć. Czy możesz podzielić się strukturą klasy Entry?
Dinis Cruz,

0

Utwórz klasę bazową modelu widoku, która ma zwykle wymagane właściwości, takie jak wynik operacji i dane kontekstowe, można również umieścić bieżące dane użytkownika i role

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

W klasie kontrolera bazowego istnieje metoda taka jak PopulateViewModelBase (), ta metoda wypełni dane kontekstowe i role użytkownika. HasError i ErrorMessage, ustaw te właściwości, jeśli istnieje wyjątek podczas pobierania danych z usługi / db. Powiąż te właściwości w widoku, aby pokazać błąd. Roli użytkownika można używać do pokazywania ukrytej sekcji w widoku opartym na rolach.

Aby zapełnić modele widoków różnymi działaniami get, można to uczynić spójnym, mając podstawowy kontroler z abstrakcyjną metodą FillModel

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

W kontrolerach

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;
}
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.