Wie Sie das Erstellen von Ansichtsmodellen zur Laufzeit vereinfachen

17

Ich entschuldige mich für die lange Frage, es liest sich ein bisschen wie ein Scherz, aber ich verspreche, es ist nicht! Ich habe meine Frage (n) unten zusammengefasst

In der MVC-Welt sind die Dinge einfach. Das Modell hat Zustand, wobei die Ansicht zeigt das Modell, und der Controller funktioniert Sachen / mit dem Modell (im Wesentlichen), hat ein Controller keinen Zustand. Um Dinge zu erledigen, hat der Controller einige Abhängigkeiten von Webservices, Repository und dem Los. Wenn Sie einen Controller instanziieren, ist es Ihnen wichtig, diese Abhängigkeiten bereitzustellen, sonst nichts. Wenn Sie eine Aktion (Methode auf dem Controller) ausführen, verwenden Sie diese Abhängigkeiten, um das Modell abzurufen oder zu aktualisieren oder um einen anderen Domänendienst aufzurufen. Wenn es einen Kontext gibt, beispielsweise wenn ein Benutzer die Details eines bestimmten Elements anzeigen möchte, übergeben Sie die ID dieses Elements als Parameter an die Aktion. Nirgendwo im Controller gibt es einen Verweis auf einen Zustand. So weit, ist es gut.

Geben Sie MVVM ein. Ich liebe WPF, ich liebe Datenbindung. Ich liebe Frameworks, die das Binden von Daten an ViewModels noch einfacher machen (mit Caliburn Micro atm). Ich bin der Meinung, dass die Dinge in dieser Welt weniger einfach sind. Machen wir die Übung noch einmal: Das Modell hat einen Status, die Ansicht zeigt das ViewModel und das ViewModel erledigt (im Grunde genommen) Dinge mit dem Modell, ein ViewModel hat einen Status! (zu klären, vielleicht delegiert alle Eigenschaften auf ein oder mehrere Modelle, aber das bedeutet , es muß einen Verweis auf das Modell eine oder andere Weise haben, der Zustand , in sich ist) Zu tunSachen, die das ViewModel hat einige Abhängigkeiten von Webservices, Repository, die Menge. Wenn Sie ein ViewModel instanziieren, müssen Sie diese Abhängigkeiten angeben, aber auch den Status. Und das, meine Damen und Herren, ärgert mich bis zum Äußersten.

Wann immer Sie ein ProductDetailsViewModelvon instanziieren müssen ProductSearchViewModel(von dem Sie das aufgerufen haben, ProductSearchWebServicedas wiederum zurückgekehrt ist IEnumerable<ProductDTO>, alle noch bei mir?), Können Sie eine der folgenden Aktionen ausführen:

  • nennen new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, das ist schlecht, stellen Sie sich 3 weitere Abhängigkeiten vor, dies bedeutet ProductSearchViewModel, dass Sie diese Abhängigkeiten auch übernehmen müssen. Auch das Ändern des Konstruktors ist schmerzhaft.
  • nennen _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, die Fabrik ist nur eine Func, sie werden leicht von den meisten IoC-Frameworks generiert. Ich denke, das ist schlecht, weil Init-Methoden eine undichte Abstraktion sind. Sie können das Schlüsselwort readonly auch nicht für Felder verwenden, die in der Init-Methode festgelegt sind. Ich bin mir sicher, dass es noch ein paar Gründe gibt.
  • call _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);So ... das ist das Muster (abstrakte Fabrik), das normalerweise für diese Art von Problem empfohlen wird. Ich dachte, es war genial, da es mein Verlangen nach statischem Tippen stillt, bis ich es tatsächlich benutzte. Die Menge des Kesselschild-Codes ist meiner Meinung nach zu groß (abgesehen von den lächerlichen Variablennamen, die ich benutze). Für jedes ViewModel, das Laufzeitparameter benötigt, erhalten Sie zwei zusätzliche Dateien (Factory-Schnittstelle und Implementierung), und Sie müssen die Nicht-Laufzeit-Abhängigkeiten wie 4 zusätzliche Male eingeben. Und jedes Mal, wenn sich die Abhängigkeiten ändern, können Sie sie auch im Werk ändern. Es fühlt sich an, als würde ich nicht einmal mehr einen DI-Container verwenden. (Ich denke, Castle Windsor hat eine Lösung dafür [mit seinen eigenen Nachteilen, korrigieren Sie mich, wenn ich falsch liege]).
  • Machen Sie etwas mit anonymen Typen oder einem Wörterbuch. Ich mag meine statische Eingabe.

