Jak sprawić, by tworzenie modeli widoków w czasie wykonywania było mniej bolesne

17

Przepraszam za to długie pytanie, brzmi to trochę jak rant, ale obiecuję, że nie! Podsumowałem moje pytania poniżej

W świecie MVC rzeczy są proste. Model ma stan, widok pokazuje model, a kontroler robi rzeczy do / z modelem (w zasadzie), kontroler nie ma stanu. Aby to zrobić , kontroler ma pewne zależności od usług sieciowych, repozytorium, partii. Kiedy tworzysz instancję kontrolera, zależy ci na dostarczeniu tych zależności, nic więcej. Wykonując akcję (metodę na kontrolerze), używasz tych zależności, aby pobrać lub zaktualizować model lub wywołać inną usługę domeny. Jeśli istnieje jakikolwiek kontekst, powiedzmy, że użytkownik chce zobaczyć szczegóły konkretnego elementu, przekazujesz identyfikator tego elementu jako parametr do działania. Nigdzie w kontrolerze nie ma odniesienia do żadnego stanu. Jak na razie dobrze.

Wpisz MVVM. Uwielbiam WPF, uwielbiam wiązanie danych. Uwielbiam frameworki, dzięki którym wiązanie danych z ViewModels jest jeszcze łatwiejsze (przy użyciu bankomatu Caliburn Micro). Wydaje mi się jednak, że na tym świecie rzeczy są mniej proste. Zróbmy ćwiczenie ponownie: Model posiada stan, widok pokazy ViewModel i ViewModel robi rzeczy do / z modelu (w zasadzie), ViewModel ma mieć stan! (wyjaśnienie; może to delegaci wszystkich właściwości do jednego lub więcej modeli, ale to oznacza, musi mieć odniesienie do modelu taki czy inny sposób, który jest stan w sobie) Aby zrobićrzeczy ViewModel ma pewne zależności od usług sieciowych, repozytorium, partii. Kiedy tworzysz ViewModel, zależy ci na dostarczeniu tych zależności, ale także na stanie. I to, panie i panowie, denerwuje mnie bez końca.

Ilekroć musisz utworzyć instancję ProductDetailsViewModelz ProductSearchViewModel(z którego zadzwoniłeś, ProductSearchWebServicektóry z kolei wrócił IEnumerable<ProductDTO>, wszyscy nadal są ze mną?), Możesz wykonać jedną z następujących czynności:

  • zadzwoń new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, to źle, wyobraź sobie 3 dodatkowe zależności, oznacza to, że ProductSearchViewModelmusisz również wziąć na siebie te zależności. Również zmiana konstruktora jest bolesna.
  • zadzwoń _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, fabryka to tylko Func, są one łatwo generowane przez większość platform IoC. Myślę, że to źle, ponieważ metody Init są nieszczelną abstrakcją. Nie można również użyć słowa kluczowego tylko do odczytu dla pól ustawionych w metodzie Init. Jestem pewien, że jest kilka innych powodów.
  • call _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);Więc ... jest to wzorzec (fabryka abstrakcyjna), który jest zwykle zalecany dla tego typu problemów. Myślałem, że to genialne, ponieważ zaspokaja moje pragnienie pisania statycznego, dopóki nie zacząłem go używać. Ilość kodu na schemacie to chyba za dużo (no wiesz, poza śmiesznymi nazwami zmiennych, z których korzystam). Dla każdego ViewModel, który potrzebuje parametrów środowiska wykonawczego, otrzymasz dwa dodatkowe pliki (interfejs fabryczny i implementacja), i musisz wpisać zależności inne niż środowisko wykonawcze, takie jak 4 dodatkowe czasy. I za każdym razem, gdy zmieniają się zależności, możesz to zmienić również w fabryce. Wydaje mi się, że nawet nie używam już pojemnika DI. (Myślę, że Castle Windsor ma na to jakieś rozwiązanie [z własnymi wadami, poprawcie mnie, jeśli się mylę]).
  • zrób coś z anonimowymi typami lub słownikiem. Lubię moje pisanie statyczne.

Więc tak. Mieszanie stanu i zachowania w ten sposób stwarza problem, który w ogóle nie istnieje w MVC. I wydaje mi się, że obecnie nie ma naprawdę odpowiedniego rozwiązania tego problemu. Teraz chciałbym obserwować kilka rzeczy:

  • Ludzie faktycznie używają MVVM. Więc albo nie przejmują się wszystkimi powyższymi, albo mają jakieś genialne inne rozwiązanie.
  • Nie znalazłem szczegółowego przykładu MVVM z WPF. Na przykład projekt próbki NDDD ogromnie pomógł mi zrozumieć niektóre koncepcje DDD. Naprawdę chciałbym, gdyby ktoś mógł skierować mnie w stronę czegoś podobnego do MVVM / WPF.
  • Może źle robię MVVM i powinienem odwrócić swój projekt do góry nogami. Może wcale nie powinienem mieć tego problemu. Wiem, że inni ludzie zadawali to samo pytanie, więc myślę, że nie jestem jedyny.

Podsumowując

  • Czy mam rację, stwierdzając, że fakt, że ViewModel jest punktem integracji zarówno stanu, jak i zachowania, jest przyczyną pewnych trudności z wzorzec MVVM jako całości?
  • Czy używanie abstrakcyjnego wzorca fabrycznego jest jedynym / najlepszym sposobem na utworzenie modelu ViewModel w sposób statyczny?
  • Czy dostępna jest coś w rodzaju szczegółowej implementacji referencyjnej?
  • Czy posiadanie dużej liczby modeli ViewModels, których stan / zachowanie ma zapach projektowy?
dvdvorle
źródło
10
Jest to zbyt długo, aby przeczytać, rozważyć zmianę, jest tam wiele nieistotnych rzeczy. Możesz przegapić dobre odpowiedzi, ponieważ ludzie nie zawracają sobie głowy czytaniem tego wszystkiego.
yannis,
Powiedziałeś, że kochasz Caliburn.Micro, ale nie wiesz, jak ten framework może pomóc w tworzeniu nowych modeli widoków? Sprawdź kilka przykładów.
Euforia
@Euphoric Mógłbyś być bardziej konkretny, Google chyba nie pomaga mi tutaj. Masz jakieś słowa kluczowe, które mogę wyszukać?
dvdvorle,
3
Myślę, że trochę upraszczasz MVC. Na pewno widok pokazuje model na początku, ale podczas działania zmienia się stan. Moim zdaniem ten zmieniający się stan to „Edycja modelu”. Oznacza to spłaszczoną wersję Modelu ze zmniejszonymi ograniczeniami spójności. W rzeczywistości to, co nazywam edycją modelu, to MVVM ViewModel. Utrzymuje stan podczas przejścia, który wcześniej był trzymany przez Widok w MVC lub zepchnięty z powrotem do nieprzydzielonej wersji Modelu, do której nie sądzę, że należy. Więc wcześniej miałeś stan „w ciągłym ruchu”. Teraz wszystko jest w ViewModel.
Scott Whitlock,
@ScottWhitlock Rzeczywiście upraszczam MVC. Ale nie twierdzę, że to źle, że stan „w strumieniu” znajduje się w ViewModel, mówię, że stłoczenie tam zachowań również utrudnia zainicjowanie ViewModel do użytecznego stanu, powiedzmy, inny ViewModel. Twój „Edytuj model” w MVC nie wie, jak się zapisać (nie ma metody zapisu). Ale kontroler wie o tym i ma wszystkie niezbędne do tego zależności.
dvdvorle,

Odpowiedzi:

2

Problem zależności podczas inicjowania nowego modelu widoku można rozwiązać za pomocą IOC.

public class MyCustomViewModel{
  private readonly IShoppingCartWebService _cartService;

  private readonly ITimeService _timeService;

  public ProductDTO ProductDTO { get; set; }

  public ProductDetailsViewModel(IShoppingCartWebService cartService, ITimeService timeService){
    _cartService = cartService;
    _timeService = timeService;
  }
}

Podczas konfigurowania pojemnika ...

Container.Register<IShoppingCartWebService,ShoppingCartWebSerivce>().As.Singleton();
Container.Register<ITimeService,TimeService>().As.Singleton();
Container.Register<ProductDetailsViewModel>();

Gdy potrzebujesz modelu widoku:

var viewmodel = Container.Resolve<ProductDetailsViewModel>();
viewmodel.ProductDTO = myProductDTO;

Podczas korzystania ze szkieletu, takiego jak kaliburn micro, często istnieje już pewna forma pojemnika IOC.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);
Mikrofon
źródło
1

Pracuję codziennie z ASP.NET MVC i pracuję nad WPF od ponad roku i tak to widzę:

MVC

Kontroler powinien koordynować działania (pobierz to, dodaj to).

Widok jest odpowiedzialny za wyświetlanie modelu.

Model zazwyczaj obejmuje dane (np. UserId, FirstName), a także stan (np. Tytuły) i zwykle jest specyficzny dla widoku.

MVVM

Model zazwyczaj przechowuje tylko dane (np. UserId, FirstName) i zwykle jest przekazywany

