Der Weg zum eigenen MVVM-Framework – Part 2 (Implementierung bestehender Funktionen)

In diesen Part kümmern wir uns um die Einbindung der bereits im Blog entstanden Funktionen (ViewModel, ExtendedPropertyBase sowie PropertyChangedBase). Um hierbei den Grey-Box-Verfahren zu entsprechen, werden zunächst alle Grundgerüste der Reihe nach angelegt, anschließend die UnitTests und erst dann die Implementierung der Funktionalität. Beginnen wir mit der Definition, was sinnvoll zu testen ist. Zunächst ist es natürlich Sinnvoll unsere beiden Property-Klassen “PropertyChangedBase” sowie “ExtendedPropertyBase” nahezu vollständig zu testen (AusnahmeDispose). Aber was ist mit unserer ViewModel-Klasse? Hier finde ich, sind Tests un-sinnvoll Die nötigen Tests wären aufgrund der Asynchronität komplex und da wir nur fertige .NET-Funktionen direkt aufrufen, ist das testen auch nahe zu unsinnvoll. 

Verwendung der „PropertyChangedBase“-Klasse

Nun gut beginnen wir mit „PropertyChangedBase“. Diese gehört zu den Core-Funktionen. Genauer zur Kategorie der „Property“-Funktionen. Also legen wir den Ordner „Property“ im „SmallMvvm.Core“ Projekt an. Hier fügen wir nun folgenden Sourcecode zunächst ein.

using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Threading;

namespace SmallMvvm.Core.Property
{
    public abstract class PropertyChangedBase : INotifyPropertyChanged
    {
        protected SynchronizationContext uiContext = SynchronizationContext.Current;

        public event PropertyChangedEventHandler PropertyChanged;

        #if NET45

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            throw new NotImplementedException();
        }

        #else

        protected virtual void OnPropertyChanged(string propertyName)
        {
            throw new NotImplementedException();
        }

        #endif

        protected virtual void MultipleOnPropertyChanged(string[] propertys)
        {
            throw new NotImplementedException();
        }

        #if !NET20 && !NET30

        protected virtual void OnPropertyChanged<T>(Expression<Func<T>> expression)
        {
            throw new NotImplementedException();
        }

        protected virtual void MultipleOnPropertyChanged<T>(Expression<Func<T>>[] expressions)
        {
            throw new NotImplementedException();
        }

        protected string GetPropertyName<T>(Expression<Func<T>> expression)
        {
            throw new NotImplementedException();
        }

        #endif
    }
}

Als Nächstes müssen wir die UnitTests dazu erstellen. Diese würde ich im „SmallMvvm.Core.UnitTests“-Projekt im Unterverzeichnis „Property“ erstellen. Hierfür erzeuge ich im gerade erzeugten „Property“-Ordner derUnitTests noch einen „_PropertyChangedBase“-Ordner und dort füge ich nun zunächst eine „PropertyChangedTestClass.cs“ ein, welche folgenden Inhalt bereitstellt:

using System;
using System.Linq.Expressions;
using SmallMvvm.Core.Property;

namespace SmallMvvm.Core.UnitTests.Property._PropertyChangedBase
{
    internal class PropertyChangedTestClass : PropertyChangedBase
    {
        #if NET45

        internal new virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);
        }

        #else

        internal new virtual void OnPropertyChanged(string propertyName)
        {
            base.OnPropertyChanged(propertyName);
        }

        #endif

        internal new virtual void MultipleOnPropertyChanged(string[] propertys)
        {
            base.MultipleOnPropertyChanged(propertys);
        }

        #if !NET20 && !NET30

        internal new virtual void OnPropertyChanged<T>(Expression<Func<T>> expression)
        {
            base.OnPropertyChanged(expression);
        }

        internal new virtual void MultipleOnPropertyChanged<T>(Expression<Func<T>>[] expressions)
        {
            base.MultipleOnPropertyChanged(expressions);
        }

        internal new string GetPropertyName<T>(Expression<Func<T>> expression)
        {
            return base.GetPropertyName(expression);
        }

        #endif
    }
}

