2009-04-23 5 views
26

J'expérimente MVVM pour la première fois et j'aime vraiment la séparation des responsabilités. Bien sûr, tout modèle de conception ne résout que de nombreux problèmes - pas tous. J'essaie donc de savoir où stocker l'état de l'application et où stocker les commandes de l'application.Où stocker les paramètres/l'état de l'application dans une application MVVM

Disons que mon application se connecte à une URL spécifique. J'ai un ConnectionWindow et un ConnectionViewModel qui supportent rassembler ces informations de l'utilisateur et invoquant des commandes pour se relier à l'adresse. Au prochain démarrage de l'application, je souhaite me reconnecter à cette même adresse sans que l'utilisateur ne soit invité. Ma solution jusqu'ici est de créer un ApplicationViewModel qui fournit une commande pour se connecter à une adresse spécifique et enregistrer cette adresse dans un stockage persistant (où il est réellement enregistré n'est pas pertinent pour cette question). Voici un modèle de classe abrégé.

Le modèle de vue de l'application:

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

    ... 
} 

Le modèle de vue de la connexion:

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

    ... 
} 

La question est la suivante: Est-ce un ApplicationViewModel la bonne façon de gérer cette situation? Sinon, comment pouvez-vous stocker l'état de l'application?

EDIT: Je voudrais savoir aussi comment cela affecte la testabilité. L'une des raisons principales de l'utilisation de MVVM est la possibilité de tester les modèles sans application hôte. Plus précisément, je m'intéresse à la compréhension de la façon dont les paramètres d'application centralisés affectent la testabilité et la capacité à simuler les modèles dépendants.

Répondre

10

Si vous n'utilisiez pas M-V-VM, la solution est simple: vous mettez ces données et fonctionnalités dans votre type dérivé d'application. Application.Current vous donne alors accès à celui-ci. Le problème ici, comme vous le savez, est que Application.Current provoque des problèmes lors du test unitaire du ViewModel. C'est ce qui doit être réparé. La première étape consiste à nous découpler d'une instance d'application concrète. Pour ce faire, définissez une interface et appliquez-la sur votre type d'application concret.

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

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

Maintenant, l'étape suivante consiste à éliminer l'appel à Application.Current dans le ViewModel en utilisant ou inversion de contrôle Locator de service.

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

    //... 
} 

Toutes les fonctionnalités "globales" sont désormais fournies via une interface de service simulable, IApplication. Vous êtes encore à gauche avec comment construire le ViewModel avec l'instance de service correcte, mais il semble que vous manipulez déjà cela? Si vous cherchez une solution là-bas, Onyx (avertissement, je suis l'auteur) peut fournir une solution là-bas. Votre application s'abonnerait à l'événement View.Created et se présenterait comme un service, et le framework s'occuperait du reste.

+0

En fait, j'ai utilisé le code Onyx ces derniers jours pour avoir un aperçu de WPF. C'est clairement défini comment je pense et j'ai beaucoup appris. –

+0

Merci. Même si vous n'utilisez pas Onyx lui-même, j'espère que les idées sont utiles. Onyx n'est certainement pas nécessaire ici, bien que la solution d'interface de service que je pense est vraiment ce que vous cherchez. – wekempf

2

Oui, vous êtes sur la bonne voie. Lorsque vous avez deux contrôles dans votre système qui ont besoin de communiquer des données, vous voulez le faire d'une manière aussi découplée que possible. Il y a plusieurs moyens de le faire.

Dans Prism 2, ils ont une zone qui ressemble à un "bus de données". Un contrôle peut produire des données avec une clé ajoutée au bus, et tout contrôle qui veut que ces données puissent enregistrer un rappel lorsque ces données changent.

Personnellement, j'ai implémenté quelque chose que j'appelle "ApplicationState". Cela a le même but. Il implémente INotifyPropertyChanged, et n'importe qui dans le système peut écrire dans les propriétés spécifiques ou s'abonner à des événements de changement. C'est moins générique que la solution Prism, mais ça marche. C'est à peu près ce que vous avez créé.

Mais maintenant, vous avez le problème de savoir comment passer l'état de l'application. La vieille façon de faire ceci est de faire un Singleton. Je ne suis pas un grand fan de ceci. Au lieu de cela, j'ai une interface définie comme:

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