Also ja. Das Mischen von Zustand und Verhalten auf diese Weise schafft ein Problem, das in MVC überhaupt nicht existiert. Und ich habe das Gefühl, dass es derzeit keine wirklich angemessene Lösung für dieses Problem gibt. Nun möchte ich einige Dinge beobachten:

  • Die Leute benutzen tatsächlich MVVM. Entweder kümmern sie sich nicht um alles, oder sie haben eine brillante andere Lösung.
  • Ich habe kein detailliertes Beispiel für MVVM mit WPF gefunden. Zum Beispiel hat mir das NDDD-Beispielprojekt sehr geholfen, einige DDD-Konzepte zu verstehen. Es würde mir sehr gefallen, wenn mich jemand in eine ähnliche Richtung wie MVVM / WPF lenken könnte.
  • Vielleicht mache ich MVVM falsch und ich sollte mein Design auf den Kopf stellen. Vielleicht sollte ich dieses Problem überhaupt nicht haben. Nun, ich weiß, dass andere Leute die gleiche Frage gestellt haben, also denke ich, dass ich nicht der einzige bin.

Zusammenfassen

  • Bin ich zu Recht der Ansicht, dass das ViewModel als Integrationspunkt für Status und Verhalten der Grund für einige Schwierigkeiten mit dem MVVM-Muster insgesamt ist?
  • Ist die Verwendung des abstrakten Factory-Musters die einzige / beste Möglichkeit, ein ViewModel statisch zu instanziieren?
  • Gibt es so etwas wie eine ausführliche Referenzimplementierung?
  • Ist es ein Designgeruch, viele ViewModels mit beiden Zuständen / Verhaltensweisen zu haben?
dvdvorle
quelle
10
Das ist zu lang, um es zu lesen, überlegen Sie, es gibt eine Menge irrelevanter Dinge. Sie könnten gute Antworten verpassen, weil die Leute sich nicht die Mühe machen, das alles zu lesen.
Yannis
Sie sagten, Sie lieben Caliburn.Micro, wissen aber nicht, wie Sie mit diesem Framework neue Ansichtsmodelle erstellen können? Überprüfen Sie einige Beispiele.
Euphorischer
@Euphoric Könnten Sie etwas genauer sein, Google scheint mir hier nicht zu helfen. Hast du ein paar Suchbegriffe, nach denen ich suchen könnte?
DVDVorle
3
Ich denke, Sie vereinfachen MVC ein bisschen. Sicher, in der Ansicht wird das Modell am Anfang angezeigt, aber während des Betriebs ändert sich der Status. Dieser sich ändernde Zustand ist meiner Meinung nach ein "Edit Model". Das heißt, eine abgeflachte Version des Modells mit reduzierten Konsistenzbeschränkungen. Tatsächlich ist das, was ich als Bearbeitungsmodell bezeichne, das MVVM ViewModel. Es behält den Status während des Übergangs bei, der zuvor entweder von der Ansicht in MVC gehalten oder in eine nicht festgeschriebene Version des Modells verschoben wurde, zu der es meines Erachtens nicht gehört. Sie hatten also vorher den Zustand "im Fluss". Jetzt ist alles im ViewModel.
Scott Whitlock
@ScottWhitlock Ich vereinfache MVC in der Tat. Aber ich sage nicht, dass es falsch ist, dass der Status "in Flux" im ViewModel ist. Ich sage, dass es auch schwieriger ist, das ViewModel auf einen verwendbaren Status zu initialisieren, beispielsweise einen anderen ViewModel. Ihr "Modell bearbeiten" in MVC kann sich nicht selbst speichern (es verfügt nicht über eine Speichermethode). Aber der Controller weiß das und hat alle Abhängigkeiten, um das zu tun.
dvdvorle

Antworten:

2

Das Problem der Abhängigkeiten beim Einleiten eines neuen Ansichtsmodells kann mit IOC behandelt werden.

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

Beim Aufstellen des Containers ...

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

Wenn Sie Ihr Ansichtsmodell benötigen:

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

Bei der Verwendung eines Frameworks wie Caliburn Micro ist häufig bereits eine Form von IOC-Containern vorhanden.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);
Mike
quelle
1