Nun haben wir also eine Klasse die uns in der Lage versetzt, die abstrakte PropertyChangedBase-Klasse zu testen. Als Nächstes folgt unser erster Test. Hierfür habe ich die Datei „OnPropertyChangedFixture.cs“ angelegt, mit diesem Inhalt:

using System.ComponentModel;
using NUnit.Framework;

namespace SmallMvvm.Core.UnitTests.Property._PropertyChangedBase
{
    [TestFixture]
    public class OnPropertyChangedFixture
    {

        [Test]
        public void Usage()
        {
            PropertyChangedEventArgs result = null;
            PropertyChangedTestClass instance = new PropertyChangedTestClass();
            instance.PropertyChanged += (sender, args) => result = args;

            Assert.That(result, Is.Null);
            instance.OnPropertyChanged("SomeProperty");

            Assert.That(result, Is.Not.Null);
            Assert.That(result.PropertyName, Is.EqualTo("SomeProperty"));
        }

        #if NET45

        private class NestedNet45PropertyChangedTestClass : PropertyChangedBase
        {
            private string _someProperty;
            public string SomeProperty
            {
                get { return _someProperty; }
                set
                {
                    _someProperty = value;
                    OnPropertyChanged();
                }
            }
        }

        [Test]
        public void Net45Usage()
        {
            PropertyChangedEventArgs result = null;
            NestedNet45PropertyChangedTestClass instance = new NestedNet45PropertyChangedTestClass();
            instance.PropertyChanged += (sender, args) => result = args;

            Assert.That(result, Is.Null);
            instance.SomeProperty = "some value";

            Assert.That(result, Is.Not.Null);
            Assert.That(result.PropertyName, Is.EqualTo("SomeProperty"));  
        }

        #endif

        [Test]
        public void UsageWithExpression()
        {
            PropertyChangedEventArgs result = null;
            PropertyChangedTestClass instance = new PropertyChangedTestClass();
            instance.PropertyChanged += (sender, args) => result = args;

            Assert.That(result, Is.Null);
            instance.OnPropertyChanged(() => result);

            Assert.That(result, Is.Not.Null);
            Assert.That(result.PropertyName, Is.EqualTo("result"));
        }
    }
}

Nun können wir zunächst diese Funktionalitäten zunächst Implementieren oder alternativ die weiteren UnitTests zur Klasse schreiben. Ich habe mich hier für zweiteres Entschieden um den Blog übersichtlicher zu gestallten. Also folgt nun der zweite Test, hierfür habe ich nun die Datei „MultipleOnPropertyChangedFixture.cs“ angelegt. Diese beinhaltet Folgendes:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq.Expressions;
using NUnit.Framework;

namespace SmallMvvm.Core.UnitTests.Property._PropertyChangedBase
{
    [TestFixture]
    public class MultipleOnPropertyChangedFixture
    {
        [Test]
        public void Usage()
        {
            List<PropertyChangedEventArgs> results = new List<PropertyChangedEventArgs>();
            PropertyChangedTestClass instance = new PropertyChangedTestClass();
            instance.PropertyChanged += (sender, args) => results.Add(args);

            Assert.That(results.Count, Is.EqualTo(0));
            instance.MultipleOnPropertyChanged(new[] { "SomeProperty1", "SomeProperty2" });

            Assert.That(results.Count, Is.EqualTo(2));
            Assert.That(results[0].PropertyName, Is.EqualTo("SomeProperty1"));
            Assert.That(results[1].PropertyName, Is.EqualTo("SomeProperty2"));  
        }

