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

Dieses Mal wollen wir uns darum kümmern, die noch offene Kern-Funktionalität komplett einzubinden. Dies wäre jedoch für die erste Version dieses Frameworks lediglich eine Klasse welche die Standard.NET-String-Klasse um einige Funktionen erweitert. Aber nichts des so trotz lasst und damit beginnen, den auch diese Klasse will erst mal geschafft sein! Beginnen wir also erneut damit, zunächst die Ordner Struktur festzulegen, in welcher unsere Klasse einmal liegen soll.Anschließend werden wir wieder die leere Klasse definieren. Wenn dies auch geschehen ist, wird es aufgrund des Grey-Box-Testverfahren dazu übergehen, die UnitTests zu implementieren. Und zu guter Letzt wird natürlich wieder die eigentliche Funktionalität implementiert. Nun gut, also lasst uns beginnen!

Grundgerüst

Wie bereits gesagt, müssen wir zunächst definieren, wo unsere Klasse liegen soll. Hierfür hatte ich das Verzeichnis „Datatypes“ im Core-Projekt gewählt. Ist dies angelegt, geht es weiter mit dem Anlegen unserer Klasse. Diese heißt bei mir „StringExtensions“. In dieser legen wir nun die Methoden „string Reverse(this string)“, „string EncodeToBase64(this string, string)“, „string DecodeFromBase64(this string, string)“ sowie „bool IsNumeric(this string)“ und „bool IsInt32(this string)“ an. Natürlich werfen standardmäßig wieder alle Methoden eine Nicht-Implementiert-Ausnahme. Somit entsteht folgendes Konstrukt:

using System;
using System.Globalization;
using System.Text;

namespace SmallMvvm.Core.Datatypes
{
    public static class StringExtensions
    {
         public static string Reverse(this string str)
         {
            throw new NotImplementedException();
         }

        public static string EncodeToBase64(this string str, string encoding = "UTF-8")
        {
            throw new NotImplementedException();
        }

        public static string DecodeFromBase64(this string str, string encoding = "UTF-8")
        {
            throw new NotImplementedException();
        }

        public static bool IsNumeric(this string str)
        {
            throw new NotImplementedException();
        }

        public static bool IsInt32(this string str)
        {
            throw new NotImplementedException();
        }
    }
}

Was hier vermutlich auffällig ist, ist das „this“-Keyword direkt vor den Datentypen bei den Parametern. Dieser sorgt dafür, dass wenn wir diese Methoden aufrufen wollen, wir das auf jedem Objekt vom Typen „string“ machen könne. Auch direkt, wenn diese nur mit „“ im Quelltext geschrieben sind – insofern die Referenz auf unseren Namespaceexistiert. Ebenso ist auffällig, dass wir nie diesen Parameter mit Angeben müssen. Und schon erschließt sich das „this“-Wort. Den mit „this“ ist die Instanz eines Strings auf der wir diese Methode Aufrufen gemeint.

 

UnitTests

Als Nächstes müssen wir unsere Tests anlegen. Hierfür ist es – derÜbersichtshalber – nötig wieder ein „Datatypes“-Ordner in unseren „Core.UnitTests“-Projekt anzulegen. Hier drin wiederum legen wir einen Ordner für unsere Klasse an mit den Namen „_StringExtensions“. Nun zu unseren Tests. Hier sind insgesamt fünf Tests anzulegen, um eine sinnige Testabdeckung zu erzielen. Dazu möchte ich anmerken, dass ich entgegen der TestDriven-Development „Notation“ zunächst sämtliche Tests machen werde und erst danach die echten Methoden implementieren werde. Eigentlich müsste dies Stück für Stück passieren.

Beginnen wir mit den UnitTests zur Reverse-Methode. Hier müssen wir neben den normalen Usage-Case auch noch das Usage mit einen leeren String sowie eine möglichte „NullReferenceException“ prüfen. Diesetests könnten z.B. so aussehen, wie im folgenden Quelltext. Aber natürlich können diese auch anders formuliert werden.

using System;
using NUnit.Framework;
using SmallMvvm.Core.Datatypes;

namespace SmallMvvm.Core.UnitTests.Datatypes._StringExtensions
{
    [TestFixture]
    public class ReverseFixture
    {
        [Test]
        public void Usage()
        {
            string str = "Hello World!";
            str = str.Reverse();

            Assert.That(str, Is.EqualTo("!dlroW olleH"));
        }

        [Test]
        public void UsageWithEmpty()
        {
            const string STR1 = "";
            string str2 = STR1.Reverse();

            Assert.That(str2, Is.EqualTo(STR1));
            Assert.That(str2, Is.EqualTo(""));
        }

