MVVM Tutorial – Part 3 (ViewModelBase und RelayCommand)

Solution Explorer

Solution Explorer

In diesem Teil erstellen wir die Basisklassen für unsere weiteren ViewModels und Commands. Dafür erstellen wir ein neues Projekt vom Typ Class Library. Als Referenzen fügen wir „WindowsBase“ und „PresentationCore“ hinzu.

ViewModel

Das Bindeglied zwischen View und Model ist das ViewModel. Da unser View sich per Data Binding an unser ViewModel bindet, müssen wir das Interface INotifyPropertyChanged implementieren, um das View zu benachrichtigen, wenn sich etwas geändert hat. Da dieser Prozess ist immer gleich ist, erstellen wir eine Basisklasse und lassen unsere konkreten ViewModel Implementationen von dieser Klasse erben.

Wir erstellen also ein neues Projekt vom Typ Class Library namens ViewModelBase und fügen die Dateien IViewModel.cs (Wir haben ja alle gelernt, dass man Interfaces benutzen soll) und ViewModel.cs hinzu. Da wir sowohl eine ViewModel Klasse erstellen wollen, welche generisch ist und direkt ein Objekt als Model bekommt, als auch eine, welche kein Objekt als Model bekommt (Das kann nützlich sein, wenn z.B. nicht ein Model, sondern mehrere, oder nur eine Verbindung, etc. als Model dienen sollen), bekommt jede Datei 2 Klassen, bzw. Interfaces.

Unsere Interfaces sind noch recht simpel:

namespace ViewModelBase
{
    using System.ComponentModel;

    /// <summary>
    /// Interface for ViewModel.
    /// </summary>
    public interface IViewModel : INotifyPropertyChanged
    {
    }
    /// <summary>
    /// Interface for ViewModel.
    /// </summary>
    /// <typeparam name="TModel">The type of the Model.</typeparam>
    public interface IViewModel<TModel> : IViewModel
    {
        /// <summary>
        /// Gets or sets the model.
        /// </summary>
        /// <value>
        /// The model.
        /// </value>
        [Browsable(false)]
        [Bindable(false)]
        TModel Model { get; set; }
    }
}

Das erste Interface ist ein reines Markerinterface, das zweite erweitert dies um ein generisches Model. Die Browsable und Bindable Attribute dienen dazu, dass unser Model nicht aus Versehen im View auftaucht. Wir wollen das Model zwar im Code verändern und darauf zugreifen können, es aber NIEMALS anzeigen.

Nun also zu unserer Klassenimplementation.
Zu erst das ViewModel:

    /// <summary>
    /// The view Model base class.
    /// </summary>
    [Serializable]
    public abstract class ViewModel : IViewModel
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModel"/> class.
        /// </summary>
        /// <remarks></remarks>
        protected ViewModel()
        {
            var initializationTask = new Task(() => Initialize());
            initializationTask.ContinueWith(result => InitializationCompletedCallback(result));
            initializationTask.Start();
        }

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        protected virtual void Initialize()
        {
        }

        /// <summary>
        /// Callback method for the async initialization.
        /// </summary>
        /// <param name="result">The result.</param>
        private void InitializationCompletedCallback(IAsyncResult result)
        {
            var initializationCompleted = InitializationCompleted;
            if (initializationCompleted != null)
            {
                InitializationCompleted(this, new AsyncCompletedEventArgs(null, !result.IsCompleted, result.AsyncState));
            }
            InitializationCompleted = null;
        }

        /// <summary>
        /// Occurs when the initialization is completed.
        /// </summary>
        public event AsyncCompletedEventHandler InitializationCompleted;

        /// <summary>
        /// Called when a property has changed.
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <remarks></remarks>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Occurs when a property value changes.
        /// </summary>
        /// <remarks></remarks>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion
    }

Wichtig ist hier der PropertyChangedEventHandler und OnPropertyChanged. Diese beiden sind für das MVVM Pattern zwingend notwendig, da sie das INotifyPropertyChanged Interface implementieren. Alle unsere Properties müssen nun wie folgt aussehen, damit bei jeder Änderung das Event ausgelöst wird:

        private int param;

        public int Param
        {
            get
            {
                return param;
            }
            set
            {
                if (Param != value)
                {
                    param = value;
                    OnPropertyChanged("Param");
                }
            }
        }

Ihr seht also, dass bei jeder Änderung die OnPropertyChanged Methode mit dem Propertynamen als Stringparameter aufgerufen wird. Leider gibt es im Framework hierfür noch keine einfachere Methode. Es gibt einige Plugins für Visual Studio, welche es erlauben eine Property mit einem Attribut zu dekorieren, sodass dieser Code beim compillieren automatisch erzeugt wird.

