Der Weg zum eigenen MVVM-Framework – Part 4 (Einbindung der Services-Funktionalität)

Weiter geht es mit unserer Reihe zum eigenen Framework. Dieses Mal kümmern wir uns um die Einbindung unserer Service-Funktionen. Also quasi das Registrieren, das rückgängig machen einer Service Registration sowie das holen von Services. Aber zunächst einmal sollten wir klären, was ein Service ist. Ein Service ist ein Dienst, welcher innerhalb einer gewissen Gültigkeits-Reichweite definiert wird. In diesen Fall sind die Services alle für die Applikation und auch nur innerhalb dieser sichtbar. Hierbei sollte man wissen, dass dies bei unserer Umsetzung nicht mehr darstellt als eine Interface gesteuerte Singleton Sammlung unserer Klassen. Auch wird dies nun das Komplexeste, was unser Framework bisher zu bieten hat. Erneut werden wir wieder so vorgehen, das zunächst die Definitionen erfolgen anschließend die Tests und zum Abschluss die Implementierung. Ebenfalls sollte beachtet werden, dass das Wissen aus diesen Part noch relevant sein wird in zukünftige Parts. Aber nun gut lasst und das proggen beginnen!

Grundgerüst

Erneut beginnen wir mit den Grundgerüsten. Hierfür haben wir zunächst zwei Interfaces, zwei eigene Ausnahme-Typen sowie eine Klasse.Allerdings wird aus Grund der Sinnlichkeit lediglich die Letzte durch Tests abgedeckt werden. Wer auch die Ausnahmen oder Interfaces abdecken will für eine 100%ige Abdeckung muss dies selber erledigen. Fangen wir also mit unseren Schnittstellen (Interfaces) an. Die erste Schnittstelle definiert hierbei jene Schnittstelle, welche alle Service-Schnittstellen später implementieren müssen, IService. Die zweite Schnittstelle hingegen definiert den Service-Provider, welchen wir auch später noch implementieren werden.

IService-Interface:

namespace SmallMvvm.Services
{
    public interface IService
    {
        string Name { get; } 
    }
}

IServiceProvider-Interface:

using System;

namespace SmallMvvm.Services
{
    public interface IServiceProvider : System.IServiceProvider
    {
        object GetService(Type serviceType, string serviceName);

        T GetService<T>() where T : class;
        T GetService<T>(string serviceName) where T : class;

        void Register<T>(T instance) where T : class;
        void Register<T>(T instance, string serviceName) where T : class;

        void Unregister<T>() where T : class;
        void Unregister<T>(string serviceName) where T : class;

        void UnregisterAll();
    }
}

Weiter geht es nun mit unseren Ausnahmen. Hierfür legen wir zunächst einen Unterordner mit den Namen „Exceptions“ im Service-Projekt an.Bei unseren eigenen Ausnahmen haben wir zunächst eine generelle „ServiceException“ (ja, mir ist klar dass dies auch ab .NET 4 im .NET-Framework existiert, jedoch nicht in älteren Versionen und auch nicht so wie unsere!) und des Weiteren noch eine „ServiceNotAvailableException“ für den Fall, dass ein Service mal nicht verfügbar ist. Da wir hier im späteren Verlauf keine Tests anlegen, findet hier auch direkt die Implementierung mit statt. Aber bevor wir diese anlegen dürfen, müssen wir noch etwas Wichtiges tun. Den am Ende soll unser Projekt lokalisierte Fehlermeldungen ausgeben können.Dafür gehen wir in die Projekt-Eigenschaften und gehen dort im linken Register auf „Ressourcen“. Hier klicken wir den blauen Text in der Mitte des Bildes an, um eine Ressourcen-Datei zu unseren Projekthinzuzufügen. Die Ressourcen-Datei für die deutsche Lokalisierung legen wir später an. Zunächst tätigen wir für unsere Umsetzungen folgende Einträge in der Englischen Ressourcen-Daten bzw. in der Standard-Ressourcen-Datei (Hinweis: die „->“ stellen hierbei die Trennung dar. Links der Name und rechts der Wert):

