2009-04-23 17 views
26

Estoy experimentando con MVVM por primera vez y realmente me gusta la separación de responsabilidades. Por supuesto, cualquier patrón de diseño solo resuelve muchos problemas, no todos. Así que estoy tratando de averiguar dónde almacenar el estado de la aplicación y dónde almacenar los comandos de toda la aplicación.Dónde almacenar la configuración/estado de la aplicación en una aplicación MVVM

Digamos que mi aplicación se conecta a una URL específica. Tengo una ConnectionWindow y un ConnectionViewModel que admiten la recopilación de esta información del usuario y la invocación de comandos para conectarse a la dirección. La próxima vez que se inicie la aplicación, quiero volver a conectarme a esta misma dirección sin preguntar al usuario.

Mi solución hasta ahora es crear un ApplicationViewModel que proporciona un comando para conectarse a una dirección específica y guardar esa dirección en algún almacenamiento persistente (donde se guarda realmente es irrelevante para esta pregunta). A continuación se muestra un modelo de clase abreviado.

La vista de la aplicación del modelo:

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 conexión de vista del modelo:

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

    ... 
} 

Así que la pregunta es la siguiente: ¿Es un ApplicationViewModel la manera correcta de manejar esto? ¿De qué otra forma podría almacenar el estado de la aplicación?

EDIT: Me gustaría saber también cómo esto afecta la capacidad de prueba. Una de las principales razones para usar MVVM es la capacidad de probar los modelos sin una aplicación host. Específicamente, me interesa saber cómo la configuración centralizada de la aplicación afecta la capacidad de prueba y la capacidad de burlarse de los modelos dependientes.

Respuesta

10

Si no estaba usando M-V-VM, la solución es simple: pone esta información y funcionalidad en su tipo de aplicación derivada. Application.Current luego le da acceso a ella. El problema aquí, como sabe, es que Application.Current causa problemas cuando la unidad prueba el ViewModel. Eso es lo que necesita ser arreglado. El primer paso es desacoplarnos de una instancia de aplicación concreta. Haga esto definiendo una interfaz e implementándola en su tipo concreto de Aplicación.

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

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

Ahora el siguiente paso es eliminar la llamada a Application.Current dentro del modelo de vista mediante el uso de Inversión de Control o el Servicio de localización.

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

    //... 
} 

Toda la funcionalidad "global" ahora se proporciona a través de una interfaz de servicio mockable, IApplication. Aún le queda la forma de construir ViewModel con la instancia de servicio correcta, pero parece que ya lo está manejando. Si está buscando una solución allí, Onyx (descargo de responsabilidad, soy el autor) puede proporcionarle una solución. Su aplicación se suscribiría al evento View.Created y se agregaría a sí mismo como un servicio, y el framework se ocuparía del resto.

+0

En realidad, he estado revisando el código Onyx durante los últimos días para obtener información sobre WPF. Definitivamente está explicado mucho de cómo pienso y he aprendido bastante. –

+0

Gracias. Incluso si no usa Onyx, espero que las ideas sean útiles. Onyx ciertamente no es necesario aquí, aunque la solución de interfaz de servicio creo que realmente es lo que estás buscando. – wekempf

2

Sí, está en el camino correcto. Cuando tiene dos controles en su sistema que necesitan comunicar datos, desea hacerlo de una manera lo más desacoplada posible. Hay varias formas de hacer esto.

En Prism 2, tienen un área que es como un "bus de datos". Un control puede producir datos con una clave que se agrega al bus, y cualquier control que quiera que los datos puedan registrar una devolución de llamada cuando cambien esos datos.

Personalmente, he implementado algo que llamo "ApplicationState". Tiene el mismo propósito. Implementa INotifyPropertyChanged, y cualquier persona en el sistema puede escribir en las propiedades específicas o suscribirse para eventos de cambio. Es menos genérico que la solución Prism, pero funciona. Esto es más o menos lo que creaste.

Pero ahora, tiene el problema de cómo pasar el estado de la aplicación. La forma de hacerlo de la vieja escuela es convertirlo en Singleton. No soy un gran admirador de esto. En cambio, he definido como una interfaz:

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

