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 theModel
through theViewModel
and does not know anything about theModel
- The
ViewModel
interacts with theModel
but is unaware of theView
- The
Model
is completely decoupled from theView
as it does not know anything about theView
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 { 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)); } } }
Example
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 » |
3 Comments »