        [Test]
        public void UsageWithExpression()
        {
            string str1 = string.Empty;
            string str2 = string.Empty;

            List<PropertyChangedEventArgs> results = new List<PropertyChangedEventArgs>();
            PropertyChangedTestClass instance = new PropertyChangedTestClass();
            instance.PropertyChanged += (sender, args) => results.Add(args);

            Assert.That(results.Count, Is.EqualTo(0));
            instance.MultipleOnPropertyChanged(new[]
                {
                    (Expression<Func<string>>) (() => str1), 
                    (Expression<Func<string>>) (() => str2)
                });

            Assert.That(results.Count, Is.EqualTo(2));
            Assert.That(results[0].PropertyName, Is.EqualTo("str1"));
            Assert.That(results[1].PropertyName, Is.EqualTo("str2"));  
        }
    }
}

Zu guter Letzt noch der dritte und abschließende Test für unserePropertyChanged-Basisklasse. Hierfür wurde nun die Datei „“ angelegt mit diesem Inhalt:

using NUnit.Framework;

namespace SmallMvvm.Core.UnitTests.Property._PropertyChangedBase
{
    [TestFixture]
    public class GetPropertyNameFixture
    {
        [Test]
        public void Usage()
        {
            PropertyChangedTestClass instance = new PropertyChangedTestClass();
            string propertyName = instance.GetPropertyName(() => instance);

            Assert.That(propertyName, Is.EqualTo("instance"));  
        }
    }
}

Nun geht es ans Werk die aktuell noch schief laufenden Tests, zu implementieren. Da wir hier die Funktionen 1:1 so wie aus diesem Blog-Post übernehmen: „Property Changed – Komplett und kompakt!“ werde ich hier keine weiteren Erläuterung zu dessen Inhalt und Funktionsweise nennen, sondern lediglich nochmals die fertige Klasse abdrucken. Fragen dürfen dennoch gerne auch in den Kommentaren oder per Mail gestellt werden. Hier also nochmals der komplette Code dieser Klasse:

using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Threading;

namespace SmallMvvm.Core.Property
{
    public abstract class PropertyChangedBase : INotifyPropertyChanged
    {
        protected SynchronizationContext uiContext = SynchronizationContext.Current;

        public event PropertyChangedEventHandler PropertyChanged;

        #if NET45

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            if (uiContext != null)
            {
                uiContext.Send(delegate
                    {
                        if (PropertyChanged != null)
                            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }, null);
            }
            else
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #else

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (uiContext != null)
            {
                uiContext.Send(delegate
                    {
                        if (PropertyChanged != null)
                            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                    }, null);
            }
            else
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endif

        protected virtual void MultipleOnPropertyChanged(string[] propertys)
        {
            foreach (string property in propertys)
                OnPropertyChanged(property);
        }

        #if !NET20 && !NET30

        protected virtual void OnPropertyChanged<T>(Expression<Func<T>> expression)
        {
            OnPropertyChanged(GetPropertyName(expression));
        }

        protected virtual void MultipleOnPropertyChanged<T>(Expression<Func<T>>[] expressions)
        {
            foreach (Expression<Func<T>> expression in expressions)
                OnPropertyChanged(GetPropertyName(expression));
        }

        protected string GetPropertyName<T>(Expression<Func<T>> expression)
        {
            MemberExpression memberExpression = expression.Body as MemberExpression;
            return memberExpression != null ? memberExpression.Member.Name : "";
        }

        #endif
    }
}

Verwendung der „ExtendedPropertyBase“-Klasse