Model widoku obejmuje zachowanie widoku (metody), jego danych (model) i interakcji (poleceń) - podobnie jak aktywny wzorzec MVP, w którym prezenter zna model. Model widoku jest specyficzny dla widoku (1 widok = 1 model widoku).

Widok odpowiada za wyświetlanie danych i powiązanie danych z modelem widoku. Podczas tworzenia widoku zwykle jest z nim tworzony powiązany model widoku.


Należy pamiętać, że wzorzec prezentacji MVVM jest specyficzny dla WPF / Silverlight ze względu na ich charakter wiązania danych.

Widok zazwyczaj wie, z którym modelem widoku jest powiązany (lub jego abstrakcją).

Radziłbym, aby traktować model widoku jako singleton, nawet jeśli jest on tworzony na podstawie widoku. Innymi słowy, powinieneś być w stanie stworzyć go przez DI za pośrednictwem kontenera MKOl i wywołać odpowiednie metody, aby powiedzieć; wczytaj model na podstawie parametrów. Coś takiego:

public partial class EditUserView
{
    public EditUserView(IContainer container, int userId) : this() {
        var viewModel = container.Resolve<EditUserViewModel>();
        viewModel.LoadModel(userId);
        DataContext = viewModel;
    }
}

Jako przykład w tym przypadku nie utworzyłbyś modelu widoku specyficznego dla aktualizowanego użytkownika - zamiast tego model zawierałby dane specyficzne dla użytkownika, które są ładowane przez jakieś wywołanie w modelu widoku.

Shelakel
źródło
Jeśli moim imieniem jest „Peter”, a moje tytuły to {„Rev”, „Dr”} *, dlaczego bierzesz pod uwagę dane FirstName i stan tytułu? Czy możesz wyjaśnić swój przykład? * niezupełnie
Pete Kirkham,
@ PeteteKirkham - przykład „tytułów”, o którym mówiłem w kontekście powiedzmy combobox. Zasadniczo, gdy wysyłasz informacje, które mają zostać utrwalone, nie wysyłasz stanu (np. Listy stanów / prowincji / tytułów), z którego dokonano wyborów. Każdy przydatny stan do przesłania z danymi (np. Używana nazwa użytkownika) powinien zostać sprawdzony w punkcie przetwarzania, ponieważ stan mógł stać się nieaktualny (jeśli korzystasz z jakiegoś wzorca asynchronicznego, takiego jak kolejkowanie wiadomości).
Shelakel,
Chociaż minęły dwa lata od tego postu, muszę skomentować na korzyść przyszłych widzów: Dwie rzeczy przeszkodziły mi w twojej odpowiedzi. Widok może odpowiadać jednemu modelowi ViewModel, ale model ViewModel może być reprezentowany przez kilka widoków. Po drugie, opisujesz anty-wzorzec Service Locator. IMHO nie powinieneś bezpośrednio rozstrzygać wszędzie viewmodels. Po to jest DI. Podejmuj decyzje w mniejszej liczbie punktów, jak możesz. Pozwól na przykład Caliburn wykonać to za Ciebie.
Jony Adamit,
1

Krótka odpowiedź na twoje pytania:

  1. Tak Stan + Zachowanie prowadzi do tych problemów, ale dotyczy to wszystkich OO. Prawdziwym winowajcą jest połączenie ViewModels, które jest rodzajem naruszenia SRP.
  2. Prawdopodobnie wpisany statycznie. Ale powinieneś zmniejszyć / wyeliminować potrzebę tworzenia instancji ViewModels z innych ViewModels.
  3. Nie, że jestem świadomy.
  4. Nie, ale posiadanie ViewModels z niepowiązanym stanem i zachowaniem (jak niektóre odwołania do modeli i niektóre odwołania do ViewModel)

Długa wersja:

Stoimy w obliczu tego samego problemu i znaleźliśmy kilka rzeczy, które mogą ci pomóc. Chociaż nie znam „magicznego” rozwiązania, te rzeczy nieco łagodzą ból.

  1. Wdrożenie modeli DTO do wiązania w celu śledzenia zmian i sprawdzania poprawności. Te „Dane” - modele podglądu nie mogą zależeć od usług i nie mogą pochodzić z kontenera. Mogą być po prostu „nowe”, przekazywane, a nawet wywodzić się z DTO. Najważniejsze jest wdrożenie modelu specyficznego dla twojej aplikacji (jak MVC).

  2. Oddziel swoje ViewModels. Caliburn ułatwia łączenie ViewModels. Sugeruje to nawet za pomocą modelu ekranu / przewodnika. Ale to sprzężenie sprawia, że ​​ViewModels jest trudny do testowania jednostkowego, stwarza wiele zależności i co najważniejsze: nakłada ciężar zarządzania cyklem życia ViewModel na twoje ViewModels. Jednym ze sposobów na ich rozdzielenie jest użycie usługi nawigacyjnej lub kontrolera ViewModel. Na przykład

    interfejs publiczny IShowViewModels {void Show (object inlineArgumentsAsAnonymousType, string regionId); }

