2009-04-23 2 views
26

Sto sperimentando MVVM per la prima volta e mi piace molto la separazione delle responsabilità. Naturalmente qualsiasi schema di progettazione risolve solo molti problemi, non tutti. Quindi sto cercando di capire dove memorizzare lo stato dell'applicazione e dove archiviare i comandi a livello di applicazione.Dove memorizzare le impostazioni/lo stato dell'applicazione in un'applicazione MVVM

Diciamo che la mia applicazione si collega a un URL specifico. Ho un ConnectionWindow e un ConnectionViewModel che supportano la raccolta di queste informazioni da parte dell'utente e il richiamo di comandi per connettersi all'indirizzo. La prossima volta che l'applicazione si avvia, desidero riconnettermi allo stesso indirizzo senza chiedere conferma all'utente.

La mia soluzione finora è creare un ApplicationViewModel che fornisca un comando per connettersi a un indirizzo specifico e per salvare quell'indirizzo in una memoria persistente (dove è effettivamente salvato è irrilevante per questa domanda). Di seguito è riportato un modello di classe abbreviato.

visualizzazione applicazione del modello:

public class ApplicationViewModel : INotifyPropertyChanged 
{ 
    public Uri Address{ get; set; } 
    public void ConnectTo(Uri address) 
    { 
     // Connect to the address 
     // Save the addres in persistent storage for later re-use 
     Address = address; 
    } 

    ... 
} 

La vista di collegamento modello:

public class ConnectionViewModel : INotifyPropertyChanged 
{ 
    private ApplicationViewModel _appModel; 
    public ConnectionViewModel(ApplicationViewModel model) 
    { 
     _appModel = model; 
    } 

    public ICommand ConnectCmd 
    { 
     get 
     { 
      if(_connectCmd == null) 
      { 
       _connectCmd = new LambdaCommand(
        p => _appModel.ConnectTo(Address), 
        p => Address != null 
        ); 
      } 
      return _connectCmd; 
     } 
    }  

    public Uri Address{ get; set; } 

    ... 
} 

Quindi la domanda è questa: un ApplicationViewModel è il modo giusto per gestire questa situazione? In quale altro modo è possibile memorizzare lo stato dell'applicazione?

MODIFICA: Mi piacerebbe sapere anche come questo influenzi la testabilità. Uno dei motivi principali per l'utilizzo di MVVM è la possibilità di testare i modelli senza un'applicazione host. Nello specifico, sono interessato a capire in che modo le impostazioni centralizzate dell'app influiscono sulla testabilità e sulla capacità di prendere in giro i modelli dipendenti.

risposta

10

Se non si utilizza M-V-VM, la soluzione è semplice: si inseriscono questi dati e funzionalità nel tipo derivato dall'applicazione. Application.Current ti dà quindi accesso. Il problema qui, come sapete, è che Application.Current causa problemi quando l'unità verifica il ViewModel. Questo è ciò che deve essere risolto. Il primo passo è separare noi stessi da un'istanza di applicazione concreta. Fallo definendo un'interfaccia e implementandola sul tuo concreto tipo di applicazione.

public interface IApplication 
{ 
    Uri Address{ get; set; } 
    void ConnectTo(Uri address); 
} 

public class App : Application, IApplication 
{ 
    // code removed for brevity 
} 

Ora il passo successivo è quello di eliminare la chiamata al Application.Current all'interno del ViewModel utilizzando Inversion of Control o il Service Locator.

public class ConnectionViewModel : INotifyPropertyChanged 
{ 
    public ConnectionViewModel(IApplication application) 
    { 
    //... 
    } 

    //... 
} 