Ich arbeite täglich mit ASP.NET MVC und arbeite seit über einem Jahr an einem WPF. Und so sehe ich das:

MVC

Der Controller soll Aktionen orchestrieren (holen Sie dies, fügen Sie das hinzu).

Die Ansicht ist für die Anzeige des Modells verantwortlich.

Das Modell umfasst in der Regel Daten (z. B. Benutzer-ID, Vorname) sowie den Status (z. B. Titel) und ist in der Regel ansichtsspezifisch.

MVVM

Das Modell enthält normalerweise nur Daten (z. B. Benutzer-ID, Vorname) und wird normalerweise weitergegeben

Das Ansichtsmodell umfasst das Verhalten der Ansicht (Methoden), ihrer Daten (Modell) und Interaktionen (Befehle) - ähnlich dem aktiven MVP-Muster, bei dem der Präsentator das Modell kennt. Das Ansichtsmodell ist ansichtsspezifisch (1 Ansicht = 1 Ansichtsmodell).

Die Ansicht ist für die Anzeige von Daten und die Datenbindung an das Ansichtsmodell verantwortlich. Beim Erstellen einer Ansicht wird normalerweise das zugehörige Ansichtsmodell erstellt.


Was Sie beachten sollten, ist, dass das MVVM-Präsentationsmuster aufgrund seiner Datenbindung für WPF / Silverlight spezifisch ist.

Die Ansicht weiß normalerweise, welchem ​​Ansichtsmodell sie zugeordnet ist (oder eine Abstraktion von einem).

Ich würde empfehlen, dass Sie das Ansichtsmodell als Singleton behandeln, auch wenn es pro Ansicht instanziiert wird. Mit anderen Worten, Sie sollten in der Lage sein, es über DI über einen IOC-Container zu erstellen und entsprechende Methoden aufzurufen. Laden Sie das Modell basierend auf Parametern. Etwas wie das:

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

In diesem Fall würden Sie beispielsweise kein für den zu aktualisierenden Benutzer spezifisches Ansichtsmodell erstellen. Stattdessen würde das Modell benutzerspezifische Daten enthalten, die durch einen Aufruf des Ansichtsmodells geladen werden.

Shelakel
quelle
Wenn mein Vorname "Peter" ist und meine Titel {"Rev", "Dr"} * lauten, warum berücksichtigen Sie dann die Daten zum Vornamen und den Titelstatus? Oder können Sie Ihr Beispiel erläutern? * nicht wirklich
Pete Kirkham
@PeteKirkham - das Beispiel für "Titel", auf das ich mich im Kontext einer Combobox bezog. Im Allgemeinen wird beim Senden von Informationen, die beibehalten werden sollen, nicht der Bundesstaat gesendet (z. B. eine Liste von Bundesstaaten / Provinzen / Titeln), aus dem eine Auswahl getroffen wurde. Jeder sinnvolle Status, der mit den Daten übertragen werden soll (z. B. der verwendete Benutzername), sollte zum Zeitpunkt der Verarbeitung überprüft werden, da der Status möglicherweise veraltet ist (wenn Sie ein asynchrones Muster wie das Einreihen von Nachrichten in die Warteschlange verwenden).
Shelakel
Obwohl seit diesem Beitrag zwei Jahre vergangen sind, muss ich einen Kommentar für zukünftige Zuschauer abgeben: Zwei Dinge haben mich bei Ihrer Antwort gestört. Eine Ansicht entspricht möglicherweise einem ViewModel, aber ein ViewModel kann durch mehrere Ansichten dargestellt werden. Zweitens beschreiben Sie das Service Locator-Anti-Pattern. IMHO sollten Sie Viewmodels nicht überall direkt auflösen. Dafür ist der DI da. Treffen Sie Ihre Entscheidungen an weniger Punkten als möglich. Lassen Sie Caliburn dies zum Beispiel für Sie erledigen.
Jony Adamit
1

Kurze Antwort auf Ihre Fragen:

  1. Ja, Zustand + Verhalten führt zu diesen Problemen, aber dies gilt für alle OO. Der eigentliche Schuldige ist die Kopplung von ViewModels, die eine Art SRP-Verstoß darstellt.
  2. Wahrscheinlich statisch geschrieben. Sie sollten jedoch die Notwendigkeit, ViewModels von anderen ViewModels aus zu instanziieren, verringern / beseitigen.
  3. Nicht, dass ich es wüsste.
  4. Nein, aber ViewModels mit nicht verbundenem Status und Verhalten (wie einige Modellreferenzen und einige ViewModel-Referenzen)

