ViewModels können per Service Locator Pattern an eine View gebunden werden um eine saubere Trennung zu schaffen. Dieser Beitrag beschreibt nicht nur die Theorie, sondern liefert auch ein praxisnahes Beispiel.

In den vorangegangenen Beiträgen Lose Kommunikation zwischen ViewModels und Lose Kommunikation zwischen ViewModels – Teil 2 habe ich gezeigt, wie ViewModels miteinander kommunizieren können, ohne sich wirklich zu kennen. In diesem Beitrag möchte ich nun einen Schritt weiter gehen und zeigen, wie ViewModels über das Service Locator Pattern an Views gebunden werden können. Dazu wurde das in den beiden erwähnten Beiträgen verwendete Beispiel nochmals erweitert.

Ausgangspunkt

Ein ViewModel wird meist als Datenkontext für Views verwendet. Dieser Datenkontext muss an irgendeiner Stelle gesetzt werden. In der Regel bleibt hier der Ansatz, eine konkrete Instanz der Eigenschaft DataContext einer View (oder eines Teiles davon) zuzuweisen. Dadurch wird eine Abhängigkeit geschaffen, die bei zukünftigen Änderungen/Erweiterungen zu Aufwand führen kann/wird. Oft sieht dies dann wie folgt aus (beispielhafter Auszug aus der App.xaml:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    MainWindow main = new MainWindow();
    main.MessageSender.DataContext = new FirstViewModel();
    main.MessageReceiver.DataContext = new SecondViewModel();

    MainWindow = main;

    MainWindow.Show();
}

Viel einfacher und schöner wäre es, das ViewModel per XAML zu binden, ohne das konkrete ViewModel wirklich zu kennen. D.h. das dahinter liegende ViewModel könnte konfiguriert werden und wäre somit austauschbar. Die Anwendung würde bei einer Änderung weiterhin funktionieren, ein erneutes Kompilieren wäre nicht notwendig, da keine Änderungen am bestehenden Sourcecode vorgenommen werden müssten.

In einigen verfügbaren Beispielen zu diesem Thema wird zwar ebenfalls das angesprochene Pattern verwendet, doch muss die Registrierung manuell vorgenommen werden. Dies führt während der Entwicklung zu zusätzlichem Aufwand, da der Locator erweitert werden muss. Dies ist nicht mein gewünschtes Verhalten. Für mich kommen zwei Alternativen (die natürlich auch kombiniert werden können) in Frage:

  • ViewModels können durch den Locator identifiziert und registriert werden. Der Entwickler muss hier nicht eingreifen und keine Erweiterungen diesbezüglich vornehmen.
  • Die einzelnen Registrierungen können über eine Konfiguration vorgenommen werden. Diese wird eingelesen und die dadurch eingebundenen ViewModels zur Laufzeit zur Verfügung gestellt.

Nachfolgend nun eine einfache Implementierung, die keinen Anspruch auf Vollständigkeit erhebt, jedoch funktionstüchtig ist und einen Ansatz für weitere Ideen bieten soll.

Konkrete Implementierung

Einen Ansatz bietet hier das Service Locator Pattern. Im nachfolgenden Beispiel wird dieses an die eigenen Bedürfnisse angepasst. Im ersten Schritt wird eine Schnittstelle definiert:

public interface ILocator
{
    void Register(string name, object o);
    object GetInstance(string name);
    object this[string name] { get; }
}

Geboten wird die Möglichkeit, eine Instanz per Register zu registrieren (sofern notwendig), auch kann eine bereits registrierte Instanz per GetInstance bezogen werden. Zusätzlich wird ein Indexer definiert. Die einzelnen ViewModels können über einen eindeutigen Namen (dieser ist frei zu vergeben) angesprochen werden. Der Indexer selbst wird in weiterer Folge für das Binding verwendet.

Damit bestehende ViewModels automatisch registriert werden können, wird ein benutzerdefiniertes Attribut definiert. Dieses kann in weiterer Folge verwendet werden, um ein ViewModel mit einem eindeutigen Namen zu kennzeichnen.

public class LocatorAttribute : Attribute
{
    public string Name { get; set; }

    public LocatorAttribute(string name)
    {
        Name = name;
    }
}

Der konkrete Locator wird nun so implementiert, dass bei der Instantiierung alle Typen der referenzierten Assemblies durchsucht und mit diesem Attribut versehene ViewModels automatisch registriert werden.

public class ViewModelLocator : ILocator
{
    private Dictionary<string, object> registeredInstances = new Dictionary<string, object>();

    public ViewModelLocator()
    {
        FindViewModels();   
    }

    public void Register(string name, object o)
    {
        if (String.IsNullOrEmpty(name))
            throw new ArgumentNullException("name");

        if (!registeredInstances.ContainsKey(name))
            registeredInstances.Add(name, o);
        else throw new AlreadyRegisteredException(String.Format("Instance with name '{0}' already registered", name));
    }

    public object GetInstance(string name)
    {
        if (registeredInstances.ContainsKey(name))
            return registeredInstances[name];
        return null;
    }