ServiceException_UnknownServiceError -> Unknown service error! 
ServiceNotAvailableException_NamedServiceNotAvailable -> Named service '{0}' of type '{1}' is not available! 
ServiceNotAvailableException_ServiceTypeNotAvailable -> Service of type '{0}' is not available! 
ServiceProvider_NameAlreadyExists -> Name already exists. ServiceName has to be unique! 
ServiceProvider_ServiceInstanceCannotNull -> Service instance cannot be null! 
ServiceProvider_ServiceName_ShouldNotBeNameOfRegisteredType -> ServiceName should not be the name of the registered type! 
ServiceProvider_ServiceNameShouldNotBeEmpty -> ServiceName should not be empty! 
ServiceProvider_ServiceOfSubmittedTypeAlreadyExists -> Service of submitted type already exists. Use unregister to remove existing service type registration!

Nun können wir wirklich unsere eigenen Ausnahmen anlegen. Beginnen wir also mit der „ServiceException“ welche wie bereits gesagt in den Unterordner „Exceptions“ gehört.

using System;
using System.Runtime.Serialization;
using SmallMvvm.Services.Properties;

namespace SmallMvvm.Services.Exceptions
{
    public class ServiceException : ApplicationException
    {
        public ServiceException()
            : base(Resources.ServiceException_UnknownServiceError)
        { }

        public ServiceException(string message) 
            : base(message)
        { }

        public ServiceException(string message, Exception innerException) 
            : base(message, innerException)
        { }

        protected ServiceException(SerializationInfo info, StreamingContext context) 
            : base(info, context)
        { }
    }
}

Weiter geht es mit der Ausnahme „ServiceNotAvailableException“ ebenfalls im „Exceptions“-Unterordner.

using System;
using SmallMvvm.Services.Properties;

namespace SmallMvvm.Services.Exceptions
{
    public class ServiceNotAvailableException : ServiceException
    {
        public string ServiceName { get; private set; }
        public Type Type { get; private set; }

        public override string Message
        {
            get { return BuildExceptionMessage(); }
        }

        public ServiceNotAvailableException(Type type)
            : this("", type)
        { }

        public ServiceNotAvailableException(string serviceName, Type type)
        {
            ServiceName = serviceName;
            Type = type;
        }

        private string BuildExceptionMessage()
        {
            if (string.IsNullOrEmpty(ServiceName) && Type == null)
                return base.Message;

            if (Type != null && string.IsNullOrEmpty(ServiceName))
                return string.Format(Resources.ServiceNotAvailableException_ServiceTypeNotAvailable, Type);

            return string.Format(Resources.ServiceNotAvailableException_NamedServiceNotAvailable, ServiceName, Type);
        }
    }
}

Also, nun zur Erklärung. Dadurch, dass unsere „ServiceException“ voneiner .NET-Framework Ausnahme ableitet, können wir diese in Zukunft mittels „throw“ auslösen. Hier gibt es eine Reihe an Konstruktoren, welche jeweils einfach nur den Basis-Konstruktor aufrufen. Alsoerstmal nichts Komplexes. Etwas besonderer ist da schon unsere“ServiceNotAvailableException“-Ausnahme. Hier haben wir zum einen die überschriebene „Message“-Eigenschaft als auch ein „this“-Konstruktor Aufruf. Aber auch dies ist ja letztendlich nur eine Klasse.Durch die Ableitung von unserer „ServiceException“ ist diese natürlich impliziert auch von einer .NET-Ausnahme abgeleitet.

Nun fehlt also nur noch unser eigentlicher Kernpunkt für die Services.Legen wir also nun unsere Grundstruktur für einen ServiceProvider an.Dieser erbt grundsätzlich einfach vom „IServiceProvider“-Interface und die dadurch erzwingenden Methoden würden theoretisch schon für unser Grundgerüst ausreichen. Weitergehend gesehen auf der Implementierung, die noch kommt, habe ich jedoch die Grundstruktur bereits ein wenig erweitert.