Weiter geht es mit unserer ExtendedPropertyBase-Klasse. Auch hier wird wieder zunächst ein Grundgerüst Implementiert. Auch diese Datei befindet sich wieder im „Property“-Ordner des Projektes „SmallMvvm.Core“. Hier also unser zu erstellendes Grundgerüst:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace SmallMvvm.Core.Property
{
    public abstract class ExtendedPropertyBase : PropertyChangedBase, IDisposable
    {
        private readonly Dictionary<string, object> _propertyValues = new Dictionary<string, object>();

        #region Get

        #if !NET20 && !NET30

        protected T Get<T>(Expression<Func<T>> expression, T defaultValue = default(T))
        {
            throw new NotImplementedException();
        }

        protected T Get<T>(string propertyName, T defaultValue = default(T))
        {
            throw new NotImplementedException();
        }

        #endif

        #endregion

        #region Set

        #if !NET20 && !NET30

        protected void Set<T>(Expression<Func<T>> expression, T value)
        {
            throw new NotImplementedException();
        }

        protected void Set<T>(string propertyName, T value)
        {
            throw new NotImplementedException();
        }

        #endif

        #if NET45

        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            throw new NotImplementedException();
        }

        #endif

        #endregion

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

Wie auch bereits zuvor werden auch diesmal wieder die Tests zuerst Implementiert. Auch diesmal werden wir wieder eine Test-Klasse benötigen welche uns das Testen ermöglicht. Allerdings ist dieses mal anders, wie wir Testen. Den sowohl die „Get“-Funktion als auch die „Set“-Funktion welche die einzig Sinnigen Tests ergeben, sind nur zusammen Testbar. Also beginnen wir nun unsere Test-Klasse anzulegen. Diese kommt ebenfalls in den „Property“-Ordner des Projektes „SmallMvvm.Core.UnitTests“ – wie auch bereits die von PropertyChanged zuvor. Diesmal legen wir auch wieder ein Ordner mit den Klassennamen an, in diesem Fall „_ExtendedPropertyBase“. Die Datei unsere Test-Klasse heißt an dieser Stelle „ExtendedPropertyTestClass.cs“ und beinhaltet folgendes:

using System;
using System.Linq.Expressions;
using SmallMvvm.Core.Property;

namespace SmallMvvm.Core.UnitTests.Property._ExtendedPropertyBase
{
    internal class ExtendedPropertyTestClass : ExtendedPropertyBase
    {
        #region Get

        #if !NET20 && !NET30

        internal new T Get<T>(Expression<Func<T>> expression, T defaultValue = default(T))
        {
            return base.Get(expression, defaultValue);
        }

        internal new T Get<T>(string propertyName, T defaultValue = default(T))
        {
            return base.Get(propertyName, defaultValue);
        }

        #endif

        #endregion

        #region Set

        #if !NET20 && !NET30

        internal new void Set<T>(Expression<Func<T>> expression, T value)
        {
            base.Set(expression, value);
        }

        internal new void Set<T>(string propertyName, T value)
        {
            base.Set(propertyName, value);
        }

        #endif

        #if NET45

        internal new bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            base.SetProperty(ref storage, value, propertyName);
        }

        #endif

        #endregion

        internal new void Dispose()
        {
            base.Dispose();
        } 
    }
}

Nun zum Test. Der Test besteht aus einen Zusammenschluss von Methoden welche zu Testen sind – in diesem Fall die bereits genante „Get“- sowie „Set“-Funktion. Die Datei zum Test wurde bei mir „GetSetFixture.cs“ genannt und hat folgenden Inhalt:

using NUnit.Framework;

namespace SmallMvvm.Core.UnitTests.Property._ExtendedPropertyBase
{
    [TestFixture]
    public class GetSetFixture
    {
        [Test]
        public void Usage()
        {
            ExtendedPropertyTestClass instance = new ExtendedPropertyTestClass();
            instance.Set("SomeProperty", "SomeValue");

            Assert.That(instance.Get("SomeProperty", "123"), Is.EqualTo("SomeValue"));  
        }

        [Test]
        public void UsageWithExpression()
        {
            string str = string.Empty;

            ExtendedPropertyTestClass instance = new ExtendedPropertyTestClass();
            instance.Set(() => str, "SomeValue");

            Assert.That(instance.Get(() => str, "123"), Is.EqualTo("SomeValue"));  
        }

        [Test]
        public void UsageWithoutSet()
        {
            string str = string.Empty;

            ExtendedPropertyTestClass instance = new ExtendedPropertyTestClass();
            Assert.That(instance.Get(() => str, "123"), Is.EqualTo("123"));  
        }