Jeszcze lepiej jest to robić za pomocą jakiejś formy wiadomości. Ale ważne jest, aby nie obsługiwać cyklu życia ViewModel z innych ViewModels. W kontrolerach MVC nie zależą od siebie, aw MVVM ViewModels nie powinny od siebie zależeć. Zintegruj je na kilka innych sposobów.

  1. Użyj swoich „dynamicznych” / „dynamicznych” właściwości kontenerów. Chociaż możliwe jest stworzenie czegoś podobnego INeedData<T1,T2,...>i wymuszenie parametrów tworzenia bezpiecznych dla typu, nie jest tego warte. Również tworzenie fabryk dla każdego typu ViewModel nie jest tego warte. Większość kontenerów IoC zapewnia rozwiązania tego problemu. Podczas działania wystąpią błędy, ale warto odłączyć i przetestować jednostkę. Nadal wykonujesz test integracji, a błędy te można łatwo wykryć.
sanosdole
źródło
0

Sposób, w jaki zwykle to robię (używając PRISM), polega na tym, że każdy zestaw zawiera moduł inicjalizacji kontenera, w którym wszystkie interfejsy, instancje są rejestrowane podczas uruchamiania.

private void RegisterResources()
{
    Container.RegisterType<IDataService, DataService>();
    Container.RegisterType<IProductSearchViewModel, ProductSearchViewModel>();
    Container.RegisterType<IProductDetailsViewModel, ProductDetailsViewModel>();
}

Biorąc pod uwagę twoje przykładowe klasy, można je zaimplementować w ten sposób, z pojemnikiem przechodzącym przez całą drogę. W ten sposób można łatwo dodawać nowe zależności, ponieważ masz już dostęp do kontenera.

/// <summary>
/// IDataService Interface
/// </summary>
public interface IDataService
{
    DataTable GetSomeData();
}

public class DataService : IDataService
{
    public DataTable GetSomeData()
    {
        MessageBox.Show("This is a call to the GetSomeData() method.");

        var someData = new DataTable("SomeData");
        return someData;
    }
}

public interface IProductSearchViewModel
{
}

public class ProductSearchViewModel : IProductSearchViewModel
{
    private readonly IUnityContainer _container;

    /// <summary>
    /// This will get resolved if it's been added to the container.
    /// Or alternately you could use constructor resolution. 
    /// </summary>
    [Dependency]
    public IDataService DataService { get; set; }

    public ProductSearchViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void SearchAndDisplay()
    {
        DataTable results = DataService.GetSomeData();

        var detailsViewModel = _container.Resolve<IProductDetailsViewModel>();
        detailsViewModel.DisplaySomeDataInView(results);

        // Create the view, usually resolve using region manager etc.
        var detailsView = new DetailsView() { DataContext = detailsViewModel };
    }
}

public interface IProductDetailsViewModel
{
    void DisplaySomeDataInView(DataTable dataTable);
}

public class ProductDetailsViewModel : IProductDetailsViewModel
{
    private readonly IUnityContainer _container;

    public ProductDetailsViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void DisplaySomeDataInView(DataTable dataTable)
    {
    }
}

Dość powszechna jest klasa ViewModelBase, z której pochodzą wszystkie modele widoków, która zawiera odwołanie do kontenera. Dopóki nauczysz się rozwiązywać wszystkie modele widoków zamiast new()'ingnich, powinno to znacznie uprościć rozwiązywanie zależności.

Martin Cooper
źródło
0

Czasami dobrze jest przejść do najprostszej definicji, a nie pełnego przykładu: http://en.wikipedia.org/wiki/Model_View_ViewModel może być może czytanie przykładu Java ZK jest bardziej pouczające niż C #.

Innym razem słuchaj instynktu jelitowego ...

Czy posiadanie dużej liczby modeli ViewModels, których stan / zachowanie ma zapach projektowy?

Czy twoje modele są odwzorowaniami obiektu na tabelę? Być może ORM pomógłby w mapowaniu do obiektów domeny podczas obsługi firmy lub aktualizacji wielu tabel.

Gerry King
źródło