3
\$\begingroup\$

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?

\$\endgroup\$
  • 2
    \$\begingroup\$ Great first question! \$\endgroup\$ – Reinderien Jul 6 at 21:39
  • 1
    \$\begingroup\$ I have a couple of doubts. The first one is that it's strange to have both IViewModel and ViewModelBase, 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\$ – Federico Rossi Jul 11 at 13:31
  • 1
    \$\begingroup\$ The second problem is that you initialize two viewModel for each view. The first one get created as parameter to the Messenger (and stored inside the ApplicationViewModel), meanwhile the second one get created by the view (and stored as its DataContext). 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\$ – Federico Rossi Jul 11 at 13:33
  • \$\begingroup\$ Thank you! I actually forgot that I was declaring the DataContext in the code behind instead of in the XAML, so I think that will be worth the fix. With your comments in mind, I will try to write a more thoughtful MVVM design (specifically figuring out how to make the IViewModel and ViewModelBase system more intuitive). \$\endgroup\$ – EricNie Jul 12 at 5:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.