Cualquier componente visual en el árbol puede implementar esta interfaz, y simplemente pasar el estado de aplicación al modelo de vista.

Luego, en la ventana raíz, cuando se dispara el evento Loaded, recorro el árbol visual y busco los controles que desean el estado de la aplicación (IApplicationStateConsumer). Les entrego el appState, y mi sistema se inicializa. Es una inyección de dependencia de un pobre hombre.

Por otro lado, Prism resuelve todos estos problemas. Me gustaría poder volver a diseñar con Prism ... pero es demasiado tarde para que sea rentable.

11

Generalmente me da un mal presentimiento sobre el código que tiene un modelo de vista que se comunica directamente con otro. Me gusta la idea de que la parte VVM del patrón debe ser básicamente conectable y nada dentro de esa área del código debe depender de la existencia de otra cosa dentro de esa sección. El razonamiento detrás de esto es que sin centralizar la lógica puede ser difícil definir la responsabilidad.

Por otro lado, en función de su código real, es posible que ApplicationViewModel tenga un nombre incorrecto, no hace que un modelo sea accesible para una vista, por lo que puede ser simplemente una mala elección de nombre.

De cualquier forma, la solución se reduce a un colapso de la responsabilidad. La forma en que veo que tiene tres cosas: para lograr

  1. permitir que el usuario solicite para conectarse a una dirección
  2. utilizar esa dirección para conectarse a un servidor
  3. Persistir esa dirección.

Le sugiero que necesite tres clases en lugar de las dos.

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

Lo siguiente a decidir es cómo la dirección llega a SettingsProvider. Podría pasarlo desde ConnectionViewModel como lo hace actualmente, pero no estoy interesado en eso porque aumenta el acoplamiento del modelo de vista y no es responsabilidad del ViewModel saber que necesita persistir. Otra opción es hacer la llamada desde el proveedor de servicios, pero realmente no siento que deba ser responsabilidad del proveedor del servicio tampoco. De hecho, no se siente responsable de nadie más que el Proveedor de Configuraciones. Lo que me lleva a creer que el proveedor de configuración debe escuchar los cambios en la dirección conectada y persistir sin intervención.En otras palabras, 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 
    } 
} 

Esto introduce apretado acoplamiento entre el Proveedor del Servicio y la SettingsProvider, lo que se quiere evitar si es posible y que haría uso de un EventAggregator aquí, que he discutido en una respuesta a this question

Para abordar los problemas de la capacidad de prueba, ahora tiene una expectativa muy definida de lo que hará cada método. ConnectionViewModel llamará a connect, The ServiceProvider se conectará y SettingsProvider persistirá. Para probar la ConnectionViewModel es probable que desee convertir el acoplamiento a la ServiceProvider de una clase a una interfaz:

public class ServiceProvider : IServiceProvider 
{ 
    ... 
} 

public class ConnectionViewModel 
{ 
    private IServiceProvider serviceProvider; 

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

    ...  
} 

A continuación, se puede utilizar un marco de burla a introducir un IServiceProvider burlado que se puede comprobar para asegurarse de que el método connect fue llamado con los parámetros esperados.

Probar las otras dos clases es más desafiante ya que dependerán de tener un servidor real y un dispositivo de almacenamiento real persistente. Puede agregar más capas de direccionamiento indirecto para retrasar esto (por ejemplo, un Proveedor de Persistencia que utiliza el Proveedor de Configuraciones) pero finalmente deja el mundo de las pruebas unitarias e ingresa las pruebas de integración. En general, cuando codigo con los patrones anteriores, los modelos y modelos de vista pueden obtener una buena cobertura de prueba unitaria, pero los proveedores requieren metodologías de prueba más complicadas.

Por supuesto, una vez que esté utilizando un EventAggregator para romper el acoplamiento y COI para facilitar las pruebas, probablemente valga la pena investigar uno de los frameworks de inyección de dependencias como el Prism de Microsoft, pero incluso si está demasiado avanzado para desarrollar -arquitecto muchas de las reglas y patrones se pueden aplicar al código existente de una manera más simple.

Cuestiones relacionadas