Où stocker les parameters / l’état de l’application dans une application MVVM

J’expérimente MVVM pour la première fois et j’aime beaucoup la séparation des responsabilités. Bien entendu, 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 à l’échelle de l’application.

Disons que mon application se connecte à une URL spécifique. J’ai un ConnectionWindow et un ConnectionViewModel qui prennent en charge la collecte de ces informations auprès de l’utilisateur et l’invocation de commandes pour se connecter à l’adresse. Au prochain démarrage de l’application, je souhaite me reconnecter à cette même adresse sans demander à l’utilisateur.

Ma solution jusqu’ici consiste à créer un ApplicationViewModel fournissant une commande permettant de se connecter à une adresse spécifique et de sauvegarder cette adresse dans un stockage persistant (l’emplacement où il est réellement enregistré n’est pas pertinent pour cette question). Vous trouverez ci-dessous 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 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 donc la suivante: un ApplicationViewModel est-il le bon moyen de gérer cela? Sinon, comment pourriez-vous stocker l’état de l’application?

EDIT: J’aimerais aussi savoir comment cela affecte la testabilité. L’une des principales raisons d’utiliser MVVM est la possibilité de tester les modèles sans application hôte. Plus précisément, je voudrais savoir comment les parameters d’application centralisés affectent la testabilité et la capacité de simuler les modèles dépendants.

Si vous n’utilisiez pas MV-VM, la solution est simple: vous mettez ces données et fonctionnalités dans votre type dérivé Application. Application.Current vous donne alors access à celle-ci. Comme vous le savez, le problème ici est que Application.Current pose des problèmes lorsque l’unité teste ViewModel. C’est ce qui doit être corrigé. La première étape consiste à nous dissocier d’une instance d’application concrète. Pour ce faire, définissez une interface et implémentez-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 } 

L’étape suivante consiste à éliminer l’appel à Application.Current dans le ViewModel à l’aide de Inversion of Control ou Service Locator.

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

Toutes les fonctionnalités “globales” sont désormais fournies via une interface de service modulable, IApplication. Il vous rest encore à construire le ViewModel avec la bonne instance de service, mais vous avez déjà l’impression de gérer cela? Si vous cherchez une solution là-bas, Onyx (disclaimer, je suis l’auteur) peut fournir une solution là-bas. Votre application s’abonnerait à l’événement View.Created et s’appendait en tant que service, tandis que la structure prendrait en charge le rest.

J’ai généralement un mauvais pressentiment à propos du code qui a un modèle de vue communiquant directement avec un autre. J’aime l’idée que la partie VVM du modèle devrait être fondamentalement enfichable et que rien dans cette partie du code ne devrait dépendre de l’existence d’autres éléments dans cette section. Le raisonnement derrière cela 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 le nom ApplicationViewModel soit mal nommé, il ne rend pas un modèle accessible à une vue; il peut donc s’agir simplement d’un mauvais choix de nom.

Dans les deux cas, la solution consiste à réduire la responsabilité. De la manière dont je vois les choses, vous avez trois choses à accomplir:

  1. Autoriser l’utilisateur à demander à se connecter à une adresse
  2. Utilisez cette adresse pour vous connecter à un serveur
  3. Persistez cette adresse.

Je suggérerais que vous ayez besoin de trois cours 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 la façon dont l’adresse est envoyée à SettingsProvider. Vous pouvez le transmettre depuis ConnectionViewModel comme vous le faites actuellement, mais cela ne me tient pas à cœur, car cela augmente le couplage du modèle de vue et le ViewModel n’a pas la responsabilité de savoir qu’il doit persister. Une autre option consiste à appeler le fournisseur de service, mais cela ne me semble pas vraiment devoir être de la responsabilité de fournisseur de service. En fait, il ne semble pas que la responsabilité de quiconque autre que celle de SettingsProvider soit engagée. Ce qui m’amène à penser que le fournisseur de parameters doit écouter les modifications apscopes à l’adresse connectée et les conserver sans intervention. En d’autres termes, un événement:

 public class ServiceProvider { public event EventHandler 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 } } 

Cela introduit un couplage étroit entre ServiceProvider et SettingsProvider, ce que vous voulez éviter si possible et je voudrais utiliser un EventAggregator ici, dont j’ai parlé dans une réponse à cette question

Pour résoudre les problèmes de testabilité, vous avez maintenant une attente très définie pour ce que fera chaque méthode. ConnectionViewModel appellera connect, le fournisseur de services se connectera et le fournisseur de parameters persistera. Pour tester ConnectionViewModel, vous souhaitez probablement convertir le couplage en ServiceProvider d’une classe à une interface:

 public class ServiceProvider : IServiceProvider { ... } public class ConnectionViewModel { private IServiceProvider serviceProvider; public ConnectionViewModel(IServiceProvider provider) { this.serviceProvider = serviceProvider; } ... } 

Vous pouvez ensuite utiliser un framework moqueur pour introduire un IServiceProvider simulé que vous pouvez vérifier pour vous assurer que la méthode de connexion a été appelée avec les parameters attendus.

Tester les deux autres classes est plus difficile, car elles reposent sur un serveur réel et un périphérique de stockage persistant réel. Vous pouvez append plusieurs couches d’indirection pour retarder cela (par exemple, un PersistenceProvider utilisé par SettingsProvider), mais vous quittez finalement le monde des tests unitaires et entrez dans le test 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 tests unitaires, mais les fournisseurs nécessitent des méthodologies de test plus complexes.

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 de regarder dans l’un des frameworks d’dependency injections tels que Prism de Microsoft, mais même si vous êtes trop tard dans le développement pour réarchiver beaucoup des règles et des modèles peuvent être appliqués au code existant d’une manière plus simple.

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

Dans Prism 2, ils ont une zone qui est un peu comme un “bus de données”. Une commande peut produire des données avec une clé ajoutée au bus, et toute commande qui souhaite ces données peut enregistrer un rappel lorsque ces données changent.

Personnellement, j’ai implémenté quelque chose que j’appelle “ApplicationState”. Il a le même but. Il implémente INotifyPropertyChanged et tout utilisateur du système peut écrire dans les propriétés spécifiques ou s’abonner à des événements de modification. 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 la transmission de l’état de l’application. La façon la plus classique de le faire est d’en faire un Singleton. Je ne suis pas un grand fan de ça. Au lieu de cela, j’ai une interface définie comme:

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

N’importe quel composant visuel de l’arborescence peut implémenter cette interface et simplement transmettre l’état Application au ViewModel.

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

D’autre part, Prism résout tous ces problèmes. J’aurais bien aimé pouvoir revenir sur l’architecture de Prism, mais il est un peu trop tard pour que je sois rentable.