WPF Series -3- MVVM Pattern

As mentioned in the previous post of this series the WPF application we implemented follows the MVVM pattern. MVVM stands for Model-View-ViewModel. The MVVM patterns intent is to provide a clean separation of concerns between UI and the business logic. It can be used on all XAML platforms.

About MVVM Pattern

Microsoft describes the motivation for MVVM pattern as follows (see The MVVM pattern).

Development technologies like Windows Forms, WPF, Silverlight, and Windows Phone provide a default experience that leads a developer down the path of dragging controls from a toolbox to a design surface, then writing code in the form’s code-behind file. As such applications grow in size and scope and are modified, complex maintenance issues begin to arise. These issues include the tight coupling between the UI controls and the business logic, which increases the cost of making UI modifications, and the difficulty of unit testing such code.

The advantages of the MVVM pattern may be summarised as follows.

  • Clean separation of concerns between UI and application/business logic
  • Better testability
  • Higher reusability of code
  • Improved maintainability

The MVVM pattern describes the following 3 core components.

View
A View is a XAML file that defines the layout and the controls of a window, dialog or page. Furthermore the bindings of the controls to the ViewModel are specified in View. It is responsible for the visualization of data.

ViewModel
The ViewModel is a C# class that gets bound by a View. It interacts with the Model to expose data to the View. The main responsibilities of the ViewModel are data preparation, validation and handling of commands and dialogs.

Model
The Model contains the business logic and provides data to be consumed by the ViewModel. It is completely decoupled from UI (i.e. WPF) specific stuff.

Important

  • The View acesses the data from the Model through the ViewModel and does not know anything about the Model
  • The ViewModel interacts with the Model but is unaware of the View
  • The Model is completely decoupled from the View as it does not know anything about the View

INotifyPropertyChanged

ViewModel classes should implement INotifyPropertyChanged (even if values of ViewModel properties do not change). Not doing so can lead to a memory leak (for details see here).

In our project we created a base class for all ViewModel classes that contains the implementation of the INotifyPropertyChanged method. By convention every ViewModel class has to inherit from ViewModelBase.

ViewModelBase

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using System.Windows;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.ViewModels
{
    public class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
    {
        protected static readonly App CurrentApp = Application.Current as App;

        public event PropertyChangedEventHandler PropertyChanged;

        protected void RaisePropertyChangedEvent(string propertyName)
        {
            Contract.Requires(!string.IsNullOrWhiteSpace(propertyName));

            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Examples

In this section I’ll provide a simple example of the components of the MVVM pattern.

View

Home.xaml

<Page x:Class="biz.dfch.CS.ArbitraryWpfApplication.UI.Pages.Home"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:vm="clr-namespace:biz.dfch.CS.ArbitraryWpfApplication.UI.ViewModels"
      xmlns:p="clr-namespace:biz.dfch.CS.ArbitraryWpfApplication.UI.Properties"
      xmlns:ext="clr-namespace:biz.dfch.CS.ArbitraryWpfApplication.UI.Extensions"
      xmlns:sew="clr-namespace:biz.dfch.CS.ArbitraryWpfApplication.UI.Domain.Sewer"
      xmlns:gl="clr-namespace:System.Globalization;assembly=mscorlib"
      mc:Ignorable="d"

      Title="{x:Static p:Resources.Page_Home_Title}" >

    <Page.DataContext>
        <vm:HomeViewModel></vm:HomeViewModel>
    </Page.DataContext>

    <DockPanel>
        <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Name="ButtonList" Style="{DynamicResource MetroCircleButtonStyle}" Content="{StaticResource appbar_interface_list}" Click="OnButtonListClick" IsEnabled="{Binding IsButtonListEnabled}"></Button>
        </StackPanel>
        <Grid VerticalAlignment="Center" HorizontalAlignment="Center" FocusManager.FocusedElement="{Binding ElementName=TextBoxMunicipalityName}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Label Grid.Row="0" Grid.Column="0" Content="{x:Static p:Resources.Page_Home_Label__Date}"/>
            <Label Grid.Row="1" Grid.Column="0" Content="{x:Static p:Resources.Page_Home_Label__Municipality}"/>
            <Label Grid.Row="2" Grid.Column="0" Content="{x:Static p:Resources.Page_Home_Label__Operator}"/>
            <DatePicker Grid.Row="0" Grid.Column ="1" Name="DatePicker" SelectedDate="{Binding CreationDate, Mode=TwoWay, ConverterCulture={x:Static gl:CultureInfo.CurrentCulture}}" FirstDayOfWeek="Monday" />
            <TextBox Name="TextBoxMunicipalityName" Grid.Row="1" Grid.Column ="1" Text="{Binding MunicipalityName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox Grid.Row="2" Grid.Column ="1" Text="{Binding Operator, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        </Grid>
    </DockPanel>
</Page>

As you can see in this sample the ViewModel (HomeViewModel) gets bound to the View by assigning it to the DataContext of the corresponding View. The assignment can either be done directly in XAML or in code behind.

Home.xaml.cs

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using biz.dfch.CS.ArbitraryWpfApplication.UI.Extensions;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.Pages
{
    public partial class Home
    {
        public Home()
        {
            InitializeComponent();
        }

        private void OnButtonListClick(object sender, System.Windows.RoutedEventArgs e)
        {
            NavigationService.NavigateToPage(nameof(Protocols));
        }
    }
}

ViewModel

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.ComponentModel.DataAnnotations;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Constants;
using biz.dfch.CS.ArbitraryWpfApplication.UI.Domain.Sewer;

namespace biz.dfch.CS.ArbitraryWpfApplication.UI.ViewModels
{
    public class HomeViewModel : ViewModelBase
    {
        private readonly ArbitraryModel model;

        public HomeViewModel ()
        {
            model = new ArbitraryModel();
        }

        #region Properties

        public string MunicipalityName
        {
            get { return model.MunicipalityName; }
            set
            {
                model.MunicipalityName = value;
                RaisePropertyChangedEvent(nameof(MunicipalityName));
                RaisePropertyChangedEvent(nameof(IsButtonListEnabled));
            }
        }
        public string Operator
        {
            get { return model.Operator; }
            set
            {
                model.Operator = value;
                RaisePropertyChangedEvent(nameof(Operator));
                RaisePropertyChangedEvent(nameof(IsButtonListEnabled));
            }
        }

        public DateTime CreationDate
        {
            get { return model.CreationDate; }
            set
            {
                model.CreationDate = value;
                RaisePropertyChangedEvent(nameof(CreationDate));
                RaisePropertyChangedEvent(nameof(IsButtonListEnabled));
            }
        }

        public bool IsButtonListEnabled => DateTime.MinValue != CreationDate
                                                   && !string.IsNullOrWhiteSpace(MunicipalityName)
                                                   && !string.IsNullOrWhiteSpace(Operator);

        #endregion Properties
    }
}

MODEL

/**
 * Copyright 2017 d-fens GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;

public class ArbitraryModel
{
    public DateTime CreationDate { get; set; } = DateTime.Today;
    public string MunicipalityName { get; set; }
    public string Operator { get; set; }
}

Finally I would like to thank Mark Withall for his great blog post The World’s Simplest C# WPF MVVM Example.

« WPF Series -2- WPF Application Design WPF Series -4- MahApps.Metro and MahApps.Metro.Resources »

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: