27 Stimmen

Wo werden Anwendungseinstellungen/Status in einer MVVM-Anwendung gespeichert?

Ich experimentiere zum ersten Mal mit MVVM und mag die Trennung der Verantwortlichkeiten sehr. Natürlich löst jedes Entwurfsmuster nur viele Probleme - nicht alle. Ich versuche also herauszufinden, wo der Anwendungsstatus und wo die anwendungsweiten Befehle gespeichert werden sollen.

Angenommen, meine Anwendung stellt eine Verbindung zu einer bestimmten URL her. Ich habe ein ConnectionWindow und ein ConnectionViewModel, die das Sammeln dieser Informationen vom Benutzer und das Aufrufen von Befehlen zur Verbindung mit der Adresse unterstützen. Wenn die Anwendung das nächste Mal startet, möchte ich die Verbindung zu derselben Adresse erneut herstellen, ohne den Benutzer dazu aufzufordern.

Meine bisherige Lösung besteht darin, ein ApplicationViewModel zu erstellen, das einen Befehl zum Herstellen einer Verbindung mit einer bestimmten Adresse und zum Speichern dieser Adresse in einem dauerhaften Speicher bereitstellt (wo sie tatsächlich gespeichert wird, ist für diese Frage irrelevant). Unten ist eine verkürzte Klasse Modell.

Das Modell der Anwendungsansicht:

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

    ...
}

Das Modell der Verbindungsansicht:

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

    ...
}

Die Frage ist also die folgende: Ist ein ApplicationViewModel der richtige Weg, dies zu handhaben? Wie könnten Sie den Anwendungsstatus sonst speichern?

EDIT : Ich würde auch gerne wissen, wie sich dies auf die Testbarkeit auswirkt. Einer der Hauptgründe für die Verwendung von MVVM ist die Möglichkeit, die Modelle ohne eine Host-Anwendung zu testen. Insbesondere bin ich daran interessiert, wie sich zentralisierte Anwendungseinstellungen auf die Testbarkeit und die Möglichkeit, die abhängigen Modelle zu mocken, auswirken.

14voto

Martin Harris Punkte 27587

Ich habe generell ein ungutes Gefühl bei Code, bei dem ein View Model direkt mit einem anderen kommuniziert. Mir gefällt die Idee, dass der VVM-Teil des Musters im Grunde steckbar sein sollte und nichts in diesem Bereich des Codes von der Existenz von etwas anderem innerhalb dieses Abschnitts abhängen sollte. Dahinter steht die Überlegung, dass es ohne eine Zentralisierung der Logik schwierig werden kann, die Verantwortlichkeiten zu definieren.

Auf der anderen Seite, auf der Grundlage Ihrer tatsächlichen Code, kann es nur sein, dass die ApplicationViewModel schlecht benannt ist, macht es nicht ein Modell zugänglich für eine Ansicht, so kann dies einfach eine schlechte Wahl des Namens sein.

In jedem Fall läuft die Lösung auf eine Aufteilung der Verantwortung hinaus. So wie ich es sehe, müssen Sie drei Dinge erreichen:

  1. Dem Benutzer erlauben, eine Verbindung zu einer Adresse zu beantragen
  2. Verwenden Sie diese Adresse, um eine Verbindung zu einem Server herzustellen
  3. Bleiben Sie bei dieser Adresse.

Ich würde vorschlagen, dass Sie statt der zwei Klassen drei brauchen.

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

Als nächstes muss entschieden werden, wie die Adresse an den SettingsProvider gelangt. Man könnte sie aus dem ConnectionViewModel übergeben, wie man es derzeit tut, aber ich bin nicht scharf darauf, weil es die Kopplung des View-Modells erhöht und es nicht in der Verantwortung des ViewModels liegt, zu wissen, dass sie persistiert werden muss. Eine andere Möglichkeit ist, den Aufruf vom ServiceProvider aus zu machen, aber ich habe auch nicht das Gefühl, dass der ServiceProvider dafür verantwortlich sein sollte. In der Tat fühlt es sich nicht wie die Verantwortung von jemand anderem als der SettingsProvider. Das führt mich zu der Annahme, dass der SettingsProvider auf Änderungen an der verbundenen Adresse achten und diese ohne Eingreifen beibehalten sollte. Mit anderen Worten ein Ereignis:

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

Dies führt zu einer engen Kopplung zwischen dem ServiceProvider und dem SettingsProvider, die Sie nach Möglichkeit vermeiden möchten, und ich würde hier einen EventAggregator verwenden, den ich in einer Antwort auf diese Frage

Um das Problem der Testbarkeit zu lösen, haben Sie jetzt eine genau definierte Erwartung, was jede Methode tun wird. Das ConnectionViewModel wird connect aufrufen, der ServiceProvider wird connect und der SettingsProvider wird persistieren. Um das ConnectionViewModel zu testen, möchten Sie wahrscheinlich die Kopplung mit dem ServiceProvider von einer Klasse in eine Schnittstelle umwandeln:

public class ServiceProvider : IServiceProvider
{
    ...
}

