Jak zmapować View Model z powrotem do Domain Model w akcji POST?

87

Każdy artykuł znaleziony w Internecie na temat korzystania z ViewModels i wykorzystania Automappera zawiera wytyczne dotyczące mapowania kierunku „Kontroler -> Widok”. Bierzesz model domeny wraz ze wszystkimi listami wyboru do jednego wyspecjalizowanego ViewModel i przekazujesz go do widoku. To jasne i dobrze.
Widok ma formę i ostatecznie jesteśmy w akcji POST. W tym miejscu na scenę pojawiają się wszystkie Segregatory Modelu wraz z [oczywiście] innym modelem Widoku, który jest [oczywiście] powiązany z oryginalnym ViewModel przynajmniej w części konwencji nazewnictwa w celu powiązania i walidacji.

Jak odwzorować to na swój model domeny?

Niech to będzie akcja wstawiania, moglibyśmy użyć tego samego Automappera. Ale co, jeśli byłaby to aktualizacja? Musimy pobrać naszą jednostkę domeny z repozytorium, zaktualizować jej właściwości zgodnie z wartościami w modelu ViewModel i zapisać w repozytorium.

DODATEK 1 (9 lutego 2010): Czasami przypisanie właściwości Modelu nie wystarczy. Należy podjąć pewne działania przeciwko modelowi domeny zgodnie z wartościami modelu widoku. To znaczy, niektóre metody powinny być wywoływane w modelu domeny. Prawdopodobnie powinien istnieć rodzaj warstwy usługi aplikacji, która znajduje się między kontrolerem a domeną, aby przetwarzać modele widoku ...


Jak zorganizować ten kod i gdzie go umieścić, aby osiągnąć następujące cele?

  • utrzymuj kontrolery cienkie
  • honorować praktykę SoC
  • postępuj zgodnie z zasadami projektowania opartego na domenie
  • być SUCHY
  • ciąg dalszy nastąpi ...
Anthony Serdyukov
źródło

Odpowiedzi:

37

Używam IBuilder interfejs i wdrożyć go za pomocą ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (implementacja) RebuildViewModel po prostu wywołujeBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

btw nie piszę ViewModel Piszę dane wejściowe, ponieważ jest znacznie krótszy, ale to nie jest naprawdę ważne,
mam nadzieję, że to pomoże

Aktualizacja: używam tego podejścia teraz w aplikacji demonstracyjnej ProDinner ASP.net MVC , teraz nazywa się ona IMapper, dostępny jest również plik PDF, w którym to podejście jest szczegółowo wyjaśnione

Omu
źródło
Podoba mi się to podejście. Jedną rzeczą, której nie jestem pewien, jest implementacja IBuilder, szczególnie w świetle warstwowej aplikacji. Na przykład mój ViewModel ma 3 SelectLists. W jaki sposób implementacja kreatora pobiera wartości listy wyboru z repozytorium?
Matt Murrell
@Matt Murrell spójrz na prodinner.codeplex.com Robię to tam i nazywam to IMapper zamiast IBuilder
Omu
6
Podoba mi się to podejście, zaimplementowałem jego próbkę tutaj: gist.github.com/2379583
Paul Stovell
Moim zdaniem nie jest to zgodne z podejściem modelu domeny. Wygląda na to, że podejście CRUD do niejasnych wymagań. Czy nie powinniśmy używać fabryk (DDD) i powiązanych metod w modelu domeny, aby przekazać jakieś rozsądne działania? W ten sposób lepiej załadujemy jednostkę z DB i zaktualizujemy ją w razie potrzeby, prawda? Więc wygląda na to, że nie jest w pełni poprawna.
Artem
7

Narzędzia takie jak AutoMapper mogą służyć do aktualizowania istniejącego obiektu danymi z obiektu źródłowego. Akcja kontrolera do aktualizacji może wyglądać następująco:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Oprócz tego, co widać w powyższym fragmencie:

  • Dane POST w celu wyświetlenia modelu + walidacja odbywa się w ModelBinder (można rozszerzyć za pomocą niestandardowych powiązań)
  • Obsługa błędów (tj. Przechwytywanie wyjątków dostępu do danych przez repozytorium) może być wykonana przez filtr [HandleError]

Akcja kontrolera jest dość cienka i obawy są oddzielone: ​​problemy z mapowaniem są rozwiązywane w konfiguracji AutoMapper, walidacja jest wykonywana przez ModelBinder, a dostęp do danych przez Repository.

PanJanek
źródło
6
Nie jestem pewien, czy Automapper jest tutaj przydatny, ponieważ nie może odwrócić spłaszczania. W końcu model domeny nie jest prostym DTO, takim jak model widoku, dlatego może nie wystarczyć przypisanie do niego niektórych właściwości. Prawdopodobnie niektóre działania powinny być wykonywane na modelu domeny zgodnie z zawartością modelu widoku. Jednak +1 za udostępnianie całkiem niezłego podejścia.
Anthony Serdyukov
@Anton ValueInjecter może odwrócić spłaszczenie;)
Omu
przy takim podejściu nie utrzymujesz kontrolera cienkiego, naruszasz SoC i DRY ... jak wspomniał Omu, powinieneś mieć oddzielną warstwę, która zajmuje się mapowaniem.
Rookian
5

Chciałbym powiedzieć, że ponownie używasz terminu ViewModel dla obu kierunków interakcji z klientem. Jeśli przeczytałeś wystarczająco dużo kodu ASP.NET MVC w środowisku naturalnym, prawdopodobnie zauważyłeś różnicę między ViewModel i EditModel. Myślę, że to ważne.

