I have been learning MVVM concepts and trying to implement it in my new project. I want to validate my work that I have been doing these past days. I want to know if I follow correctly the MVVM pattern. I understand that everyone has their way of working/thinking, but as I mention earlier what I need to know is, am I respecting the CORE of MVVM
What I want to know:
- Do I follow the MVVM pattern?
- Is there a better / alternative approach that I could use (following the MVVM pattern, of course)?
After learning, I came to the conclusion that I need some Helpers class, so I've created some.
The clViewModelBase
class which implements the INotifyPropertyChanged
interface, will help me to notify if any of my properties has changed. I did this so I don't have to implement the interface on each and every single View Model:
using System.ComponentModel;
class clViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
}
The clDelegateCommand
class which implements the ICommand
interface, which will help me to fire up the events on the button:
using System;
using System.Windows.Input;
class clDelegateCommand : ICommand
{
private Action<object> _executionAction;
private Predicate<object> _canExecutePredicate;
public clDelegateCommand(Action<object> execute)
: this(execute, null)
{ }
public clDelegateCommand(Action<object> execute, Predicate<object> canExecute)
{
if (execute == null)
{
throw new ArgumentNullException("execute");
}
this._executionAction = execute;
this._canExecutePredicate = canExecute;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public bool CanExecute(object parameter)
{
return this._canExecutePredicate == null ? true : this._canExecutePredicate(parameter);
}
public void Execute(object parameter)
{
if (!this.CanExecute(parameter))
{
throw new InvalidOperationException("The command is not valid for execution, check the CanExecute method before attempting to execute.");
}
this._executionAction(parameter);
}
}
The clConverter
which implements the IMultiValueConverter
interface, will update my values only after the user has clicked on Save. This class returns an array of the updated values.
using System;
using System.Windows.Data;
using System.Collections;
using System.Linq;
class clConverter : IMultiValueConverter
{
public clConverter() { }
#region IMultiValueConverter Methods
public object Convert(object[] values, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return values.ToList();
}
public object[] ConvertBack(object value, System.Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
//throw new System.NotImplementedException();
return ((IEnumerable)value).Cast<object>().Select(x => x.ToString()).ToArray();
}
#endregion
}
the clBaseAddEditDeleteViewModel which derives the clViewModelBase, this is to control my form display
class clBaseAddEditDeleteViewModel : clViewModelBase
{
#region Variables
private bool _IsListEnabled;
private bool _IsDetailEnabled;
#endregion
#region Properties
public bool IsListEnabled
{
get { return this._IsListEnabled; }
set
{
this._IsListEnabled = value;
OnPropertyChanged("IsListEnabled");
}
}
public bool IsDetailEnabled
{
get { return this._IsDetailEnabled; }
set
{
this._IsDetailEnabled = value;
OnPropertyChanged("IsDetailEnabled");
}
}
#endregion
public void SetNormalUIDisplay()
{
this.IsListEnabled = true;
this.IsAddMode = false;
this.IsDetailEnabled = false;
}
public void SetEditUIDisplay()
{
this.IsAddMode = true;
this.IsDetailEnabled = true;
this.IsListEnabled = false;
}
}
Here's my Model that obviously derives the clViewModelBase
and also the IDataErrorInfo
interface to manage my errors, ID cannot be empty etc...
class clPart : clViewModelBase, IDataErrorInfo
{
#region Variables
private string _ID;
#endregion
#region Properties
/// <summary>
/// Gets or Set the ID of this part
/// </summary>
public virtual string ID
{
get { return this._ID; }
set
{
this._ID = value;
OnPropertyChanged("ID");
}
}
#endregion
#region IDataErrorInfo Members
string IDataErrorInfo.Error
{
get { return null; }
}
string IDataErrorInfo.this[string propertyName]
{
get
{
return GetValidationError(propertyName);
}
}
#endregion
#region Validation
/// <summary>
/// All the properties that has to be validated
/// </summary>
static readonly string[] ValidatedProperties =
{
"ID"
};
public bool IsValid
{
get
{
foreach (string property in ValidatedProperties)
if (GetValidationError(property) != null)
return false;
return true;
}
}
private string GetValidationError(String propertyName)
{
string error = null;
switch (propertyName)
{
case "ID":
error = ValidatePartID();
break;
}
return error;
}
private string ValidatePartID()
{
if (String.IsNullOrWhiteSpace(this.ID))
{
return "The Part ID cannot be empty.";
}
return null;
}
#endregion
}
My view consist of 2 forms: my MainWindow
and the User Control. MainWindow
contains 2 sections:
- Left section that has my ListBox, for each part I will add a user control for it
- Right section that has all my details displayed
Main Window XAML:
<Window x:Class="Mvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Helpers="clr-namespace:Mvvm.ViewModel.Helpers"
xmlns:vm="clr-namespace:Mvvm.View"
Title="MainWindow" Height="483" Width="922.725" WindowStartupLocation="CenterScreen">
<Window.Resources>
<Helpers:clConverter x:Key="PartConverter" />
</Window.Resources>
<Grid x:Name="grdMain" RenderTransformOrigin="0.449,0.707">
<Label x:Name="lblPartID" Content="Part ID" HorizontalAlignment="Left" Margin="382,86,0,0" VerticalAlignment="Top" Width="109"/>
<TextBox x:Name="txtPartID"
IsEnabled="{Binding IsDetailEnabled}"
Text="{Binding Path=CurrentSelectedPart.ID, Mode=OneWay, ValidatesOnDataErrors=True}"
HorizontalAlignment="Left" Height="23" Margin="498,89,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="195"/>
<ToolBar x:Name="tbSubMenu" HorizontalAlignment="Left" Margin="367,35,0,0" VerticalAlignment="Top" Width="548" Height="28">
<Button x:Name="btnSaveDetails" IsEnabled="{Binding Path=IsDetailEnabled}" Command="{Binding SaveCommand}">
<Image Source="Images/Save_6530.ico"/>
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource PartConverter}" Mode="TwoWay">
<Binding ElementName="txtPartID" Path="Text"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
<Separator/>
<Button x:Name="btnEditDetail" Command="{Binding EditCommand}">
<Image Source="Images/PencilTool_206.png"/>
</Button>
<Separator/>
<Button x:Name="btnCancelDetail" IsEnabled="{Binding Path=IsDetailEnabled}" Command="{Binding CancelCommand}">
<Image Source="Images/Cancel(build)_194_32.bmp"/>
</Button>
</ToolBar>
<ListBox Name="listParts" HorizontalAlignment="Left" Height="402" Margin="10,40,0,0" IsEnabled="{Binding Path=IsListEnabled}"
VerticalAlignment="Top" Width="352" SelectedItem="{Binding Path=CurrentSelectedPart}"
ItemsSource="{Binding Path=CollectionPart}" >
<ListBox.ItemTemplate>
<DataTemplate>
<vm:ucPart />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>
My MainWindow
:
base.DataContext = new clPartViewModel();
My ViewModel
:
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System.Collections;
using System.Linq;
using System.Collections.Generic;
using Mvvm.Model;
using Mvvm.ViewModel.Helpers;
/// <summary>
///
/// </summary>
class clPartViewModel : clBaseAddEditDeleteViewModel
{
#region Variables
private ObservableCollection<clPart> _CollectionPart;
private clPart _CurrentSelectedPart;
private clDelegateCommand _EditCommand;
private clDelegateCommand _SaveCommand;
private clDelegateCommand _CancelCommand;
#endregion
#region Properties
public ObservableCollection<clPart> CollectionPart
{
get { return this._CollectionPart; }
set
{
if (this._CollectionPart == value)
return;
this._CollectionPart = value;
OnPropertyChanged("CollectionPart");
}
}
public clPart CurrentSelectedPart
{
get { return this._CurrentSelectedPart; }
set
{
if (this._CurrentSelectedPart == value)
return;
this._CurrentSelectedPart = value;
OnPropertyChanged("CurrentSelectedPart");
}
}
public ICommand EditCommand
{
get
{
if (this._EditCommand == null)
this._EditCommand = new clDelegateCommand(new Action<object>(EditPart), null);
return this._EditCommand;
}
}
public ICommand CancelCommand
{
get
{
if (this._CancelCommand == null)
this._CancelCommand = new clDelegateCommand(new Action<object>(CancelPart), null);
return this._CancelCommand;
}
}
public ICommand SaveCommand
{
get
{
if (this._SaveCommand == null)
this._SaveCommand = new clDelegateCommand(new Action<object>(SaveChanges), null);
return this._SaveCommand;
}
}
#endregion
/// <summary>
/// Cunstructor of the clPartViewModel class
/// </summary>
public clPartViewModel()
{
this.CollectionPart = new ObservableCollection<clPart>
{
new clPart { ID = "[0000001]" },
new clPart { ID = "[0000002]" },
new clPart { ID = "[0000003]" },
new clPart { ID = "[0000004]" },
new clPart { ID = "[0000005]" }
};
this.SetNormalUIDisplay();
}
private void EditPart(object obj)
{
this.SetEditUIDisplay();
}
private void CancelPart(object obj)
{
// TODO : revert changes
this.SetNormalUIDisplay();
}
private void SearchByPartID(string ID)
{
this.CollectionPart.Where(x => x.ID == ID);
}
private void SaveChanges(object obj)
{
string[] Part = ((IEnumerable)obj).Cast<object>().Select(x => x.ToString()).ToArray();
if (IsPartIdDuplicate(Convert.ToString(Part[0])))
return;
this._CurrentSelectedPart.ID = Convert.ToString(Part[0]);
this.SetNormalUIDisplay();
}
/// <summary>
/// Validate if we have duplicates ID's
/// </summary>
/// <returns>True, if we have duplicates</returns>
public bool IsPartIdDuplicate(string IDToValidate)
{
foreach (clPart Parts in this.CollectionPart)
{
if (Parts.ID == IDToValidate && Parts.ID != this.CurrentSelectedPart.ID)
return true;
}
return false;
}
}
My user control view XAML to load in my ListBox
:
<UserControl x:Class="Mvvm.View.ucPart"
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"
mc:Ignorable="d"
d:DesignHeight="60" Width="230">
<Grid Name="DetailsGruid" HorizontalAlignment="Left" VerticalAlignment="Center" Width="230">
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="75" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<Border Grid.RowSpan="2" Grid.ColumnSpan="3" BorderBrush="DarkRed"
BorderThickness="1,1,1,1" CornerRadius="8,8,8,8" />
<Image Name="ImgPart" Grid.RowSpan="2" Grid.Column="0" Margin="5" Source="/Mvvm;component/Images/Save_6530.ico" />
<TextBox Grid.Row="0" Grid.Column="1" Name="txtID" Width="Auto" Margin="5"
HorizontalAlignment="Stretch" VerticalAlignment="Center" IsEnabled="False"
Text="{Binding Path=ID}" />
</Grid>