using System;
using System.Collections.Generic;
using SmallMvvm.Services.Exceptions;
using SmallMvvm.Services.Properties;

namespace SmallMvvm.Services
{
    public class ServiceProvider : IServiceProvider
    {
        private readonly Dictionary<string, object> _services = new Dictionary<string, object>();

        #region Get Service

        public object GetService(Type serviceType)
        {
            throw new NotImplementedException();
        }

        public object GetService(Type serviceType, string serviceName)
        {
            throw new NotImplementedException();
        }

        public T GetService<T>()
            where T : class
        {
            throw new NotImplementedException();
        }

        public T GetService<T>(string serviceName)
            where T : class
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Register

        public void Register<T>(T instance)
            where T : class
        {
            throw new NotImplementedException();
        }

        public void Register<T>(T instance, string serviceName)
            where T : class
        {
            throw new NotImplementedException();
        }

        #endregion

        #region Unregister

        public void Unregister<T>()
            where T : class
        {
            throw new NotImplementedException();
        }

        public void Unregister<T>(string serviceName)
            where T : class
        {
            throw new NotImplementedException();
        }

        public void UnregisterAll()
        {
            throw new NotImplementedException();
        }

        #endregion

        private static string CreateKeyFromType(Type type)
        {
            throw new NotImplementedException();
        }

        private static string CreateKeyFromType<T>()
        {
            throw new NotImplementedException();
        }
    }
}

Nun haben wir also unser Grundgerüst komplett. Hier dürften die generischen Aufrufe direkt auffallen. Da jedoch die definition von generischen Elementen relativ Komplex ist, möchte ich an dieser Stelle auf der MSDN verweisen. Weitere Infos hier: Generics (C# Programming Guide)

 

UnitTests

Lasst uns also nun weitermachen mit unseren Tests. Hierfür legen wir im Test-Projekt nun einen Unterordner mit den Namen „_ServiceProvider“ an. Hier drin legen wir nun zunächst drei Test-Services an, welche wir in den UnitTests verwenden wollen.

namespace SmallMvvm.Services.UnitTests._ServiceProvider
{
    // Test Service A
    public interface ITestServiceA : IService
    { }

    public class TestServiceA : ITestServiceA
    {
        public string Name { get { return "Service A"; } }
    }

    // Test Service B
    public interface ITestServiceB : IService
    { }

    public class TestServiceB : ITestServiceB
    {
        public string Name { get { return "Service B"; } }
    }

    // Test Service C
    public interface ITestServiceC : IService
    { }

    public class TestServiceC : ITestServiceC
    {
        public string Name { get { return "Service C"; } }
    }
}

Weiter geht es mit unseren eigentlichen Tests. Fangen wir an mit unseren Tests für die „GetService“-Funktionalitäten. Hierfür benötigen wir die Prüfung des „Usage“ also des „So nutzen wir es“-Falls als auch der Fall, dass ein geforderter Service eventuell noch gar nicht registriert wurde.

using NUnit.Framework;
using SmallMvvm.Services.Exceptions;

namespace SmallMvvm.Services.UnitTests._ServiceProvider
{
    [TestFixture]
    public class GetServiceFixture
    {
        [Test]
        public void GetUsage()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            serviceProvider.Register<ITestServiceA>(new TestServiceA());

            ITestServiceA testServiceA = serviceProvider.GetService<ITestServiceA>();
            Assert.That(testServiceA.Name, Is.EqualTo("Service A"));  
        }

        [Test]
        public void FailToGetUnregisteredService()
        {
            IServiceProvider serviceProvider = new ServiceProvider();

            ServiceNotAvailableException exception = Assert.Throws<ServiceNotAvailableException>(() => serviceProvider.GetService<ITestServiceA>());
            Assert.That(exception.GetType(), Is.EqualTo(typeof(ServiceNotAvailableException)));  
        }
    }
}