        [Test, SetUICulture("en-US")]
        public void WillThrowNullReferenceException()
        {
            string str = null;
            NullReferenceException exception = Assert.Throws<NullReferenceException>(() => str.Reverse());

            Assert.That(exception.GetType(), Is.EqualTo(typeof(NullReferenceException)));
            Assert.That(exception.Message, Is.EqualTo("Object reference not set to an instance of an object."));
        }
    }
}

Weiter geht es mit den Tests zum Codieren und Decodieren von Base64-Strings. Für diese Tests benötigen wir ein in Base64-Codiertes Objekt, welches als Testreferenz genutzt werden kann. Dies wird im Unterordner „Testfiles“ des „Core.UnitTests“-Projekt mit der Eigenschaft immer kopiert zu werden, hinzugefügt. Als Testfile kann folgende verwendet werden: Base64-Testfile (bitte Entpacken). Sowohl zum Codieren als auch zum Decodieren sind jeweils vier Tests nötig. Und zwar das Usagemit Daten direkt aus dem Quelltext, Usage mit der Binary-Datei, die mögliche ArgumentException sowie die mögliche ArgumentNullException.Folglich könnten diese beiden Tests wie jene in den folgenden Beispielen aussehen (erste ist zum Codieren, zweite zum Decodieren):

using System;
using System.IO;
using NUnit.Framework;
using SmallMvvm.Core.Datatypes;

namespace SmallMvvm.Core.UnitTests.Datatypes._StringExtensions
{
    [TestFixture]
    public class EncodeToBase64Fixture
    {
        [Test]
        public void Usage()
        {
            string str = "Hello World!";
            str = str.EncodeToBase64();

            Assert.That(str, Is.EqualTo("SGVsbG8gV29ybGQh"));
        }

        [Test]
        public void UsageEncodeBinaryContent()
        {
            const string EXPECTED_RESULT = "Qk10AAAAAAAAADYAAAAoAAAABQAAAAUAAAABABAAAAAAAD4AAADvv70AAADvv" +
                                           "70AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
                                           "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

            string content;
            using (StreamReader reader = new StreamReader(".\\Testfiles\\SomeFile.bin"))
            {
                string tmp = reader.ReadToEnd();
                content = tmp.EncodeToBase64();
            }

            Assert.That(content, Is.EqualTo(EXPECTED_RESULT));
        }

        [Test, SetUICulture("en-US")]
        public void WillThrowArgumentException()
        {
            ArgumentException exception = Assert.Throws<ArgumentException>(() => "".EncodeToBase64("blah"));

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentException)));
            Assert.That(exception.Message, Is.EqualTo("'blah' is not a supported encoding name.\r\nParameter name: name"));
        }

        [Test, SetUICulture("en-US")]
        public void WillThrowArgumentNullException()
        {
            string str = null;
            ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() => str.EncodeToBase64());

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentNullException)));
            Assert.That(exception.Message, Is.EqualTo("String reference not set to an instance of a String.\r\nParameter name: s"));
        }
    }
}
using System;
using System.IO;
using NUnit.Framework;
using SmallMvvm.Core.Datatypes;

namespace SmallMvvm.Core.UnitTests.Datatypes._StringExtensions
{
    [TestFixture]
    public class DecodeFromBase64Fixture
    {
        [Test]
        public void Usage()
        {
            string str = "SGVsbG8gV29ybGQh";
            str = str.DecodeFromBase64();

            Assert.That(str, Is.EqualTo("Hello World!"));
        }

        [Test]
        public void UsageDecodeBinaryContent()
        {
            const string CONTENT = "Qk10AAAAAAAAADYAAAAoAAAABQAAAAUAAAABABAAAAAAAD4AAADvv70AAADvv" +
                                   "70AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
                                   "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";

            const string FILE_NAME = ".\\SomeFile2.bin";

            if (File.Exists(FILE_NAME))
                File.Delete(FILE_NAME);

            using (StreamWriter writer = new StreamWriter(FILE_NAME, false))
            {
                writer.Write(CONTENT.DecodeFromBase64());
            }

            string origContent;
            using (StreamReader reader = new StreamReader(".\\Testfiles\\SomeFile.bin"))
            {
                origContent = reader.ReadToEnd();
            }

            string fakeContent;
            using (StreamReader reader = new StreamReader(FILE_NAME))
            {
                fakeContent = reader.ReadToEnd();
            }

            Assert.That(origContent, Is.EqualTo(fakeContent));
        }