Tout composant visuel dans l'arbre peut mettre en œuvre cette interface, et il suffit de passer l'état d'application du ViewModel.

Ensuite, dans la fenêtre racine, lorsque l'événement Loaded est déclenché, je parcours l'arborescence visuelle et recherche les contrôles qui veulent l'état de l'application (IApplicationStateConsumer). Je leur remets l'appState, et mon système est initialisé. C'est l'injection de dépendance d'un pauvre.

D'autre part, Prism résout tous ces problèmes. Je souhaiterais pouvoir revenir en arrière et réorganiser en utilisant Prism ... mais il est un peu trop tard pour que je sois rentable.

11

J'ai généralement une mauvaise idée du code qui a un modèle de vue communiquant directement avec un autre. J'aime l'idée que la partie VVM du pattern devrait être fondamentalement connectable et rien à l'intérieur de cette zone du code ne devrait dépendre de l'existence de quelque chose d'autre dans cette section. Le raisonnement derrière ceci est que sans centraliser la logique, il peut devenir difficile de définir la responsabilité. D'autre part, en fonction de votre code actuel, il se peut que ApplicationViewModel soit mal nommé, il ne rend pas un modèle accessible à une vue, ce qui peut être un mauvais choix de nom.

De toute façon, la solution se résume à une rupture de responsabilité. La façon dont je vois que vous avez trois choses à réaliser:

  1. permettent à l'utilisateur de demander de se connecter à une adresse
  2. Utilisez cette adresse pour vous connecter à un serveur
  3. persist cette adresse.

Je suggérerais que vous avez besoin de trois classes au lieu de deux.

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 prochaine chose à décider est de savoir comment l'adresse parvient au SettingsProvider. Vous pouvez le transmettre à partir de ConnectionViewModel comme vous le faites actuellement, mais je n'y tiens pas parce que cela augmente le couplage du modèle de vue et que ViewModel n'est pas responsable de savoir qu'il a besoin de persister. Une autre option consiste à faire l'appel à partir du ServiceProvider, mais il ne me semble pas que ce soit la responsabilité du ServiceProvider. En fait, il ne se sent pas comme la responsabilité de quelqu'un d'autre que le SettingsProvider. Ce qui m'amène à croire que le fournisseur de paramètres devrait écouter les changements de l'adresse connectée et persister sans intervention.En d'autres termes un événement:

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

Ceci introduit un couplage étroit entre le ServiceProvider et le SettingsProvider, que vous voulez éviter, si possible, et j'utiliser un EventAggregator ici, que je l'ai discuté dans une réponse à this question Pour résoudre les problèmes de testabilité, vous avez maintenant une attente très définie pour ce que chaque méthode va faire. Le ConnectionViewModel appellera connect, le ServiceProvider se connectera et le SettingsProvider persistera. Pour tester la ConnectionViewModel vous voulez probablement convertir le couplage au ServiceProvider d'une classe à une interface:

public class ServiceProvider : IServiceProvider 
{ 
    ... 
} 

public class ConnectionViewModel 
{ 
    private IServiceProvider serviceProvider; 

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

    ...  
} 

Ensuite, vous pouvez utiliser un cadre moqueur pour introduire un IServiceProvider moqué que vous pouvez vérifier que la méthode de connexion a été appelé avec les paramètres attendus.

Il est plus difficile de tester les deux autres classes, car elles reposeront sur un véritable serveur et un véritable périphérique de stockage persistant. Vous pouvez ajouter plus de couches d'indirection pour retarder cela (par exemple un PersistenceProvider que le SettingsProvider utilise) mais finalement vous quittez le monde des tests unitaires et entrez dans les tests d'intégration. Généralement, lorsque je code avec les modèles ci-dessus les modèles et les modèles de vue peuvent obtenir une bonne couverture de test unitaire, mais les fournisseurs exigent des méthodologies de test plus compliquées. Bien sûr, une fois que vous utilisez un EventAggregator pour rompre le couplage et IOC pour faciliter les tests, il vaut probablement la peine d'examiner l'un des frameworks d'injection de dépendance tels que Microsoft Prism, mais même si vous êtes trop en retard pour le développement. -architecte un grand nombre de règles et de modèles peuvent être appliqués au code existant d'une manière plus simple.

Questions connexes