Weiter mit den Tests zum Registrieren von Services. Hier gibt es mit Abstand am meisten zu testen. Den hier müssen wir wiedermal den“Usage“-Fall prüfen, den Fall, dass ein Service bereits existiert, der Fall das die Instanz „NULL“ ist, der Fall, dass der Servicename leer ist sowie der Fall, dass der Servicename leer ist. Dies sieht also wie folgt aus:

using System;
using NUnit.Framework;

namespace SmallMvvm.Services.UnitTests._ServiceProvider
{
    [TestFixture]
    public class RegisterServiceFixture
    {
        [Test]
        public void RegisterUsage()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            serviceProvider.Register<ITestServiceA>(new TestServiceA());
            serviceProvider.Register<ITestServiceB>(new TestServiceB());
            serviceProvider.Register<ITestServiceC>(new TestServiceC());

            ITestServiceA testServiceA = serviceProvider.GetService<ITestServiceA>();
            ITestServiceB testServiceB = serviceProvider.GetService<ITestServiceB>();
            ITestServiceC testServiceC = serviceProvider.GetService<ITestServiceC>();

            Assert.That(testServiceA.Name, Is.EqualTo("Service A"));  
            Assert.That(testServiceB.Name, Is.EqualTo("Service B"));  
            Assert.That(testServiceC.Name, Is.EqualTo("Service C"));  
        }

        [Test, SetUICulture("en-US")]
        public void FailToRegisterExistingService()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            serviceProvider.Register<IService>(new TestServiceA());

            ArgumentException exception = Assert.Throws<ArgumentException>(() => serviceProvider.Register<IService>(new TestServiceB()));
            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentException)));
            Assert.That(exception.Message.Contains("Service of submitted type already exists. Use unregister to remove existing service type registration!"), Is.True);
        }

        [Test, SetUICulture("en-US")]
        public void FailToRegisterNullInstance()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() => serviceProvider.Register<IService>(null));

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentNullException)));
            Assert.That(exception.Message.Contains("Service instance cannot be null!"), Is.True);
        }

        [Test, SetUICulture("en-US")]
        public void FailToRegisterEmptyServiceName()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            ArgumentException exception = Assert.Throws<ArgumentException>(() => serviceProvider.Register<IService>(new TestServiceA(), string.Empty));

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentException)));
            Assert.That(exception.Message.Contains("ServiceName should not be empty!"), Is.True);
        }

        [Test, SetUICulture("en-US")]
        public void FailToRegisterSameServiceNameAsServiceType()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            ArgumentException exception = Assert.Throws<ArgumentException>(() => serviceProvider.Register<IService>(new TestServiceA(), typeof(IService).FullName));

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentException)));
            Assert.That(exception.Message.Contains("ServiceName should not be the name of the registered type!"), Is.True);
        }
    }
}

Der letzte Test, den wir benötigen, ist der Test unserer“UnregisterService“-Methoden. Hier ist letztendlich nur der „Usage“-Fallzu testen, da wir hier indirekt den fehlerhaften Fall mit testen.

using NUnit.Framework;
using SmallMvvm.Services.Exceptions;

namespace SmallMvvm.Services.UnitTests._ServiceProvider
{
    [TestFixture]
    public class UnregisterServiceFixture
    {
        [Test]
        public void UnregisterUsage()
        {
            IServiceProvider serviceProvider = new ServiceProvider();
            serviceProvider.Register<ITestServiceA>(new TestServiceA());

            ITestServiceA testServiceA = serviceProvider.GetService<ITestServiceA>();
            Assert.That(testServiceA.Name, Is.EqualTo("Service A"));  

            serviceProvider.Unregister<ITestServiceA>();

            ServiceNotAvailableException exception = Assert.Throws<ServiceNotAvailableException>(serviceProvider.Unregister<ITestServiceA>);
            Assert.That(exception.GetType(), Is.EqualTo(typeof(ServiceNotAvailableException)));
            Assert.That(exception.Message, Is.EqualTo(string.Format("Named service '{0}' of type '{1}' is not available!", typeof(ITestServiceA).FullName, typeof(ITestServiceA))));  
        }
    }
}