        [Test, SetUICulture("en-US")]
        public void WillThrowArgumentException()
        {
            ArgumentException exception = Assert.Throws<ArgumentException>(() => "".DecodeFromBase64("blah"));

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentException)));
            Assert.That(exception.Message, Is.EqualTo("'blah' is not a supported encoding name.\r\nParameter name: name"));
        }

        [Test, SetUICulture("en-US")]
        public void WillThrowArgumentNullException()
        {
            string str = null;
            ArgumentNullException exception = Assert.Throws<ArgumentNullException>(() => str.DecodeFromBase64());

            Assert.That(exception.GetType(), Is.EqualTo(typeof(ArgumentNullException)));
            Assert.That(exception.Message, Is.EqualTo("Value cannot be null.\r\nParameter name: InString"));
        }
    }
}

Nun uns noch die Tests für die numerischen Prüfungen. Diese beiden sind sehr kurz gehalten, da wir für beide lediglich die Usage-Testimplementierung benötigten. Hier wären dafür die Beispiel Tests (erstes für IsNumeric, zweiteres für IsInt32):

using NUnit.Framework;
using SmallMvvm.Core.Datatypes;

namespace SmallMvvm.Core.UnitTests.Datatypes._StringExtensions
{
    [TestFixture]
    public class IsNumericFixture
    {
        [Test]
        public void Usage()
        {
            Assert.That("123".IsInt32(), Is.True);
            Assert.That("123.24".IsNumeric(), Is.True);
            Assert.That("12a.24".IsNumeric(), Is.False);
            Assert.That("qwertz".IsNumeric(), Is.False);
            Assert.That(((string)null).IsNumeric(), Is.False);
        }
    }
}
using NUnit.Framework;
using SmallMvvm.Core.Datatypes;

namespace SmallMvvm.Core.UnitTests.Datatypes._StringExtensions
{
    [TestFixture]
    public class IsInt32Fixture
    {
        [Test]
        public void Usage()
        {
            Assert.That("123".IsInt32(), Is.True);
            Assert.That("123.24".IsInt32(), Is.False);
            Assert.That("12a.24".IsInt32(), Is.False);
            Assert.That("qwertz".IsInt32(), Is.False);
            Assert.That(((string)null).IsNumeric(), Is.False);
        }
    }
}

 

Implementierung

Kommen wir also nun zur Implementierung. Da diese relativ kurz ist, gibt es diesmal erst den Quelltext und anschließend kleinere Erläuterungen.

using System;
using System.Globalization;
using System.Text;

namespace SmallMvvm.Core.Datatypes
{
    public static class StringExtensions
    {
         public static string Reverse(this string str)
         {
            char[] array = str.ToCharArray();
            char[] arrayReverse = new char[array.Length];

            int counter = 0;
            for (int i = array.Length - 1; i >= 0; i--)
            {
                arrayReverse[counter] = array[i];
                counter++;
            }

            return new string(arrayReverse);
         }

        public static string EncodeToBase64(this string str, string encoding = "UTF-8")
        {
            Encoding encode = Encoding.GetEncoding(encoding);
            byte[] bytes = encode.GetBytes(str);

            return Convert.ToBase64String(bytes);
        }

        public static string DecodeFromBase64(this string str, string encoding = "UTF-8")
        {
            Encoding encode = Encoding.GetEncoding(encoding);
            byte[] bytes = Convert.FromBase64String(str);

            return encode.GetString(bytes);
        }

        public static bool IsNumeric(this string str)
        {
            decimal d;
            return decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out d);
        }

        public static bool IsInt32(this string str)
        {
            int d;
            return int.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out d);
        }
    }
}

Mit ist klar, dass wir auch die Reverse-Methode von Linq nutzen können, jedoch erst ab .NET 3.5 und noch nicht in .NET 2.0, was auch noch vielerorts verwendet wird. Diese Frage werden schließlich einige bis zum Ende des „Tutorials“ geschleppt haben. 😉 Anschließend gibt es leider auch nicht all zu viel zu erläutern. Die Reverse-Methode dient zum zeichengenauen Umkehren eines Strings. Die EncodeToBase64sowie DecodeFromBase64 Methoden dienen für die Behandlung vonBase64 Kodierte Daten. Die IsNumeric Prüfung, prüft auf Zahlen unabhängig ob Gleitkomma oder nicht und die IsInt32 prüft nur auf Ganzzahlen. Natürlich könnte hier auch der Datentyp „long“ verwendet werden, um das Spektrum zu erweitern. Und natürlich könnte dies auch noch parallel mit „unsigned“-Typen passieren, um so auch auf nur positive Zahlen zu prüfen. Diese Erweiterung steht jedem frei offen.Kommen wir also zum Ende für diesen Part.

 

Abschließend

Nun möchte ich noch sagen, ich hoffe, dass dies wieder einigen Helfen konnte, den das ist mein Hauptziel mit dieser Aktion.

Fragen, Anregungen, Kritik, Hinweise und vieles mehr bitte über die Kommentar-Funktion. Nachfolgenden nun nochmal 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 21