Come rendere meno dolorosa la creazione di modelli di visualizzazione in fase di esecuzione


17

Chiedo scusa per la lunga domanda, che legge un po 'come uno sfogo, ma prometto che non lo è! Di seguito ho riassunto le mie domande

Nel mondo MVC, le cose sono semplici. Il modello ha stato, la vista mostra il modello e il controller fa cose al / con il modello (in pratica), un controller non ha stato. Per fare cose il Controller ha alcune dipendenze dai servizi web, dal repository, dal lotto. Quando si crea un'istanza di un controller, ci si preoccupa di fornire tali dipendenze, nient'altro. Quando si esegue un'azione (metodo sul controller), si utilizzano tali dipendenze per recuperare o aggiornare il modello o chiamare un altro servizio di dominio. Se c'è qualche contesto, supponiamo che alcuni utenti vogliano vedere i dettagli di un particolare oggetto, si passa l'ID di quell'elemento come parametro all'Azione. Da nessuna parte nel controller c'è alcun riferimento a qualsiasi stato. Fin qui tutto bene.

Inserisci MVVM. Adoro WPF, adoro l'associazione dei dati. Adoro i framework che rendono ancora più semplice l'associazione dei dati a ViewModels (utilizzando Caliburn Micro atm). Sento che le cose sono meno semplici in questo mondo però. Facciamo l'esercizio di nuovo: il modello è stato, la vista mostra il ViewModel, e il ViewModel fa roba per / con il modello (in pratica), un ViewModel fa avere stato! (per chiarire; forse delega tutte le proprietà di uno o più modelli, ma che significa che deve avere un riferimento al modello un modo o nell'altro, che è stato in sé) Per fareroba il ViewModel ha alcune dipendenze da servizi web, repository, il lotto. Quando si crea un'istanza di ViewModel, ci si preoccupa di fornire tali dipendenze, ma anche lo stato. E questo, onorevoli colleghi, mi infastidisce senza fine.

Ogni volta che devi istanziare a ProductDetailsViewModeldal ProductSearchViewModel(da cui hai chiamato il ProductSearchWebServiceche a sua volta è tornato IEnumerable<ProductDTO>, tutti sono ancora con me?), Puoi fare una di queste cose:

  • chiama new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, questo è male, immagina altre 3 dipendenze, questo significa che anche i ProductSearchViewModelbisogni devono assumersi. Anche cambiare il costruttore è doloroso.
  • chiamata _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, la fabbrica è solo un Func, sono facilmente generati dalla maggior parte dei framework IoC. Penso che questo sia negativo perché i metodi Init sono un'astrazione che perde. Inoltre, non è possibile utilizzare la parola chiave readonly per i campi impostati nel metodo Init. Sono sicuro che ci sono alcune ragioni in più.
  • chiama _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);Quindi ... questo è il modello (fabbrica astratta) che di solito è raccomandato per questo tipo di problema. Ho pensato che fosse geniale poiché soddisfa la mia brama di scrittura statica, fino a quando non ho iniziato a usarlo. La quantità di codice boilerplate è che penso troppo (sai, a parte i ridicoli nomi delle variabili che uso). Per ogni ViewModel che necessita di parametri di runtime otterrai due file extra (interfaccia di fabbrica e implementazione) e dovrai digitare le dipendenze non di runtime come 4 volte extra. E ogni volta che cambiano le dipendenze, puoi cambiarle anche in fabbrica. Mi sembra di non usare più nemmeno un contenitore DI. (Penso che Castle Windsor abbia una sorta di soluzione per questo [con i suoi svantaggi, correggimi se sbaglio]).
  • fare qualcosa con tipi o dizionario anonimi. Mi piace la mia digitazione statica.

Quindi si La miscelazione di stato e comportamento in questo modo crea un problema che non esiste affatto in MVC. E sento che al momento non esiste una soluzione davvero adeguata per questo problema. Ora vorrei osservare alcune cose:

  • Le persone usano effettivamente MVVM. Quindi o non si preoccupano di tutto quanto sopra, o hanno qualche altra brillante soluzione.
  • Non ho trovato un esempio approfondito di MVVM con WPF. Ad esempio, il progetto di esempio NDDD mi ha aiutato immensamente a comprendere alcuni concetti di DDD. Mi piacerebbe davvero che qualcuno potesse indicarmi qualcosa di simile per MVVM / WPF.
  • Forse sto sbagliando MVVM e dovrei capovolgere il mio design. Forse non dovrei avere questo problema. Beh, so che altre persone hanno posto la stessa domanda, quindi penso di non essere il solo.