public class ConnectionViewModel 
{
    private IServiceProvider serviceProvider;

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

    ...       
}

Dann können Sie ein Mocking-Framework verwenden, um einen gespotteten IServiceProvider einzuführen, den Sie überprüfen können, um sicherzustellen, dass die Verbindungsmethode mit den erwarteten Parametern aufgerufen wurde.

Das Testen der beiden anderen Klassen ist schwieriger, da sie auf einen echten Server und ein echtes dauerhaftes Speichergerät angewiesen sind. Sie können weitere indirekte Ebenen hinzufügen, um dies zu verzögern (z. B. einen PersistenceProvider, den der SettingsProvider verwendet), aber letztendlich verlassen Sie die Welt der Unit-Tests und gelangen zu den Integrationstests. Wenn ich mit den obigen Mustern programmiere, können die Modelle und View-Modelle im Allgemeinen eine gute Unit-Test-Abdeckung erhalten, aber die Provider erfordern kompliziertere Testmethoden.

Wenn Sie einen EventAggregator verwenden, um die Kopplung aufzubrechen, und IOC, um das Testen zu erleichtern, lohnt es sich wahrscheinlich, eines der Dependency-Injection-Frameworks wie Microsofts Prism in Betracht zu ziehen, aber selbst wenn Sie in der Entwicklung zu spät dran sind, um eine neue Architektur zu entwickeln, können viele der Regeln und Muster auf einfachere Weise auf bestehenden Code angewendet werden.

10voto

wekempf Punkte 2711

Wenn Sie nicht M-V-VM verwenden, ist die Lösung einfach: Sie legen diese Daten und Funktionen in Ihrem abgeleiteten Anwendungstyp ab. Application.Current ermöglicht Ihnen dann den Zugriff darauf. Das Problem dabei ist, wie Sie wissen, dass Application.Current Probleme beim Unit-Test des ViewModels verursacht. Das ist es, was behoben werden muss. Der erste Schritt besteht darin, uns von einer konkreten Anwendungsinstanz zu entkoppeln. Dazu definieren Sie eine Schnittstelle und implementieren diese in Ihrem konkreten Anwendungstyp.

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

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

Der nächste Schritt besteht nun darin, den Aufruf von Application.Current innerhalb des ViewModels zu eliminieren, indem die Inversion of Control oder der Service Locator verwendet wird.

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

  //...
}

Die gesamte "globale" Funktionalität wird jetzt über eine spottbare Service-Schnittstelle, IApplication, bereitgestellt. Sie sind immer noch mit, wie man das ViewModel mit dem richtigen Service-Instanz zu konstruieren, aber es klingt wie Sie bereits behandeln, dass? Wenn Sie nach einer Lösung suchen, kann Onyx (Disclaimer, ich bin der Autor) eine Lösung dafür bieten. Ihre Anwendung würde das Ereignis View.Created abonnieren und sich selbst als Dienst hinzufügen, und das Framework würde sich um den Rest kümmern.

2voto

Brian Genisio Punkte 47135

Ja, Sie sind auf dem richtigen Weg. Wenn Sie zwei Steuerungen in Ihrem System haben, die Daten kommunizieren müssen, sollten Sie dies so entkoppelt wie möglich tun. Es gibt mehrere Möglichkeiten, dies zu tun.

In Prism 2 gibt es einen Bereich, der so etwas wie ein "Datenbus" ist. Ein Steuerelement kann Daten mit einer Taste erzeugen, die dem Bus hinzugefügt wird, und jedes Steuerelement, das diese Daten haben möchte, kann einen Rückruf registrieren, wenn sich diese Daten ändern.

Ich persönlich habe etwas implementiert, das ich "ApplicationState" nenne. Es hat den gleichen Zweck. Es implementiert INotifyPropertyChanged, und jeder im System kann auf die spezifischen Eigenschaften schreiben oder sich für Änderungsereignisse anmelden. Es ist weniger allgemein als die Prism-Lösung, aber es funktioniert. Das ist ziemlich genau das, was Sie erstellt haben.

Nun stellt sich aber das Problem, wie der Anwendungsstatus weitergegeben werden kann. Die alte Schule Weg, dies zu tun ist, um es ein Singleton zu machen. Ich bin kein großer Fan davon. Stattdessen habe ich eine Schnittstelle definiert als:

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

Jede visuelle Komponente im Baum kann diese Schnittstelle implementieren und einfach den Anwendungsstatus an das ViewModel übergeben.

Dann, im Root-Fenster, wenn das Loaded-Ereignis ausgelöst wird, durchlaufe ich den visuellen Baum und suche nach Steuerelementen, die den appState (IApplicationStateConsumer) haben wollen. Ich übergebe ihnen den appState, und mein System ist initialisiert. Es ist eine "poor-man's dependency injection".

Prism hingegen löst all diese Probleme. Ich wünschte, ich könnte zurückgehen und mit Prism eine neue Architektur erstellen... aber es ist ein bisschen zu spät für mich, um kosteneffektiv zu sein.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X