Der ganze restliche Kram in der Klasse dient lediglich dazu, dass man die Initialize() Methode überschreiben kann, welche automatisch in einem Task (also möglicherweise asynchron) aufgerufen wird. Da ich öfter das Problem hatte, dass ich in einem ViewModel einiges Initialisieren musste und so die GUI eingefroren ist, habe ich mich dazu entschlossen, dies in die Basis ViewModel Implementation auszulagern. Dies ist also für eine minimale Implementation nicht zwingend notwendig.

Nun fehlt nurnoch unsere Implementation für ein generisches Model. Da man oft ein objekt als Model hat, kann man auch hier einiges direkt in die Basisimplementation auslagern:

    /// <summary>
    /// The view Model base class.
    /// </summary>
    public abstract class ViewModel<TModel> : ViewModel, IViewModel<TModel> where TModel : class
    {
        private TModel model;

        /// <summary>
        /// The Model encapsulated by this ViewModel.
        /// </summary>
        /// <remarks>If you change this, all needed PropertyChanged events will be raised automatically.</remarks>
        [Browsable(false)]
        [Bindable(false)]
        public TModel Model
        {
            get
            {
                return model;
            }
            set
            {
                if (Model != value)
                {
                    // get all properties
                    var properties = this.GetType().GetProperties(BindingFlags.Public);
                    // all values before the model has changed
                    var oldValues = properties.Select(p => p.GetValue(this, null));
                    var enumerator = oldValues.GetEnumerator();

                    model = value;

                    // call OnPropertyChanged for all changed properties
                    foreach (var property in properties)
                    {
                        enumerator.MoveNext();
                        var oldValue = enumerator.Current;
                        var newValue = property.GetValue(this, null);

                        if ((oldValue == null && newValue != null)
                            || (oldValue != null && newValue == null)
                            || (!oldValue.Equals(newValue)))
                        {
                            OnPropertyChanged(property.Name);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ViewModel"/> class.
        /// </summary>
        /// <remarks></remarks>
        protected ViewModel(TModel model)
            : base()
        {
            this.model = model;
        }

        /// <summary>
        /// Returns a hash code for this instance.
        /// </summary>
        /// <returns>
        /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
        /// </returns>
        public override int GetHashCode()
        {
            return Model.GetHashCode();
        }

        /// <summary>
        /// Determines whether the specified <see cref="System.Object"/> is equal to this instance.
        /// </summary>
        /// <param name="obj">The <see cref="System.Object"/> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="System.Object"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public override bool Equals(object obj)
        {
            if (obj == null)
                return false;

            var other = obj as IViewModel<TModel>;

            if (other == null)
                return false;

            return Equals(other);
        }

        /// <summary>
        /// Determines whether the specified <see cref="IViewModel<TModel>"/> is equal to this instance.
        /// </summary>
        /// <param name="other">The <see cref="IViewModel<TModel>"/> to compare with this instance.</param>
        /// <returns>
        ///   <c>true</c> if the specified <see cref="IViewModel<TModel>"/> is equal to this instance; otherwise, <c>false</c>.
        /// </returns>
        public bool Equals(IViewModel<TModel> other)
        {
            if (other == null)
                return false;

            if (Model == null)
                return Model == other.Model;

            return Model.Equals(other.Model);
        }
    }

Hier können wir also direkt im Constructor ein Model übergeben. Das Schöne hieran ist, dass wir so einfachen Zugriff auf unser Model von außen bekommen und beim Ändern des Models direkt für alle geänderten Properties OnPropertyChanged aufrufen. Wenn sich nun z.B. unser Model in der Datenbank geändert hat und wir unser ViewModel updaten müssen, sparen wir uns das manuelle Zuweisen aller Properties und können einfach das Model austauschen. Auch können wir hier bereits ein sinnvolles GetHashCode und Equals implementieren, indem wir beides an das Model delegieren.

RelayCommand

Commands können in WPF an diverse Steuerelemente (z.B. einen Button) gebunden werden, welche dann bei bestimmten Ereignissen (z.B. Clicked) ausgeführt werden. Dafür müssen alle Command Klassen das Interface ICommand implementieren. Dieses Interface definiert im Grunde 2 Dinge: Eine Execute und eine CanExecute Methode. Zusätzlich wird noch ein CanExecuteChanged Event definiert. Da sich alle Commands normalerweise nur in der Execute und CanExecute Methode unterscheiden, erstellen wir hierfür eine RelayCommand Klasse, welche beides im Konstruktor übergeben bekommt (Ein Hoch auf Lambda Expressions).

    /// <summary>
    /// A simple relay Command for easy use of the Command pattern.
    /// </summary>
    /// <remarks></remarks>
    [Serializable]
    public class RelayCommand : ICommand
    {
        #region Fields

        /// <summary>
        /// The action to execute.
        /// </summary>
        private readonly Action<object> execute;

        /// <summary>
        /// The Predicate to indicate wether this command can execure or not.
        /// </summary>
        private readonly Predicate<object> canExecute;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="RelayCommand"/> class.
        /// </summary>
        /// <param name="execute">The execute.</param>
        /// <remarks></remarks>
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RelayCommand"/> class.
        /// </summary>
        /// <param name="execute">The execute.</param>
        /// <param name="canExecute">The can execute.</param>
        /// <remarks></remarks>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
            {
                throw new ArgumentNullException("execute");
            }

            this.execute = execute;
            this.canExecute = canExecute;
        }
        #endregion // Constructors

        #region ICommand Members

        /// <summary>
        /// Defines the method that determines whether the command can execute in its current state.
        /// </summary>
        /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
        /// <returns>true if this command can be executed; otherwise, false.</returns>
        /// <remarks></remarks>
        [DebuggerStepThrough]
        public bool CanExecute(object parameter)
        {
            if (canExecute == null)
            {
                return true;
            }

            return canExecute(parameter);
        }

        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute.
        /// </summary>
        /// <remarks></remarks>
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        /// <summary>
        /// Defines the method to be called when the command is invoked.
        /// </summary>
        /// <param name="parameter">Data used by the command.  If the command does not require data to be passed, this object can be set to null.</param>
        /// <remarks></remarks>
        public void Execute(object parameter)
        {
            execute(parameter);
        }

        #endregion // ICommand Members
    }

Wie ihr sicherlich schon bemerkt habt, bekommen sowohl Execute, als auch CanExecute ein parameter vom Typ object übergeben. Dieses kann im XAML Code definiert werden. Oft ist dies aber auch einfach null. Wenn ihr in eurem Command ein parameter möchtet, vergesst nicht Typechecks mit einzubauen, da es keine Garantie gibt, dass nicht jemand anderes euer ViewModel mit einem falschem Parametertyp benutzt! Das CanExecuteChanged Event delegieren wir einfach an den CommandManager von WPF weiter und brauchen uns nicht weiter darum kümmern.

Hier wieder die aktuelle Solution: MVVMTutorial Solution (Model und ViewModelBase)

Jetzt haben wir also eine Basis, mit ViewModel und RelayCommand. Im nächsten Teil implementieren wir dann ein konkretes ViewModel für unsere Person Klasse.

3 Gedanken zu „MVVM Tutorial – Part 3 (ViewModelBase und RelayCommand)

  1. Stefan Dube

    Vielen Dank für dies sehr Hilfreiche Artikelserie. (auch 2 Jahre später noch hilfreich 😉 ) Leider enthält der Code zwei kleine Fehler, die aber leicht korrigiert werden können:

    Falls ich in der Adaption nicht woanders was falsch gemacht habe, benötigt GetProperties() den Parameter
    ( BindingFlags.Instance | BindingFlags.Public )
    . Mit
    BindingFlags.Public
    werden keine Properties zurückgeliefet.
    Mit

    var oldValues = propertyInfoList.Select( _p => _p.GetValue( this, null ) );

    erhält oldValues nur eine Referenz auf die Properties. Durch

    var oldValueList = new List( propertyInfoList.Select( _p => _p.GetValue( this, null ) ) );

    bekommt man eine echte Kopie.

  2. Michael Hoffmann

    Hallo,
    sehr schönes Tutorial, dadurch ist mir einiges verständlicher.
    Ich habe dennoch eine Frage zur OnPropertyChanged Methode und dem ViewModel.

    wenn ich die OnPropertyChanged Methode dahingehend abändere, das ich [CallerMemberName] benutze, dann kann ich doch das Property TModel Model so abändern, oder?:
    [Browsable(false)]
    [Bindable(false)]
    public TModel Model
    {
    get => model;
    set
    {
    if(Model != value)
    {
    model = value;
    OnPropertyChanged();
    }
    }
    }
    Denn wenn bei OnPropertyChanged [CallerMemberName] angegben ist und ich keinen Paramter übergebe, werden doch alle Properties aktualisiert, richtig?

  3. Benjamin Lukner

    Kleiner Hinweis:
    InitializationCompleted(this, new AsyncCompletedEventArgs(null, !result.IsCompleted, result.AsyncState));
    ist falsch. Es muss natürlich
    initializationCompleted(this, new AsyncCompletedEventArgs(null, !result.IsCompleted, result.AsyncState));
    (mit kleinem i) heißen, sonst ist die gesamte angestrebte Thread-Safety ausgehebelt.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.