I have been learning MVVM and I have decided to create a small framework for simple MVVM programs I can make in the future. This program provides navigation between a main menu, settings menu, start menu, and exit button. There is no functionality yet, and this is because I want to work on the navigation framework before I implement any models. Mainly, I tend to get confused on the relationships between view and viewmodel AND viewmodel to viewmodel communication. Here is what I got.
Bases - Helper Files
DelegateCommand.cs - ICommand Implementation
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MVVMPractice.Bases.ViewModel
{
/// <summary>
/// ICommand Implementation.
/// </summary>
class DelegateCommand : ICommand
{
private readonly Action<object> _executeAction;
private readonly Func<object, bool> _canExecuteAction;
/// <summary>
/// Delecate with execution action and canExecute bool
/// </summary>
/// <param name="executeAction">Function that gets executed on command</param>
/// <param name="canExecuteAction">Function that determines if the command can be executed</param>
public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteAction)
{
_executeAction = executeAction;
_canExecuteAction = canExecuteAction;
}
/// <summary>
/// Execute
/// </summary>
/// <param name="parameter"></param>
public void Execute(object parameter) => _executeAction(parameter);
/// <summary>
/// CanExecute
/// </summary>
/// <param name="parameter"></param>
/// <returns></returns>
public bool CanExecute(object parameter) => _canExecuteAction?.Invoke(parameter) ?? true;
public event EventHandler CanExecuteChanged;
/// <summary>
/// Determine if the command can be executed
/// </summary>
public void InvokeCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
IViewModel.cs - Used by all menu viewmodels
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMPractice.Bases.ViewModel
{
/// <summary>
/// Interfaces for main ViewModels
/// </summary>
interface IViewModel
{
/// <summary>
/// Get name string
/// </summary>
string Name { get; }
}
}
Messenger.cs - Viewmodel to Viewmodel Communication
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMPractice.Bases.ViewModel
{
/// <summary>
/// Messenger takes and gives an object as a message for viewmodel communication
/// </summary>
public class Messenger
{
//Instance
private static Messenger _instance;
public static Messenger Instance => _instance ?? (_instance = new Messenger());
//declaring EventHandler
public event EventHandler<MessageValueChangedEventArgs> MessageValueChanged;
//Custom Event args
public class MessageValueChangedEventArgs : EventArgs
{
public string PropertyName { get; set; }
public object NewValue { get; set; }
}
//raising the event for a property
public void RaiseMessageValueChanged(string propertyName, object value)
{
MessageValueChanged?.Invoke(this, new MessageValueChangedEventArgs() { PropertyName = propertyName, NewValue = value });
}
}
}
ViewModelBase.cs - All ViewModels inherit this class
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMPractice.Bases.ViewModel
{
/// <summary>
/// All viewmodels inherit ViewModelBase
/// </summary>
class ViewModelBase : INotifyPropertyChanged
{
//Declare Messenger
protected Messenger _messenger;
//Declare PropertyChanged event handler
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Invokes PropertyChanged when a property is changed. Using INotifyPropertyChanged
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="field"></param>
/// <param name="newValue"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
protected bool SetProptery<T>(ref T field, T newValue, [CallerMemberName]string propertyName = null)
{
if(!EqualityComparer<T>.Default.Equals(field, newValue))
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}
}
}
Application Files - Main window used for navigation
ApplicationViewModel.cs
using MVVMPractice.Bases.ViewModel;
using MVVMPractice.Main;
using MVVMPractice.Main.Settings;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MVVMPractice
{
/// <summary>
/// Application View Model for ApplicationView
/// </summary>
class ApplicationViewModel : ViewModelBase
{
/// <summary>
/// IViewModel Interface: Variable stores current IViewModel
/// </summary>
private IViewModel _currentViewModel;
/// <summary>
/// ApplicationViewModel Constructor
/// </summary>
public ApplicationViewModel()
{
_messenger = Messenger.Instance;
_messenger.MessageValueChanged += OnMessengerValueChanged;
MainViewModel mainViewModel = new MainViewModel();
SettingsViewModel settingsViewModel = new SettingsViewModel();
//debug line for testing new pages
//CurrentViewModel = settingsViewModel;
CurrentViewModel = mainViewModel;
}
/// <summary>
/// Get / Set IViewModel CurrentViewModel
/// </summary>
public IViewModel CurrentViewModel
{
get
{
return _currentViewModel;
}
set
{
SetProptery(ref _currentViewModel, value);
}
}
/// <summary>
/// Notify Change for Messenger Value. Changes CurrentViewModel if property string is "CurrentViewModel"
/// </summary>
/// <param name="sender">Object sender</param>
/// <param name="e">Event Args for Messenger</param>
private void OnMessengerValueChanged(object sender, Messenger.MessageValueChangedEventArgs e)
{
if (e.PropertyName == "CurrentViewModel")
{
CurrentViewModel = (IViewModel)e.NewValue;
}
}
}
}
ApplicationView.xaml
<Window x:Class="MVVMPractice.ApplicationView"
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:local="clr-namespace:MVVMPractice" xmlns:main="clr-namespace:MVVMPractice.Main" xmlns:settings="clr-namespace:MVVMPractice.Main.Settings" xmlns:start="clr-namespace:MVVMPractice.Main.Start"
Title="ApplicationView" Height="450" Width="800">
<Window.Resources>
<DataTemplate DataType="{x:Type main:MainViewModel}">
<main:MainView/>
</DataTemplate>
<DataTemplate DataType="{x:Type settings:SettingsViewModel}">
<settings:SettingsView/>
</DataTemplate>
<DataTemplate DataType="{x:Type start:StartViewModel}">
<start:StartView/>
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentViewModel}"/>
</Window>
ApplicationView.xaml.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace MVVMPractice
{
/// <summary>
/// Interaction logic for ApplicationView.xaml
/// </summary>
public partial class ApplicationView : Window
{
public ApplicationView()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
}
}
}
Main - Default User Control
MainViewModel.cs
using MVVMPractice.Bases.ViewModel;
using MVVMPractice.Main.Settings;
using MVVMPractice.Main.Start;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace MVVMPractice.Main
{
/// <summary>
/// ViewModel for MainMenu
/// </summary>
class MainViewModel : ViewModelBase, IViewModel
{
/// <summary>
/// IViewModel name
/// </summary>
public string Name
{
get { return "MainMenu"; }
}
/// <summary>
/// Constructor for MainViewModel
/// </summary>
public MainViewModel()
{
_messenger = Messenger.Instance;
_exitCommand = new DelegateCommand(OnExitCommand, CanExitCommand);
_settingsCommand = new DelegateCommand(OnSettingsCommand, CanSettingsCommand);
_startCommand = new DelegateCommand(OnStartCommand, CanStartCommand);
}
#region Start Command
//Declare DelegateCommand
private readonly DelegateCommand _startCommand;
//Declare ICommand
public ICommand StartCommand => _startCommand;
/// <summary>
/// CanExecute, returns true
/// </summary>
/// <param name="parameter"></param>
/// <returns>True</returns>
private bool CanStartCommand(object parameter)
{
return true;
}
/// <summary>
/// OnExecute, submits StartViewModel to messenger
/// </summary>
/// <param name="parameter"></param>
public void OnStartCommand(object parameter)
{
_messenger.RaiseMessageValueChanged("CurrentViewModel", new StartViewModel());
}
#endregion
#region Settings Command
//Declare DelegateCommand
private readonly DelegateCommand _settingsCommand;
//Declare ICommand
public ICommand SettingsCommand => _settingsCommand;
/// <summary>
/// CanExecute, returns true
/// </summary>
/// <param name="parameter"></param>
/// <returns>True</returns>
private bool CanSettingsCommand(object parameter)
{
return true;
}
/// <summary>
/// OnExecute, submits SettingsViewModel to messenger
/// </summary>
/// <param name="parameter"></param>
private void OnSettingsCommand(object parameter)
{
_messenger.RaiseMessageValueChanged("CurrentViewModel", new SettingsViewModel());
}
#endregion
#region Exit Command
//Declare DelegateCommand
private readonly DelegateCommand _exitCommand;
//Declare ICommand
public ICommand ExitCommand => _exitCommand;
/// <summary>
/// CanExecute, returns true
/// </summary>
/// <param name="parameter"></param>
/// <returns>True</returns>
private bool CanExitCommand(object parameter)
{
return true;
}
/// <summary>
/// OnExecute, shuts down the program
/// </summary>
/// <param name="Parameter"></param>
private void OnExitCommand(object Parameter)
{
App.Current.Shutdown();
}
#endregion
}
MainView.xaml
<UserControl x:Class="MVVMPractice.Main.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MVVMPractice.Main.Settings"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate DataType="{x:Type local:SettingsViewModel}">
<local:SettingsView/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Button Command="{Binding StartCommand}" Content="Start" HorizontalAlignment="Left" Margin="145,97,0,0" VerticalAlignment="Top" Width="75"/>
<Button Command="{Binding SettingsCommand}" Content="Settings" HorizontalAlignment="Left" Margin="145,116,0,0" VerticalAlignment="Top" Width="75"/>
<Button Command="{Binding ExitCommand}" Content="Exit" HorizontalAlignment="Left" Margin="145,135,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</UserControl>
MainView.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMPractice.Main
{
/// <summary>
/// Interaction logic for MainView.xaml
/// </summary>
public partial class MainView : UserControl
{
public MainView()
{
var viewModel = new MainViewModel();
DataContext = viewModel;
InitializeComponent();
}
}
}
Start Menu StartViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MVVMPractice.Main.Start
{
/// <summary>
/// ViewModel for Start
/// </summary>
class StartViewModel : ViewModelBase, IViewModel
{
/// <summary>
/// IViewModel name
/// </summary>
public string Name
{
get { return "Start"; }
}
/// <summary>
/// Constructor for StartViewModel
/// </summary>
public StartViewModel()
{
_messenger = Messenger.Instance;
_backCommand = new DelegateCommand(OnBackCommand, CanBackCommand);
}
#region BackCommand
//Declare DelegateCommand
private readonly DelegateCommand _backCommand;
//Delcare ICommand
public ICommand BackCommand => _backCommand;
/// <summary>
/// CanExecute, returns true
/// </summary>
/// <param name="parameter"></param>
/// <returns>True</returns>
private bool CanBackCommand(object parameter)
{
return true;
}
/// <summary>
/// OnExecute, Submits MainViewModel to messenger
/// </summary>
/// <param name="parameter"></param>
private void OnBackCommand(object parameter)
{
_messenger.RaiseMessageValueChanged("CurrentViewModel", new MainViewModel());
}
#endregion
}
}
StartView.xaml
<UserControl x:Class="MVVMPractice.Main.Start.StartView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MVVMPractice.Main.Start"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Label Content="InStart" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="387" Width="780" Background="#FFCD4646"/>
<Button Command="{Binding BackCommand}" Content="Back" HorizontalAlignment="Left" Margin="10,420,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</UserControl>
StartView.xaml.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMPractice.Main.Start
{
/// <summary>
/// Interaction logic for StartView.xaml
/// </summary>
public partial class StartView : UserControl
{
public StartView()
{
StartViewModel viewModel = new StartViewModel();
DataContext = viewModel;
InitializeComponent();
}
}
}
Settings menu
SettingsViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MVVMPractice.Main.Settings
{
/// <summary>
/// ViewModel for Settings Menu
/// </summary>
class SettingsViewModel : ViewModelBase, IViewModel
{
/// <summary>
/// IViewModel name
/// </summary>
public string Name
{
get { return "SettingsMenu"; }
}
/// <summary>
/// Constructor for SettingsViewModel
/// </summary>
public SettingsViewModel()
{
_messenger = Messenger.Instance;
_backCommand = new DelegateCommand(OnBackCommand, CanBackCommand);
}
#region BackCommand
//Create DelegateCommand
private readonly DelegateCommand _backCommand;
//Create ICommand
public ICommand BackCommand => _backCommand;
/// <summary>
/// CanExecute. Returns True
/// </summary>
/// <param name="parameter"></param>
/// <returns>True</returns>
private bool CanBackCommand(object parameter)
{
return true;
}
/// <summary>
/// OnExecute. Submits a MainViewModel to messenger
/// </summary>
/// <param name="parameter"></param>
private void OnBackCommand(object parameter)
{
_messenger.RaiseMessageValueChanged("CurrentViewModel", new MainViewModel());
}
#endregion
}
}
SettingsView.xaml
<UserControl x:Class="MVVMPractice.Main.Settings.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MVVMPractice.Main.Settings"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Label Content="InSettings" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="405" Width="780" Background="#FFED6A6A"/>
<Button Command="{Binding BackCommand}" Content="Back" HorizontalAlignment="Left" Margin="10,420,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</UserControl>
SettingsView.xaml.cs
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMPractice.Main.Settings
{
/// <summary>
/// Interaction logic for SettingsView.xaml
/// </summary>
public partial class SettingsView : UserControl
{
public SettingsView()
{
SettingsViewModel viewModel = new SettingsViewModel();
DataContext = viewModel;
InitializeComponent();
}
}
}
This works out fine, but I want to know how I can improve before I start moving forward, or am I even taking steps in the correct direction?
IViewModelandViewModelBase, even though I've understood that you had to differentiate between the pages (Main, Start and Settings) and the container window (Application). It works fine but could be confusing for someone coming to to your project as newcomer. \$\endgroup\$ApplicationViewModel), meanwhile the second one get created by the view (and stored as itsDataContext). I think this could lead to a multitude of problems. To solve that it should be as easy as set<ContentControl Content="{Binding CurrentViewModel}" DataContext="{Binding CurrentViewModel}"/>and removing the viewModel creation from the view constructors. I didn't test this, so take it as an idea. These were my thoughts. Good work overall \$\endgroup\$