Riassumere

  • Ho ragione a concludere che avere ViewModel come punto di integrazione sia per stato che per comportamento è la ragione di alcune difficoltà con il modello MVVM nel suo insieme?
  • L'uso del modello astratto di fabbrica è l'unico / miglior modo per creare un'istanza di un ViewModel in modo tipicamente statico?
  • È disponibile qualcosa come un'implementazione di riferimento approfondita?
  • Avere un sacco di ViewModels con sia stato / comportamento ha un odore di design?

10
È troppo lungo da leggere, prendere in considerazione una revisione, ci sono molte cose irrilevanti. Potresti perdere buone risposte perché le persone non si preoccuperanno di leggere tutto ciò.
yannis,

Hai detto di amare Caliburn.Micro, ma non sai come questo framework può aiutarti a creare un'istanza di nuovi modelli di visualizzazione? Guarda alcuni esempi.
Euforico,

@Euforico Potresti essere un po 'più specifico, Google non sembra aiutarmi qui. Hai delle parole chiave che potrei cercare?
dvdvorle,

3
Penso che stai semplificando un po 'MVC. Sicuramente la vista mostra il modello all'inizio, ma durante il funzionamento cambia stato. Questo stato mutevole è, a mio avviso, un "Modifica modello". Cioè, una versione appiattita del modello con restrizioni di coerenza ridotte. In effetti, ciò che chiamo un modello di modifica è MVVM ViewModel. Mantiene lo stato durante la transizione, che era precedentemente detenuto da View in MVC, o reinserito in una versione non impegnata del Modello, dove non credo che appartenga. Quindi prima avevi dichiarato "in flusso". Ora è tutto nel ViewModel.
Scott Whitlock,

@ScottWhitlock Sto davvero semplificando MVC. Ma non sto dicendo che è sbagliato che lo stato "in flusso" sia nel ViewModel, sto dicendo che stipare il comportamento anche lì rende più difficile inizializzare il ViewModel a uno stato utilizzabile, per esempio un altro ViewModel. Il tuo "Modifica modello" in MVC non sa come salvare se stesso (non ha un metodo Save). Ma il controller lo sa e ha tutte le dipendenze necessarie per farlo.
dvdvorle,

Risposte:


2

Il problema delle dipendenze all'avvio di un nuovo modello di vista può essere gestito con 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;
  }
}

Durante l'installazione del contenitore ...

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

Quando hai bisogno del tuo modello di visualizzazione:

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

Quando si utilizza un framework come Caliburn Micro , è spesso presente una qualche forma di contenitore IOC.

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

1

Lavoro quotidianamente con ASP.NET MVC e ho lavorato su un WPF per oltre un anno ed è così che lo vedo:

MVC

Il controller dovrebbe orchestrare le azioni (recuperare questo, aggiungerlo).

La vista è responsabile della visualizzazione del modello.

Il modello comprende in genere dati (es. UserId, FirstName) nonché stato (es. Titoli) e di solito è specifico per la visualizzazione.

MVVM

Il modello in genere contiene solo dati (es. UserId, FirstName) e di solito viene passato in giro

Il modello di vista comprende il comportamento della vista (metodi), i suoi dati (modello) e le interazioni (comandi) - simile al modello MVP attivo in cui il relatore è a conoscenza del modello. Il modello della vista è specifico della vista (1 vista = 1 vista modello).

La vista è responsabile della visualizzazione e dell'associazione dei dati al modello di vista. Quando viene creata una vista, di solito viene creato il modello di vista associato.


Quello che dovresti ricordare è che il modello di presentazione MVVM è specifico di WPF / Silverlight a causa della loro natura di associazione dei dati.