Die lange Version:

Wir stehen vor dem gleichen Problem und haben einige Dinge gefunden, die Ihnen helfen können. Obwohl ich die "magische" Lösung nicht kenne, lindern diese Dinge die Schmerzen ein wenig.

  1. Implementieren Sie bindbare Modelle von DTOs zur Änderungsverfolgung und -validierung. Diese "Data" -ViewModels dürfen nicht von Diensten abhängen und stammen nicht aus dem Container. Sie können einfach "neu" aufgesetzt, weitergegeben und sogar vom DTO abgeleitet werden. Fazit ist, ein für Ihre Anwendung spezifisches Modell (wie MVC) zu implementieren.

  2. Entkoppeln Sie Ihre ViewModels. Caliburn erleichtert das Zusammenkoppeln der ViewModels. Es schlägt es sogar durch sein Screen / Conductor-Modell vor. Diese Kopplung erschwert jedoch den Komponententest der ViewModels, führt zu zahlreichen Abhängigkeiten und ist am wichtigsten: Die Verwaltung des ViewModel-Lebenszyklus wird Ihren ViewModels auferlegt. Eine Möglichkeit, sie zu entkoppeln, ist die Verwendung eines Navigationsdienstes oder eines ViewModel-Controllers. Z.B

    öffentliche Schnittstelle IShowViewModels {void Show (Objekt inlineArgumentsAsAnonymousType, Zeichenfolge regionId); }

Noch besser ist es, dies durch irgendeine Form von Nachrichten zu tun. Es ist jedoch wichtig, den ViewModel-Lebenszyklus nicht von anderen ViewModels aus zu verarbeiten. In MVC sind Controller nicht voneinander abhängig, und in MVVM sollten ViewModels nicht voneinander abhängig sein. Integrieren Sie sie auf andere Weise.

  1. Verwenden Sie Ihre Container "stringly" -Typ / dynamische Funktionen. Obwohl es möglich sein könnte, so etwas zu erstellen INeedData<T1,T2,...>und typsichere Erstellungsparameter zu erzwingen, lohnt es sich nicht. Auch das Erstellen von Fabriken für jeden ViewModel-Typ lohnt sich nicht. Die meisten IoC-Container bieten hierfür Lösungen. Sie werden zur Laufzeit Fehler bekommen, aber die Entkopplung und die Testbarkeit der Einheiten sind es wert. Sie führen immer noch eine Art Integrationstest durch, und diese Fehler können leicht entdeckt werden.
Sanosdole
quelle
0

So wie ich das normalerweise mache (mit PRISM), enthält jede Assembly ein Container-Initialisierungsmodul, in dem alle Schnittstellen und Instanzen beim Start registriert werden.

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

Und in Anbetracht Ihrer Beispielklassen würde dies so implementiert, dass der Container vollständig durchgereicht wird. Auf diese Weise können neue Abhängigkeiten einfach hinzugefügt werden, da Sie bereits Zugriff auf den Container haben.

/// <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)
    {
    }
}

Es ist durchaus üblich, eine ViewModelBase-Klasse zu haben, von der alle Ansichtsmodelle abgeleitet sind und die einen Verweis auf den Container enthält. Solange Sie sich angewöhnen, alle Ansichtsmodelle anstelle von new()'ingdiesen aufzulösen, sollte dies die Auflösung von Abhängigkeiten erheblich vereinfachen.

Martin Cooper
quelle
0

Manchmal ist es besser, zur einfachsten Definition zu gehen, als zu einem vollständigen Beispiel: http://en.wikipedia.org/wiki/Model_View_ViewModel Vielleicht ist das Lesen des ZK Java-Beispiels aufschlussreicher als das in C #.

Andere Male höre auf deinen Bauchgefühl ...

Ist es ein Designgeruch, viele ViewModels mit beiden Zuständen / Verhaltensweisen zu haben?

Sind Ihre Modelle Objekt-pro-Tabelle-Zuordnungen? Möglicherweise hilft ein ORM bei der Zuordnung zu Domänenobjekten, während das Geschäft abgewickelt oder mehrere Tabellen aktualisiert werden.

Gerry King
quelle