Come rendere meno dolorosa la creazione di viewmodels in fase di esecuzione

17

Mi scuso per la lunga domanda, si legge un po 'come una sfuriata, ma prometto che non lo è! Ho riassunto la mia domanda / e di seguito

Nel mondo MVC, le cose sono semplici. Il modello ha lo stato, la vista mostra il modello e il controllore fa cose a / con il modello (in pratica), un controllore non ha stato. Per fare il controller ha alcune dipendenze da servizi web, repository, il lotto. Quando installi un controller a cui tieni a fornire tali dipendenze, nient'altro. Quando si esegue un'azione (metodo su Controller), si utilizzano tali dipendenze per recuperare o aggiornare il modello o chiamare un altro servizio di dominio. Se c'è un contesto, dì che alcuni utenti vogliono 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.

Immettere MVVM. Amo WPF, adoro il binding dei dati. Adoro i framework che rendono ancora più semplice l'associazione dei dati a ViewModels (utilizzando Caliburn Micro a.t.m.). Sento che le cose sono meno dirette in questo mondo. Facciamo di nuovo l'esercizio: il Modello ha lo stato, il View mostra ViewModel, e il ViewModel fa cose a / con il Modello (in pratica), un ViewModel ha lo stato! (per chiarire, forse delega tutte le proprietà a uno o più Modelli, ma ciò significa che deve avere un riferimento al modello in un modo o nell'altro, che è lo stato in sé) Per fare riempire il ViewModel ha alcune dipendenze da servizi web, repository, il lotto. Quando installi un ViewModel a cui tieni a fornire tali dipendenze, ma anche lo stato. E questo, signore e signori, mi infastidisce fino alla fine.

Ogni volta che hai bisogno di istanziare un ProductDetailsViewModel da ProductSearchViewModel (da cui hai chiamato ProductSearchWebService che a sua volta ha restituito 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 ProductSearchViewModel deve assumere anche queste dipendenze. Anche cambiare il costruttore è doloroso.
  • chiama _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO); , il factory è solo un Func, sono facilmente generati dalla maggior parte dei framework IoC. Penso che questo sia male perché i metodi Init sono un'astrazione che perde. Non è inoltre possibile utilizzare la parola chiave readonly per i campi impostati nel metodo Init. Sono sicuro che ci sono altri motivi.
  • chiama _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO); Quindi ... questo è il modello (factory astratto) che di solito è raccomandato per questo tipo di problema. Pensavo fosse geniale dal momento che soddisfaceva la mia brama di digitazione statica, finché non ho iniziato a usarlo. La quantità di codice boilerplate è penso troppo (sai, a parte i ridicoli nomi delle variabili che uso). Per ogni ViewModel che richiede i parametri di runtime, si otterranno due file aggiuntivi (interfaccia di fabbrica e implementazione) e sarà necessario digitare le dipendenze non di runtime come 4 volte in più. E ogni volta che cambiano le dipendenze, puoi cambiarlo anche in fabbrica. Sembra che non usi più nemmeno un contenitore DI. (I pensa Castle Windsor ha qualche tipo di soluzione per questo [con i suoi stessi svantaggi, correggimi se sbaglio]).
  • fai qualcosa con tipi o dizionari anonimi. Mi piace la mia digitazione statica.

Quindi, sì. Lo stato e il comportamento di miscelazione in questo modo crea un problema che non esiste affatto in MVC. E mi sembra che al momento non ci sia una soluzione adeguata per questo problema. Ora mi piacerebbe osservare alcune cose:

  • Le persone utilizzano effettivamente MVVM. Quindi 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 campione NDDD mi ha aiutato immensamente a comprendere alcuni concetti DDD. Mi piacerebbe davvero che qualcuno mi indicasse qualcosa di simile a MVVM / WPF.
  • Forse sto facendo l'MVVM in modo sbagliato e dovrei rovesciare il mio progetto. Forse non dovrei avere questo problema. Beh, so che altre persone hanno fatto la stessa domanda quindi penso di non essere l'unico.

Per riepilogare

  • Sono corretto nel concludere che il fatto che ViewModel sia un punto di integrazione sia per lo stato sia per il comportamento è la causa di alcune difficoltà con il pattern MVVM nel suo complesso?
  • Sta usando il pattern factory astratto l'unico / miglior modo per istanziare un ViewModel in modo tipizzato staticamente?
  • Esiste qualcosa come un'implementazione di riferimento approfondita disponibile?
  • C'è un sacco di ViewModels con uno stato / comportamento un odore di design?
posta dvdvorle 02.11.2012 - 11:46
fonte

5 risposte

2

Il problema delle dipendenze durante l'avvio di un nuovo modello di visualizzazione 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'impostazione del contenitore ...

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

Quando ti serve il tuo modello di visualizzazione:

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

Quando utilizzi un framework come caliburn micro spesso è già presente qualche forma di contenitore IOC.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);
    
risposta data 04.03.2013 - 18:40
fonte
1