La vista in genere sa a quale modello di vista è associato (o un'astrazione di uno).

Vi consiglierei di trattare il modello di vista come un singleton, anche se è istanziato per vista. In altre parole, dovresti essere in grado di crearlo tramite DI tramite un contenitore IOC e chiamare metodi appropriati su di esso per dire; carica il suo modello in base a parametri. Qualcosa come questo:

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

Ad esempio, in questo caso, non si creerebbe un modello di visualizzazione specifico per l'utente che si sta aggiornando, ma conterrà i dati specifici dell'utente che vengono caricati tramite una chiamata sul modello di visualizzazione.


Se il mio nome è "Peter" e i miei titoli sono {"Rev", "Dr"} *, perché consideri i dati di FirstName e lo stato del titolo? O puoi chiarire il tuo esempio? * non proprio
Pete Kirkham il

@PeteKirkham - l'esempio dei "titoli" a cui mi riferivo nel contesto di dire una casella combinata. Generalmente quando si inviano informazioni da persistere non si invierà lo stato (es. Un elenco di stati / province / titoli) utilizzato per effettuare selezioni. Qualsiasi stato utile da trasferire con i dati (ad es. È il nome utente in uso) deve essere verificato al momento dell'elaborazione poiché lo stato potrebbe essere diventato obsoleto (se si utilizzava un modello asincrono come l'accodamento dei messaggi).
Shelakel

Anche se sono passati due anni da questo post, devo fare un commento a favore dei futuri spettatori: due cose mi hanno disturbato con la tua risposta. Una vista forse corrisponde a un ViewModel, ma un ViewModel può essere rappresentato da più viste. In secondo luogo, quello che stai descrivendo è l'anti-pattern di Service Locator. IMHO non dovresti risolvere direttamente i modelli di visualizzazione ovunque. Ecco a cosa serve il DI. Fai le tue risoluzioni in meno punti possibile. Lascia che Caliburn faccia questo lavoro per te, per esempio.
Jony Adamit,

1

Risposta breve per le tue domande:

  1. Sì State + Behavior porta a questi problemi, ma questo è vero per tutto OO. Il vero colpevole è l'accoppiamento di ViewModels che è una specie di violazione di SRP.
  2. Digitato staticamente, probabilmente. Tuttavia, è necessario ridurre / eliminare la necessità di creare istanze di ViewModels da altri ViewModels.
  3. Non che ne sia consapevole.
  4. No, ma con ViewModels con stato e comportamento non correlati (come alcuni riferimenti Model e alcuni riferimenti ViewModel)

La versione lunga:

Stiamo affrontando lo stesso problema e abbiamo trovato alcune cose che potrebbero aiutarti. Sebbene non conosca la soluzione "magica", quelle cose stanno alleviando un po 'il dolore.

  1. Implementare modelli associabili da DTO per il rilevamento e la convalida delle modifiche. Quei modelli "Dati" -View non devono dipendere dai servizi e non provengono dal contenitore. Possono essere semplicemente "nuovi", fatti passare e possono persino derivare dal DTO. La linea di fondo è implementare un modello specifico per l'applicazione (come MVC).

  2. Disaccoppia i tuoi ViewModels. Caliburn semplifica l'accoppiamento dei ViewModels. Lo suggerisce anche attraverso il suo modello di schermo / conduttore. Ma questo accoppiamento rende i ViewModel difficili da testare, creare molte dipendenze e soprattutto: impone l'onere della gestione del ciclo di vita di ViewModel sui ViewModel. Un modo per separarli è usare qualcosa come un servizio di navigazione o un controller ViewModel. Per esempio

    interfaccia pubblica IShowViewModels {void Show (object inlineArgumentsAsAnonymousType, string regionId); }

Ancora meglio sta facendo questo con qualche forma di messaggistica. Ma la cosa importante non è gestire il ciclo di vita di ViewModel da altri ViewModel. Nei controller MVC non dipendono l'uno dall'altro e in MVVM ViewModels non dovrebbero dipendere l'uno dall'altro. Integrali attraverso altri modi.

  1. Usa le funzionalità "stringhe" di tipo contenitore / dinamiche. Sebbene sia possibile creare qualcosa di simile INeedData<T1,T2,...>e applicare parametri di creazione sicuri per i tipi, non ne vale la pena. Anche la creazione di fabbriche per ogni tipo di ViewModel non vale la pena. La maggior parte dei container IoC fornisce soluzioni a questo. Riceverai errori in fase di esecuzione ma ne vale la pena disaccoppiamento e testabilità dell'unità. Esegui ancora una sorta di test di integrazione e questi errori vengono individuati facilmente.

0

Il modo in cui lo faccio di solito (usando PRISM) è che ogni assembly contiene un modulo di inizializzazione del contenitore, in cui tutte le interfacce e le istanze sono registrate all'avvio.

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

E date le tue classi di esempio, verrebbero implementate in questo modo, con il contenitore che passerà fino in fondo. In questo modo è possibile aggiungere facilmente eventuali nuove dipendenze poiché si ha già accesso al contenitore.

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

È abbastanza comune avere una classe ViewModelBase, da cui derivano tutti i modelli di vista, che contiene un riferimento al contenitore. Fintanto che prendi l'abitudine di risolvere tutti i modelli di visualizzazione invece di new()'ingloro, dovrebbe rendere molto più semplice la risoluzione di tutte le dipendenze.


0

A volte è bene passare alla definizione più semplice piuttosto che a un esempio completo: http://en.wikipedia.org/wiki/Model_View_ViewModel forse leggere l'esempio ZK Java è più illuminante di quello C #.

Altre volte ascolta il tuo istinto ...

Avere un sacco di ViewModels con sia stato / comportamento ha un odore di design?

I tuoi modelli sono mappature oggetto per tabella? Forse un ORM aiuterebbe a mappare gli oggetti di dominio durante la gestione dell'azienda o l'aggiornamento di più tabelle.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.