ViewModel reprezentuje wszystkie informacje wymagane do renderowania widoku. Może to obejmować dane renderowane w statycznych, nieinteraktywnych miejscach, a także dane wyłącznie w celu sprawdzenia, aby zdecydować, co dokładnie renderować. Akcja GET kontrolera jest ogólnie odpowiedzialna za pakowanie ViewModel dla jego widoku.

EditModel (lub może ActionModel) reprezentuje dane wymagane do wykonania akcji, którą użytkownik chciał wykonać dla tego testu POST. Więc EditModel naprawdę próbuje opisać akcję. To prawdopodobnie wykluczy niektóre dane z ViewModel i chociaż są powiązane, myślę, że ważne jest, aby zdać sobie sprawę, że rzeczywiście są różne.

Jeden pomysł

To powiedziawszy, możesz bardzo łatwo mieć konfigurację AutoMapper do przejścia z Model -> ViewModel i inną, aby przejść z EditModel -> Model. Następnie różne akcje kontrolera muszą po prostu używać AutoMapper. Do diabła, EditModel może mieć na sobie funkcje do walidacji jego właściwości względem modelu i zastosowania tych wartości do samego modelu. Nie robi nic innego i masz ModelBinders w MVC, aby mimo wszystko zamapować żądanie na EditModel.

Inny pomysł

Poza tym ostatnio zastanawiałem się nad tym, co działa na podstawie idei ActionModelu, ponieważ klient wysyła do Ciebie z powrotem opis kilku działań, które wykonał użytkownik, a nie tylko jedna duża porcja danych. Z pewnością wymagałoby to trochę Javascript po stronie klienta, ale myślę, że pomysł jest intrygujący.

Zasadniczo, gdy użytkownik wykonuje akcje na wyświetlonym ekranie, Javascript zaczyna tworzyć listę obiektów akcji. Przykładem może być to, że użytkownik jest na ekranie informacji o pracowniku. Aktualizują nazwisko i dodają nowy adres, ponieważ pracownik był niedawno żonaty. Pod osłonami daje to ChangeEmployeeNamea iAddEmployeeMailingAddress obiekty do listy. Użytkownik klika „Zapisz”, aby zatwierdzić zmiany, a Ty przesyłasz listę dwóch obiektów, z których każdy zawiera tylko informacje potrzebne do wykonania każdej akcji.

Potrzebowałbyś bardziej inteligentnego ModelBinder niż domyślny, ale dobry serializator JSON powinien być w stanie zająć się mapowaniem obiektów akcji po stronie klienta na obiekty po stronie serwera. Te po stronie serwera (jeśli jesteś w środowisku dwuwarstwowym) mogą łatwo mieć metody, które zakończyłyby akcję na modelu, z którym pracują. Tak więc akcja kontrolera kończy się po prostu uzyskaniem identyfikatora instancji Model do ściągnięcia i listy działań do wykonania na niej. Albo akcje mają w sobie identyfikator, który je bardzo rozdziela.

Więc może coś takiego zostanie zrealizowane po stronie serwera:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

To naprawdę sprawia, że ​​akcja wysyłania zwrotnego jest dość ogólna, ponieważ opierasz się na swoim ModelBinder, aby uzyskać poprawną instancję IUserAction i instancję IUserAction w celu wykonania prawidłowej logiki lub (bardziej prawdopodobne) wywołania modelu z informacjami.

Jeśli pracujesz w środowisku 3-warstwowym, IUserAction może być po prostu prostym DTO do wykonania przez granicę i wykonania w podobnej metodzie w warstwie aplikacji. W zależności od tego, jak zrobisz tę warstwę, można ją bardzo łatwo podzielić i nadal pozostać w transakcji (przychodzi na myśl żądanie / odpowiedź Agathy i wykorzystanie mapy tożsamości DI i NHibernate).

W każdym razie jestem pewien, że nie jest to doskonały pomysł, wymagałoby to trochę JS po stronie klienta do zarządzania, a nie byłem jeszcze w stanie zrobić projektu, aby zobaczyć, jak się rozwija, ale post próbował pomyśleć o tym, jak dostać się tam iz powrotem, więc pomyślałem, że przedstawię swoje myśli. Mam nadzieję, że to pomoże i chciałbym usłyszeć o innych sposobach zarządzania interakcjami.

Sean Copenhaver
źródło
Ciekawy. Jeśli chodzi o rozróżnienie między ViewModel i EditModel ... czy sugerujesz, że w przypadku funkcji edycji użyjesz ViewModel do utworzenia formularza, a następnie powiążesz się z EditModel, gdy użytkownik go opublikuje? Jeśli tak, jak poradziłbyś sobie z sytuacjami, w których musiałbyś ponownie opublikować formularz z powodu błędów walidacji (na przykład gdy ViewModel zawiera elementy do wypełnienia listy rozwijanej) - czy po prostu uwzględnisz również elementy rozwijane w EditModel? W takim przypadku jaka byłaby różnica między nimi?
UpTheCreek
Domyślam się, że obawiasz się, że jeśli używam EditModel i pojawia się błąd, muszę odbudować mój ViewModel, który może być bardzo drogi. Powiedziałbym, że po prostu przebuduj ViewModel i upewnij się, że ma miejsce na umieszczanie komunikatów powiadomień użytkownika (prawdopodobnie zarówno pozytywnych, jak i negatywnych, takich jak błędy walidacji). Jeśli okaże się, że jest to problem z wydajnością, zawsze możesz buforować ViewModel do czasu zakończenia następnego żądania tej sesji (prawdopodobnie jest to post z EditModel).
Sean Copenhaver