        [Test]
        public void UsageAfterDispose()
        {
            string str = string.Empty;

            ExtendedPropertyTestClass instance = new ExtendedPropertyTestClass();
            instance.Set(() => str, "SomeValue");
            instance.Dispose();

            Assert.That(instance.Get(() => str), Is.Null);
        }
    }
}

Auch dieses mal geht es nun dazu über die Funktionalität der eigentlichen Klasse zu Implementieren. Da es sich aber ebenso wie bei der PropertyChanged-Basisklasse um eine Implementierung eines bereits im Blog detaillierten Objektes handelt, wird dies wieder mal mit abdrucken des Quelltextes abgeharkt. Der Original Blog-Post zu dieser Klasse ist folgender: „Propertys mal ganz anders“ und der Quellcode sollte nun so aussehen:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace SmallMvvm.Core.Property
{
    public abstract class ExtendedPropertyBase : PropertyChangedBase, IDisposable
    {
        private readonly Dictionary<string, object> _propertyValues = new Dictionary<string, object>();

        #region Get

        #if !NET20 && !NET30

        protected T Get<T>(Expression<Func<T>> expression, T defaultValue = default(T))
        {
            return Get(GetPropertyName(expression), defaultValue);
        }

        protected T Get<T>(string propertyName, T defaultValue = default(T))
        {
            return _propertyValues.ContainsKey(propertyName) ? (T)_propertyValues[propertyName] : defaultValue;
        }

        #endif

        #endregion

        #region Set

        #if !NET20 && !NET30

        protected void Set<T>(Expression<Func<T>> expression, T value)
        {
            Set(GetPropertyName(expression), value);
        }

        protected void Set<T>(string propertyName, T value)
        {
            if (_propertyValues.ContainsKey(propertyName))
            {
                if (_propertyValues[propertyName] != null &&
                    (_propertyValues[propertyName] ?? new object()).Equals(value))
                    return;

                if (value is IComparable)
                {
                    if (_propertyValues[propertyName] != ((IComparable)value))
                    {
                        _propertyValues[propertyName] = value;
                        OnPropertyChanged(propertyName);
                    }
                }
                else
                {
                    _propertyValues[propertyName] = value;
                    OnPropertyChanged(propertyName);
                }
            }
            else
            {
                _propertyValues.Add(propertyName, value);
                OnPropertyChanged(propertyName);
            }
        }

        #endif

        #if NET45

        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Equals(storage, value)) 
                return false;

            storage = value;
            OnPropertyChanged(propertyName);

            return true;
        }

        #endif

        #endregion

        public void Dispose()
        {
            _propertyValues.Clear();
        }
    }
}

Verwendung der „ViewModel“-Klasse

Zu guter letzt noch unsere ViewModel-Klasse. Diese wird in das Root-Verzeichnes vom „SmallMvvm.Presentation“-Projekt abgelegt und bekommt auch keine UnitTests. Also können wir es an dieser Stelle kurz fassen und die bereits erarbeitete Klasse aus dem alten Blog-Post übernehmen. Der Original Blog-Post ist diesmal hier zu finden: „Falscher Thread? Kein Problem!“ Und die Klasse sieht nun wie folgt aus:

using System;
using System.Threading;
using SmallMvvm.Core.Property;

namespace SmallMvvm.Presentation
{
    public abstract class ViewModel : ExtendedPropertyBase
    {
        #region Default context

        public void SendToUiThread(Action action, object stateParams = null)
        {
            uiContext.Send(state => action.Invoke(), stateParams);
        }

        public void PostToUiThread(Action action, object stateParams = null)
        {
            uiContext.Post(state => action.Invoke(), stateParams);
        }

        #endregion

        #region Custom context

        public void SendToThread(Action action, SynchronizationContext context, object stateParams = null)
        {
            context.Send(state => action.Invoke(), stateParams);
        }

        public void PostToThread(Action action, SynchronizationContext context, object stateParams = null)
        {
            context.Post(state => action.Invoke(), stateParams);
        }

        #endregion
    }
}

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 unseren 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 10