We will be based on the MVVM conceptual module to build out physical module.
C: Make a MVVM WPF Demo
From online, a lot of articles claim that they made a simplest sample of MVVM pattern, which suggests how difficult to make a simple model for MVVM. In this article, we want to introduce the concept of the MVVM, so we want the simplest model. Instead of making one, we use one from
MVVM for Beginners (thanks to the author). With some explanations, we try to make the context clue very clear to demo the MVVM concept.
- Step 1: Create a View
- Step 2: Create a Model
- Step 3: Create a Binder
- Step 4: Create a DataContext
- Step 5: Create a ViewModel
- Step 6: Create a Synchronization
- Step 7: Create a Command
C-0: Create a WPF app
We use the current version of Visual Studio 2019 16.9.4 and NET Framework 4.8 to build the app:
- Start Visual Studio and select Create a new project.
- In the Create a new project dialog,
- on the left side panel select Windows,
- on the right Select WPF Application (.NET Framework)
- Named as SamplesMVVM > OK.
Step 1: Create a View
Open MainWidow.xaml and add replace the <Grid>
tag with:
- <Grid>
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="150"/>
- <ColumnDefinition Width="5"/>
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
-
- <DockPanel>
- <TextBlock Text="Added Names"
- DockPanel.Dock="Top" Margin="5,3"/>
- <ListBox></ListBox>
- </DockPanel>
-
- <GridSplitter Grid.Column="1"
- VerticalAlignment="Stretch" Width="5"
- Background="Gray" HorizontalAlignment="Left" />
-
- <Grid Grid.Column="2">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="Auto"/>
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="Auto"/>
- </Grid.RowDefinitions>
-
- <TextBlock Grid.Row="0" Text="Name" Margin="5,3"/>
- <TextBox Grid.Row="0" Grid.Column="1" Margin="5,3"/>
-
- <TextBlock Grid.Row="1" Text="Your name is:" Margin="5,3"/>
- <TextBlock Grid.Row="1" Grid.Column="1" Margin="5,3"/>
-
- <Button Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Left"
- Content="Add Me" Margin="5,3" MinWidth="75" />
-
- </Grid>
-
- </Grid>
Run the app (F5), you should see the following,
When one clicks the button or edits the text box, nothing happens! Let's fix that in the following steps.
C-1: Make a Separation between Model and View
Step 2: Create a Model
Right Click project > Add > Class, Choose Class Name as Model.cs
- public class Model
- {
- public string CurrentName { get; set; }
-
- public ObservableCollection<string> AddedNames { get; } = new ObservableCollection<string>();
- }
Step 3: Create a Binder
We modify MainWidow.xaml file to bind the list box, TextBox and BlockBox to the Model Data:
- <Grid>
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="150"/>
- <ColumnDefinition Width="5"/>
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
-
- <DockPanel>
- <TextBlock Text="Added Names" DockPanel.Dock="Top" Margin="5,3"/>
- <!--<ListBox></ListBox>-->
- <ListBox ItemsSource="{Binding AddedNames}"></ListBox>
- </DockPanel>
-
- <GridSplitter Grid.Column="1"
- VerticalAlignment="Stretch" Width="5"
- Background="Gray" HorizontalAlignment="Left" />
-
- <Grid Grid.Column="2">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="Auto"/>
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="Auto"/>
- <RowDefinition Height="Auto"/>
- </Grid.RowDefinitions>
-
- <!--<TextBlock Grid.Row="0" Text="Name" Margin="5,3"/>
- <TextBox Grid.Row="0" Grid.Column="1" Margin="5,3"/>
-
- <TextBlock Grid.Row="1" Text="Your name is:" Margin="5,3"/>
- <TextBlock Grid.Row="1" Grid.Column="1" Margin="5,3"/>-->
-
- <TextBlock Grid.Row="0" Text="Name" Margin="5,3"/>
- <TextBox Grid.Row="0" Grid.Column="1"
- Text="{Binding CurrentName,
- UpdateSourceTrigger=PropertyChanged}" Margin="5,3"/>
-
- <TextBlock Grid.Row="1" Text="Your name is:" Margin="5,3"/>
- <TextBlock Grid.Row="1" Grid.Column="1"
- Text="{Binding CurrentName}" Margin="5,3"/>
-
- <Button Grid.Row="2" Grid.ColumnSpan="2" HorizontalAlignment="Left"
- Content="Add Me" Margin="5,3" MinWidth="75" />
- </Grid>
- </Grid>
Step 4: Create a DataContext
Now, we have to associate this model with the view, we can simply bind them together through the
DataContext property that is a special property that will flow through all elements of the visual tree. We set it on the
MainWindow
constructor, it will be available to all controls.
- public partial class MainWindow : Window
- {
- public MainWindow()
- {
- InitializeComponent();
-
- this.DataContext = new Model();
- //this.DataContext = new ViewModel(new Model());
- }
- }
Now, run the app, if we type a name in the name box, such like
Henry, we will see the name in the BlockBox below,
Here, there is a problem that we bind the front end, the View, directly to the back end data, the Model, by MVC principle, the View and Model should be separated by a middle operational tier, the Controller, otherwise, the operational work will either be mixed in Model, which will break the rule that Model only holds the data and its operation, i.e., business rule; or be mixed in the View, which will also break the SoC rule.
Let us fix it by making a middle operational tier, ViewModel:
Step 5: Create a ViewModel
Now, we create a middle operational tier, ViewModel. Now, it will only include the proxy of the model, while later on it will include all operational action (not business related) in it, such as Synchronization by
INotifyPropertyChanged interface or Command Calling by
ICommand:
- public class ViewModel
- {
- private Model _model;
- public ViewModel(Model model)
- {
- _model = model;
- }
-
- public string CurrentName
- {
- get { return _model.CurrentName; }
- set
- {
- if (value == _model.CurrentName)
- return;
- _model.CurrentName = value;
- }
- }
-
- public ObservableCollection<string> AddedNames
- {
- get { return _model.AddedNames; }
- }
- }
Step 4-1: Modify DataContext to ViewModel
We re-bind the View to the ViewModel, instead of Model directly:
- public partial class MainWindow : Window
- {
- public MainWindow()
- {
- InitializeComponent();
-
- //this.DataContext = new Model();
- this.DataContext = new ViewModel(new Model());
-
- }
- }
Now, the result will the same as previously, however, the concept is totally different that we introduce a middle tier ViewModel, and Model and View are separated.
C-2: Make Synchronization between ViewModel and View
From Step 1, we have successfully separated View and Model to each other. At the same time, when we run the app, we found out the input name synchronously appears on the BlockBox below the Text Input Box. However, this synchronous action might not happen for all controls on the View, for example, the
Add Me button is not affected. We need to implement the
INotifyPropertyChanged or/and
INotifyCollectionChanged interfaces to make them happen.
Note
Step 6: Make a Synchronization
Let ViewModel derived from
INotifyPropertyChanged interface to enforce the notification to the View when ViewModel has any changes.
- public class ViewModel : INotifyPropertyChanged
- {
- private Model _model;
- public ViewModel(Model model)
- {
- _model = model;
- }
-
- public string CurrentName
- {
- get { return _model.CurrentName; }
- set
- {
- if (value == _model.CurrentName)
- return;
- _model.CurrentName = value;
- OnPropertyChanged();
- }
- }
-
- public ObservableCollection<string> AddedNames
- {
- get { return _model.AddedNames; }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
C-3: Make Command from View to Call back
Step 7: Make a Command:
Button
and other actionable items (such as
MenuItem
) work through an interface named
ICommand. One glaring omission of WPF is that it doesn't provide an out of the box, simple, model friendly
ICommand
implementation. One could refer here to the seminal
RelayCommand by Josh Smith... but since it's a very simple interface and I don't want to use any third party, we are just going to implement it inline.... Add this to the
Model
class:
- public ViewModel(Model model)
- {
- AddCommand = new AddNameCommand(this);
- }
-
- class AddNameCommand : ICommand
- {
- ViewModel parent;
-
- public AddNameCommand(ViewModel parent)
- {
- this.parent = parent;
- parent.PropertyChanged += delegate { CanExecuteChanged?.Invoke(this, EventArgs.Empty); };
- }
-
- public event EventHandler CanExecuteChanged;
-
- public bool CanExecute(object parameter) { return !string.IsNullOrEmpty(parent.CurrentName); }
-
- public void Execute(object parameter)
- {
- parent.AddedNames.Add(parent.CurrentName); ;
- parent.CurrentName = null;
- }
- }
-
- public ICommand AddCommand { get; private set; }
The final code for ViewModel will be like this
- using System;
- using System.Collections.ObjectModel;
- using System.ComponentModel;
- using System.Runtime.CompilerServices;
- using System.Windows.Input;
-
- namespace SimplestMVVM
- {
- public class ViewModel : INotifyPropertyChanged
- {
- private Model _model;
- public ViewModel(Model model)
- {
- AddCommand = new AddNameCommand(this);
- _model = model;
- }
-
- public string CurrentName
- {
- get { return _model.CurrentName; }
- set
- {
- if (value == _model.CurrentName)
- return;
- _model.CurrentName = value;
- OnPropertyChanged();
- }
- }
-
- public ObservableCollection<string> AddedNames
- {
- get { return _model.AddedNames; }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- void OnPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- class AddNameCommand : ICommand
- {
- ViewModel parent;
-
- public AddNameCommand(ViewModel parent)
- {
- this.parent = parent;
- parent.PropertyChanged += delegate { CanExecuteChanged?.Invoke(this, EventArgs.Empty); };
- }
-
- public event EventHandler CanExecuteChanged;
-
- public bool CanExecute(object parameter) { return !string.IsNullOrEmpty(parent.CurrentName); }
-
- public void Execute(object parameter)
- {
- parent.AddedNames.Add(parent.CurrentName); ;
- parent.CurrentName = null;
- }
- }
-
- public ICommand AddCommand { get; private set; }
- }
- }