Das waren auch schon unsere Tests. Aufgrund der letzten Parts denke ich auch diese sind großteils selbsterklärend. Ansonsten steht euch gerne die Kontakt-Funktion sowie die private Nachrichten-Funktion für Fragen zur Verfügung. Lasst uns also nun mit der Implementierung unseres ServiceProviders fortführen und im Fazit den Zusammenhang erklären.

 

Implementierung

Also bei der Implementierung gibt es eigtl. nicht sonderlich viel zu beachten. Außer Halt, dass wir bereits zuvor die bereits angelegten Einträge in den Ressourcen benötigen. Aktuell sollten natürlich auch wieder sämtliche Tests vor der Wand laufen. Dies wollen wir also nun beheben.

using System;
using System.Collections.Generic;
using SmallMvvm.Services.Exceptions;
using SmallMvvm.Services.Properties;

namespace SmallMvvm.Services
{
    public class ServiceProvider : IServiceProvider
    {
        private readonly Dictionary<string, object> _services = new Dictionary<string, object>();

        #region Get Service

        public object GetService(Type serviceType)
        {
            return GetService(serviceType, CreateKeyFromType(serviceType));
        }

        public object GetService(Type serviceType, string serviceName)
        {
            if (!_services.ContainsKey(serviceName))
            {
                if (serviceName == CreateKeyFromType(serviceType))
                    throw new ServiceNotAvailableException(serviceType);

                throw new ServiceNotAvailableException(serviceName, serviceType);
            }

            return _services[serviceName];
        }

        public T GetService<T>()
            where T : class
        {
            return GetService<T>(CreateKeyFromType<T>());
        }

        public T GetService<T>(string serviceName)
            where T : class
        {
            if (!_services.ContainsKey(serviceName))
            {
                if (serviceName == CreateKeyFromType<T>())
                    throw new ServiceNotAvailableException(typeof (T));

                throw new ServiceNotAvailableException(serviceName, typeof (T));
            }

            return (T) _services[serviceName];
        }

        #endregion

        #region Register

        public void Register<T>(T instance)
            where T : class
        {
            if (instance == null)
                throw new ArgumentNullException("instance", Resources.ServiceProvider_ServiceInstanceCannotNull);

            if (_services.ContainsKey(CreateKeyFromType<T>()))
                throw new ArgumentException(Resources.ServiceProvider_ServiceOfSubmittedTypeAlreadyExists, "instance");

            _services.Add(CreateKeyFromType<T>(), instance);
        }

        public void Register<T>(T instance, string serviceName)
            where T : class
        {
            if (instance == null)
                throw new ArgumentNullException("instance", Resources.ServiceProvider_ServiceInstanceCannotNull);

            if (serviceName == null)
                throw new ArgumentNullException("serviceName", Resources.ServiceProvider_ServiceInstanceCannotNull);

            if (serviceName.Trim() == String.Empty)
                throw new ArgumentException(Resources.ServiceProvider_ServiceNameShouldNotBeEmpty, "serviceName");

            if (serviceName.Trim() == CreateKeyFromType<T>())
                throw new ArgumentException(Resources.ServiceProvider_ServiceName_ShouldNotBeNameOfRegisteredType, "serviceName");

            if (_services.ContainsKey(serviceName.Trim()))
                throw new ArgumentException(Resources.ServiceProvider_NameAlreadyExists, "serviceName");

            _services.Add(serviceName, instance);
        }

        #endregion

        #region Unregister

        public void Unregister<T>()
            where T : class
        {
            Unregister<T>(CreateKeyFromType<T>());
        }

        public void Unregister<T>(string serviceName)
            where T : class
        {
            if (!_services.ContainsKey(serviceName))
                throw new ServiceNotAvailableException(serviceName, typeof(T));

            _services.Remove(serviceName);
        }