    public object this[string name]
    {
        get
        {
            return GetInstance(name);
        }
    }

    private void FindViewModels()
    {
        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (Assembly currentAssembly in assemblies)
        {
            foreach (Type currentType in currentAssembly.GetTypes())
            {
                foreach (object customAttribute in currentType.GetCustomAttributes(true))
                {
                    LocatorAttribute locAttribute = customAttribute as LocatorAttribute;
                    if (locAttribute != null)
                    {
                        if (!registeredInstances.ContainsKey(locAttribute.Name))
                        {
                            object instance = Activator.CreateInstance(currentType);
                            registeredInstances.Add(locAttribute.Name, instance);
                        }

                    }
                }
            }
        }
    }
}

In dieser Implementierung werden nun – wie bereits erwähnt – bei der Instantiierung alle mit dem Attribut versehenen ViewModels automatisch registriert. Dieser Mechanismus kann beispielsweise verwendet werden, wenn ViewModels in eigene Assemblies ausgelagert sind und ein Austausch daher relativ einfach vorgenommen werden kann. Es ist zusätzlich auch denkbar, an dieser Stelle eine Konfiguration einzulesen oder einen anderen Mechanismus zu wählen.

Über den Indexer kann nun auf die einzelnen ViewModel-Instanzen zugegriffen werden. Die im Beispiel verwendete MainWindow verwendet zwei ViewModels, um das zuvor implementierte Messaging zu zeigen. Um mit dem Locator zu arbeiten, wird dieser der Anwendung als Ressource zur Vefügung gestellt:

<Application.Resources>
    <loc:ViewModelLocator x:Key="ViewModelLocator"/>
</Application.Resources>

In der View kann nun auf diese Instanz zugegriffen und auf den eindeutigen Namen von ViewModels gebunden werden:

<StackPanel>
    <StackPanel DataContext="{Binding [FirstViewModel], Source={StaticResource ViewModelLocator}}">
        <TextBlock Text="Nachricht eingeben"/>
        <TextBox MinLines="5" Text="{Binding Message}"/>
        <Button Content="Nachricht senden" Command="{Binding SendMessageCommand}"/>
    </StackPanel>
    <StackPanel DataContext="{Binding [SecondViewModel], Source={StaticResource ViewModelLocator}}">
        <TextBox MinLines="5" Text="{Binding Message}"/>
    </StackPanel>
</StackPanel>`

Zu sehen ist die Bindung bei den beiden Kind-StackPanel-Elementen. Die Eigenschaft Source wird auf die zuvor erstellte Ressource gesetzt. Der Path selbst verweist auf den implementierten Indexer und definiert, welches ViewModel zu verwenden ist. Ein Kennen der tatsächlichen Instanz ist nicht notwendig. Diese Aufgabe wird durch den ViewModelLocator übernommen.

Nachfolgend ist das aktualisierte Beispiel angehängt.

Download WPF Messaging Demo

Auch in diesem Fall würde ich mich über Anregungen jeglicher Art freuen. Interessant sind vor allem Szenarien, die ich nicht bedacht habe und die über diese Implementierung nicht abgedeckt werden können.

Die Implementierung des Locators, des Messengers und des Basis-ViewModels wurde in eine eigene Assembly ausgelagert und kann daher relativ einfach extrahiert und verwendet werden.

Weiter geht es im zweiten Teil: Binden von ViewModels via Locator Teil 2

Über den Autor

Norbert Eder

Ich bin ein leidenschaftlicher Softwareentwickler und Fotograf. Mein Wissen und meine Gedanken teile ich nicht nur hier im Blog, sondern auch in Fachartikeln und Büchern.

4 Kommentare

  • Die beste Variante eines ViewModel locators die ich kenne:

    geekswithblogs.net/HouseOfBilz/archive/2010/06/04/adventures-in-mvvm-ndash-viewmodel-location-and-creation.aspx

  • Vielen Dank Norbert!

    Habe deine Implementierung gerade in Projekt eingebaut, funktioniert prächtig!

    Ich habe diesen noch für meine Zwecke ein bisschen umgebaut:
    – wurde als Singleton implementiert
    – habe diesen blendable gemacht gibt zur Designzeit einen leeren Locator retour
    – ViewModels werden in einer bestimmten Reihenfolge registriert (Attribut wurde um eine Order erweitert.
    – Weiters interiert meine Version nicht über alle Assemblies und holt sich per Extension Methods gleich die Types die das Attribut haben

    LG
    Jürgen

  • Hallo Jürgen,

    vielen Dank für deine Rückmeldung. Ich habe meine Variante inzwischen natürlich auch weiterentwickelt (wächst mit den Projekten). Vielleicht zeigst mir deine Lösung mal bei Gelegenheit.

  • Hallo Norbert!

    Können wir gerne mal machen.
    Würden Dir sowieso gerne unser "Vorzeige"-WPF Projekt zeigen.

    Wird noch ein gutes Monat dauern melden uns bei Dir!

    LG
    Jürgen