Tutte le funzionalità "globale" sono realizzati tramite un'interfaccia di servizio mockable, IApplication. Hai ancora a disposizione come costruire ViewModel con l'istanza di servizio corretta, ma sembra che tu stia già gestendo questo? Se stai cercando una soluzione lì, Onyx (disclaimer, io sono l'autore) può fornire una soluzione lì. La tua Applicazione si iscriverà all'evento View.Created e si aggiungerà come servizio e il framework si occuperà del resto.

+0

Negli ultimi giorni sto riversando il codice Onyx per raccogliere informazioni su WPF. È decisamente definito come penso e ho imparato un bel po '. –

+0

Grazie. Anche se non usi Onyx, spero che le idee siano utili. Onyx non è certamente necessario qui, anche se la soluzione dell'interfaccia di servizio credo sia davvero quella che stai cercando. – wekempf

2

Sì, sei sulla buona strada. Quando hai due controlli nel tuo sistema che hanno bisogno di comunicare dati, vuoi farlo in un modo che sia il più possibile disaccoppiato. Ci sono diversi modi per farlo.

In Prism 2, hanno un'area che è un po 'come un "bus dati". Un controllo potrebbe produrre dati con una chiave aggiunta al bus e qualsiasi controllo che desideri che i dati possano registrare una richiamata quando tali dati cambiano.

Personalmente, ho implementato qualcosa che chiamo "ApplicationState". Ha lo stesso scopo. Implementa INotifyPropertyChanged e chiunque nel sistema può scrivere su proprietà specifiche o iscriversi per eventi di modifica. È meno generico della soluzione Prism, ma funziona. Questo è praticamente ciò che hai creato.

Ma ora avete il problema di come passare lo stato dell'applicazione. Il modo in cui la vecchia scuola lo fa è renderlo un Singleton. Non sono un grande fan di questo. Invece, ho un'interfaccia definita come:

public interface IApplicationStateConsumer 
{ 
    public void ConsumeApplicationState(ApplicationState appState); 
} 

Qualsiasi componente visiva nell'albero può implementare questa interfaccia, e semplicemente passare allo stato di applicazione al ViewModel.

Quindi, nella finestra radice, quando viene attivato l'evento Loaded, attraverso l'albero visivo e cerco controlli che richiedono lo stato dell'app (IApplicationStateConsumer). Consegno loro l'appState e il mio sistema è inizializzato. È un'iniezione di dipendenza da uomo povero.

D'altro canto, Prism risolve tutti questi problemi. Vorrei poter tornare indietro e riprogettare usando Prism ... ma è un po 'troppo tardi per me essere redditizio.

11

Generalmente ho un brutto presentimento riguardo al codice che ha un modello di vista che comunica direttamente con un altro. Mi piace l'idea che la parte VVM del pattern dovrebbe essere sostanzialmente inseribile e nulla all'interno di quell'area del codice dovrebbe dipendere dall'esistenza di qualsiasi altra cosa all'interno di quella sezione. Il ragionamento dietro questo è che senza centralizzare la logica può diventare difficile definire la responsabilità.

D'altra parte, in base al codice effettivo, potrebbe essere semplicemente che ApplicationViewModel è mal chiamato, non rende un modello accessibile a una vista, quindi potrebbe semplicemente essere una scelta scadente di nome.

In entrambi i casi, la soluzione si riduce alla responsabilità. Il mio modo di vedere si hanno tre cose da realizzare:

  1. consentono all'utente di richiesta di connessione a un indirizzo
  2. utilizzare tale indirizzo per la connessione a un server
  3. Persist quell'indirizzo.

Ti suggerisco di avere bisogno di tre classi invece delle tue due.

public class ServiceProvider 
{ 
    public void Connect(Uri address) 
    { 
     //connect to the server 
    } 
} 

public class SettingsProvider 
{ 
    public void SaveAddress(Uri address) 
    { 
     //Persist address 
    } 

    public Uri LoadAddress() 
    { 
     //Get address from storage 
    } 
} 

public class ConnectionViewModel 
{ 
    private ServiceProvider serviceProvider; 

    public ConnectionViewModel(ServiceProvider provider) 
    { 
     this.serviceProvider = serviceProvider; 
    } 

    public void ExecuteConnectCommand() 
    { 
     serviceProvider.Connect(Address); 
    }   
} 

La prossima cosa da decidere è come l'indirizzo arriva a SettingsProvider. Si può passare da ConnectionViewModel come si fa attualmente, ma non ne sono entusiasta perché aumenta l'accoppiamento del modello di visualizzazione e non è responsabilità del ViewModel sapere che ha bisogno di essere persistente. Un'altra opzione è quella di effettuare la chiamata dal ServiceProvider, ma non mi sembra che debba esserlo anche la responsabilità di ServiceProvider. In effetti non sembra che la responsabilità di nessuno sia diversa da SettingsProvider. Il che mi porta a credere che il fornitore di impostazioni dovrebbe ascoltare le modifiche all'indirizzo collegato e persistere senza intervento.In altre parole, un evento:

public class ServiceProvider 
{ 
    public event EventHandler<ConnectedEventArgs> Connected; 
    public void Connect(Uri address) 
    { 
     //connect to the server 
     if (Connected != null) 
     { 
      Connected(this, new ConnectedEventArgs(address)); 
     } 
    } 
} 

public class SettingsProvider 
{ 

    public SettingsProvider(ServiceProvider serviceProvider) 
    { 
     serviceProvider.Connected += serviceProvider_Connected; 
    } 

    protected virtual void serviceProvider_Connected(object sender, ConnectedEventArgs e) 
    { 
     SaveAddress(e.Address); 
    } 

    public void SaveAddress(Uri address) 
    { 
     //Persist address 
    } 

    public Uri LoadAddress() 
    { 
     //Get address from storage 
    } 
} 

Questo introduce stretto accoppiamento tra il ServiceProvider e SettingsProvider, che si vuole evitare, se possibile, e mi piacerebbe utilizzare un EventAggregator qui, che ho discusso in una risposta ad this question

Per risolvere i problemi di testabilità, ora si ha un'aspettativa molto definita per ciò che ogni metodo farà. ConnectionViewModel chiamerà connect, The ServiceProvider si connetterà e SettingsProvider persisterà. Per testare il ConnectionViewModel probabilmente si desidera convertire l'aggancio alla ServiceProvider da una classe ad un'interfaccia:

public class ServiceProvider : IServiceProvider 
{ 
    ... 
} 

public class ConnectionViewModel 
{ 
    private IServiceProvider serviceProvider; 

    public ConnectionViewModel(IServiceProvider provider) 
    { 
     this.serviceProvider = serviceProvider; 
    } 

    ...  
} 

quindi è possibile utilizzare un quadro beffardo per introdurre un IServiceProvider deriso che è possibile controllare per assicurarsi che il metodo connect è stato chiamato con i parametri attesi.

Testare le altre due classi è più difficile poiché si baseranno su un server reale e un dispositivo di archiviazione permanente. È possibile aggiungere più livelli di riferimento indiretto per ritardare questo (ad esempio un PersistenceProvider utilizzato da SettingsProvider), ma alla fine si lascia il mondo del test delle unità e si entra in test di integrazione. Generalmente quando codifico con i modelli sopra i modelli e i modelli di visualizzazione è possibile ottenere una buona copertura del test unitario, ma i provider richiedono metodologie di test più complicate.

Naturalmente, una volta che si utilizza un EventAggregator per interrompere l'accoppiamento e l'IOC per facilitare il test, è probabilmente opportuno esaminare uno dei framework di distribuzione delle dipendenze come Prism di Microsoft, ma anche se si è troppo tardi in fase di sviluppo per -architettura molte regole e schemi possono essere applicati al codice esistente in un modo più semplice.