        public void UnregisterAll()
        {
            _services.Clear();
        }

        #endregion

        private static string CreateKeyFromType(Type type)
        {
            return type.FullName;
        }

        private static string CreateKeyFromType<T>()
        {
            return CreateKeyFromType(typeof(T));
        }
    }
}

Kurz und knapp zur Implementierung gibt es zu sagen, dass es eigentlich immer nur eine Methode gibt, pro Gruppe die Arbeitet, alle anderen delegieren nur soweit durch. Mit Hilfe dieser Klasse können wir also nun – insofern wir diese immer durch Reichen, was noch kommen wird – Anwendungsweit auf verschiedene Services zurückgreifen. Diese können sowohl komplexe Methoden und Klassen beinhalten als auch z.B. nur die Anwendungskonfigurationen (Settings.cs).

Aber uns fehlt ja noch etwas. Ja richtig, die deutsche Lokalisierung.Hierfür gehen wir nun bei und legen eine Datei mit den Namen „Resources.de-DE.resx“ an (genauer Name ist wichtig). Diese verschieben wir anschließend zu unseren „Properties“-Ordner im Projekt. Nun können wir die Tabelle wie auch schon bei der Standard-Ressourcen-Datei mit einem Doppelklick öffnen und hier nun die Deutschen Lokalisierungen einfügen. Hier ist nur wichtig das der Name 1:1 mit den aus der Standard-Ressource-Datei übereinstimmt (erneut gilt, dass unser Trennzeichen das „->“ ist und wieder links der Name und rechts der Wert):

ServiceException_UnknownServiceError -> Unbekannter Servicefehler! 
ServiceNotAvailableException_NamedServiceNotAvailable -> Benannter Service '{0}' vom Typ '{1}' ist nicht verfügbar! 
ServiceNotAvailableException_ServiceTypeNotAvailable -> Service vom Typ '{0}' ist nicht verfügbar! 
ServiceProvider_NameAlreadyExists -> Name existiert bereits. ServiceName muss einzigartig sein! 
ServiceProvider_ServiceInstanceCannotNull -> Service Instanz darf nicht NULL sein! 
ServiceProvider_ServiceName_ShouldNotBeNameOfRegisteredType -> ServiceName darf nicht der selbe wie der Typ sein! 
ServiceProvider_ServiceNameShouldNotBeEmpty -> ServiceName darf nicht leer sein! 
ServiceProvider_ServiceOfSubmittedTypeAlreadyExists -> Service dieses Typen existiert bereits. Benutze die "unregister"-Methode um den alten Service zuvor zu entfernen!

Damit sind wir auch bereits mit unserer Implementierung fertig.Sämtliche Tests sollen nun auch wieder „grün“ laufen und funktionieren.

 

Abschließend

Nun möchte ich noch sagen, ich hoffe, dass dies wieder einigen Helfen konnte, den das ist mein Hauptziel mit dieser Aktion. Dazu möchte ich hinzufügen, da ich bereits einiges an Rückmeldung diesbezüglich erhielt: Ich versuche immer möglichst viel durch Sourcecode zu erläutern. Wer mehr Informationen möchte, kann sich gerne imRedmine registrieren und dort den Projektverlauf mit zusätzlichen Informationen wie auch die Informationen der einzelnen Tickets sehen.Zusätzlich stehe ich natürlich gerne für Fragen bereit, nutzt hierfür doch bitte die Kommentar Funktion damit alle etwas von eure Fragenhaben.

Fragen, Anregungen, Kritik, Hinweise und vieles mehr bitte über die Kommentar-Funktion. Nachfolgenden nun noch mal die Referenzen zu unserem Projekt.

Redmine-Projekt (SmallMvvm):
https://redmine.kruse-familie.eu/projects/small-mvvm

Repository (Mercurial, benötigt Redmine-Login):
https://kruse-familie.eu/hg/small-mvvm

Ebenfalls könnt Ihr den aktuellen Stand des Frameworks hier beziehen: Small MVVM – Revision 36