Lavoro quotidianamente con ASP.NET MVC e ho lavorato su un WPF da oltre un anno e questo è il modo in cui lo vedo:

MVC

Il controllore dovrebbe orchestrare le azioni (prendi questo, aggiungilo).

La visualizzazione è responsabile della visualizzazione del modello.

Il modello in genere comprende dati (ad esempio UserId, FirstName) nonché lo stato (ad esempio Titoli) e di solito è specifico della vista.

MVVM

Solitamente il modello contiene solo dati (ad esempio UserId, FirstName) e di solito viene passato intorno

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

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

Ciò che dovresti ricordare è che il pattern di presentazione MVVM è specifico per WPF / Silverlight a causa della loro natura di associazione dei dati.

La vista in genere conosce il modello di vista a cui è associato (o un'astrazione di uno).

Ti consiglierei di trattare il modello di visualizzazione come un singleton, anche se è istanziato per visualizzazione. In altre parole, dovresti essere in grado di crearlo tramite DI tramite un contenitore IOC e chiamare su di esso i metodi appropriati per dirlo; caricare il suo modello in base ai parametri. Qualcosa del genere:

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

Come esempio in questo caso, non creerai un modello di visualizzazione specifico per l'utente che si sta aggiornando, invece il modello conterrà dati specifici dell'utente che verranno caricati tramite alcune chiamate sul modello di visualizzazione.

    
risposta data 06.03.2013 - 09:24
fonte
1

Risposta breve per le tue domande:

  1. Sì Stato + Il comportamento porta a questi problemi, ma questo è vero per tutti gli OO. Il vero colpevole è l'accoppiamento di ViewModels che è una specie di violazione SRP.
  2. Scritto staticamente, probabilmente. Ma dovresti ridurre / eliminare la necessità di istanziare ViewModels da altri ViewModels.
  3. Non che io sappia.
  4. No, ma con ViewModels con stato e ampli non collegati; comportamento (come alcuni riferimenti a modelli e alcuni riferimenti a ViewModel)

La versione lunga:

Siamo di fronte allo stesso problema e abbiamo trovato alcune cose che potrebbero aiutarti. Anche se non conosco la soluzione "magica", quelle cose stanno attenuando un po 'il dolore.

  1. Implementa modelli associabili da DTO per il rilevamento e la convalida delle modifiche. Questi "Data" -ViewModels non devono dipendere dai servizi e non provengono dal contenitore. Possono essere solo "nuovi" ed escogitati, potrebbero persino derivare dal DTO. La linea di fondo consiste nell'implementare un modello specifico per la tua applicazione (come MVC).

  2. Dissocia i tuoi ViewModels. Caliburn rende facile accoppiare i ViewModels insieme. Lo suggerisce persino attraverso il suo modello Screen / Conductor. Ma questo accoppiamento rende i ViewModel difficili da testare, crea molte dipendenze e il più importante: impone il peso della gestione del ciclo di vita di ViewModel su ViewModels. Un modo per separarli consiste nell'utilizzare qualcosa come un servizio di navigazione o un controller ViewModel. Per esempio.

    interfaccia pubblica IShowViewModels {      void Show (oggetto inlineArgumentsAsOnonymousType, string regionId); }

Ancora meglio è farlo con una qualche forma di messaggistica. Ma l'importante è non gestire il ciclo di vita di ViewModel da altri ViewModels. Nei controller MVC non dipendono l'uno dall'altro, e in MVVM ViewModels non dovrebbe dipendere l'uno dall'altro. Integrali in altri modi.

  1. Utilizza le tue caratteristiche "a stringhe" / dinamiche dei contenitori. Sebbene sia possibile creare qualcosa come INeedData<T1,T2,...> e applicare i 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 contenitori IoC fornisce soluzioni a questo. Otterrai errori in fase di esecuzione ma ne valuterà la de-accoppiamento e la testabilità dell'unità. Continui a fare qualche tipo di test di integrazione e quegli errori sono facilmente individuabili.
risposta data 30.06.2016 - 00:33
fonte
0

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

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

E date le classi di esempio, sarebbero implementate in questo modo, con il contenitore che viene passato fino in fondo. In questo modo è possibile aggiungere facilmente nuove dipendenze poiché hai 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 visualizzazione, che contiene un riferimento al contenitore. Finché hai preso l'abitudine di risolvere tutti i modelli di visualizzazione anziché new()'ing , dovrebbe rendere la risoluzione delle dipendenze molto più semplice.

    
risposta data 04.03.2013 - 19:03
fonte
0

A volte è meglio andare alla definizione più semplice piuttosto che a un esempio completo: link forse leggere l'esempio di ZK Java è più illuminante di quello di C #.

Altre volte ascolta il tuo istinto ...

Is having a lot of ViewModels with both state/behavior a design smell?

I tuoi modelli sono oggetti per mapping di tabelle? Forse un ORM aiuterebbe la mappatura degli oggetti del dominio durante la gestione dell'azienda o l'aggiornamento di più tabelle.

    
risposta data 10.03.2013 - 11:57
fonte

Leggi altre domande sui tag