How to use the MVVM Pattern correctly?

Sarah 186 Reputation points
2022-03-06T16:25:40.303+00:00

I have spent the last weeks and months trying to understand the MVVM pattern better. But apparently I still have difficulties to apply the theory in practice. I hope with the following app I will understand the pattern forever.
I want to use the pattern correctly, I have created a ViewModel for each view. Unfortunately I don't know how to realize some things in MVVM. Therefore I have commented them out in the code.
Maybe someone can show me how this works?
I need to see the clean separation between Model, View and ViewModel. Therefore I use clear naming conventions (View, ViewModel and Model).
A big request, no new approach for the interface "IDataErrorInfo". It is the basis for a panned extension of the app.

Thanks

MainWindow

<Window x:Class="cManagement.MainWindow"
        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:cManagement.ViewModels" 
        mc:Ignorable="d"
        Title="cManagement"
               Height="530" MinHeight="530" Width="725" MinWidth="725" 
               WindowStartupLocation="CenterScreen">



    <Window.DataContext>
        <local:ContactListingViewModel/>
    </Window.DataContext>

    <Grid Margin="10" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBox Grid.Row="0" Grid.Column="0" Height="25" Text="{Binding FilterContact, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding ErrorCollection[FilterContact]}"/>

        <ListView x:Name="ContactListView" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding View}"  ScrollViewer.HorizontalScrollBarVisibility="Disabled" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding Contact}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
                        <TextBlock Width="200" Text="{Binding Firstname}" TextWrapping="Wrap" />
                        <TextBlock Width="200" Text="{Binding Lastname}" TextWrapping="Wrap" />
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Vertical" VerticalAlignment="Top" HorizontalAlignment="Center" >
            <Button Height="30" Width="80" Content="Add" Margin="0,5,0,10" Command="{Binding Cmd}" CommandParameter="Add"/>
            <Button Height="30" Width="80" Content="Edit" Margin="10" Command="{Binding Cmd}" CommandParameter="Edit"/>
            <Button Height="30" Width="80" Content="Delete" Margin="10" Command="{Binding Cmd}" CommandParameter="Delete"/>
        </StackPanel>

    </Grid>
</Window>

AddContactView

<Window x:Class="cManagement.Views.AddContactView"
        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:cManagement.Views"
        xmlns:Sys="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="AddContact"
               Height="200" 
               Width="450" 
               WindowStartupLocation="CenterOwner">


    <Grid Margin="0,10,0,0">
        <StackPanel DataContext="{Binding Contact}">

            <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" >
                <Label Content="Firstname:"  FontWeight="SemiBold"/>
                <TextBox Height="25" Width="290" MaxLength="91" Margin="0,0,5,0" Text="{Binding Firstname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding ErrorCollection[Firstname]}" HorizontalContentAlignment="left" VerticalContentAlignment="Center"/>
                <Label Content="Lastname:" FontWeight="SemiBold"/>
                <TextBox Height="25" Width="290" MaxLength="91" Margin="0,0,5,0" Text="{Binding Lastname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" ToolTip="{Binding ErrorCollection[Lastname]}" HorizontalContentAlignment="left" VerticalContentAlignment="Center"/>
            </StackPanel>


        </StackPanel>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10,0,10,10">
            <Button Height="30" Width="80" Content="Cancel" Margin="10,0,0,0" Command="{Binding CmdCancel}"/>
            <Button Height="30" Width="80" Content="Save" Margin="10,0,5,0" Command="{Binding CmdSave}"/>
        </StackPanel>
    </Grid>
</Window>

AddContactViewModel.cs

using cManagement.Commands;
using System;
using System.Collections.Generic;

namespace cManagement.ViewModels
{
    class AddContactViewModel : BaseViewModel, IDataErrorInfo
    {
        private ContactListingViewModel contactListingViewModel;

        public DelegateCommand CmdSave { get => new DelegateCommand(CanCreateContact, CmdExecSave); }

        public void CmdExecSave(Object parameter)
        {
            //addContactWindow.DialogResult = true;
        }



        public DelegateCommand CmdCancel { get => new DelegateCommand(CmdExecCancel); }
        public void CmdExecCancel(Object parameter)
        {
            //addContactWindow.DialogResult = false;
        }

        public bool CanCreateContact =>
            HasFirstname &&
            HasLastname &&
            Contact != null;

        private bool HasFirstname => !string.IsNullOrEmpty(Firstname);
        private bool HasLastname => !string.IsNullOrEmpty(Lastname);

        #region Propertyname
        public string firstname;
        public string Firstname
        {
            get { return firstname; }
            set
            {
                firstname = value;
                onPropertyChanged();
                onPropertyChanged(nameof(CanCreateContact));
            }
        }

        public string lastname;
        public string Lastname
        {
            get { return lastname; }
            set
            {
                lastname = value;
                onPropertyChanged();
                onPropertyChanged(nameof(CanCreateContact));
            }
        }
        #endregion Propertyname

        #region ErrorInfo
        public Dictionary<string, string> ErrorCollection { get; private set; } = new Dictionary<string, string>();
        public string Error { get => String.Empty; }
        public string this[string propertyName]
        {
            get
            {
                string result = null;

                switch (propertyName)
                {
                    case "Firstname":
                        if (string.IsNullOrWhiteSpace(Firstname))
                            result = "Firstname cannot be empty";

                        else if (Firstname.Length < 5)
                            result = "Firstname must be a minimum of 5 characters.";

                        break;

                    case "Lastname":
                        if (string.IsNullOrWhiteSpace(Lastname))
                            result = "Lastname cannot be empty";

                        else if (Firstname.Length < 5)
                            result = "Lastname must be a minimum of 5 characters.";

                        break;
                }

                if (ErrorCollection.ContainsKey(propertyName))
                {
                    ErrorCollection[propertyName] = result;
                }
                else if (result != null)
                {
                    ErrorCollection.Add(propertyName, result);
                }

                onPropertyChanged("ErrorCollection");

                return result;
            }
        }
        #endregion ErrorInfo
    }
}

ContactListingViewModel .cs

using cManagement.Commands;
using cManagement.Models;
using cManagement.Services;
using cManagement.Views;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;

namespace cManagement.ViewModels
{
    public class ContactListingViewModel : BaseViewModel, IDataErrorInfo
    {
        AppDbContext db = new AppDbContext();

        public ContactListingViewModel()
        {
            db.tblContact.Load();
            cvs.Source = db.tblContact.Local.ToObservableCollection();
            cvs.Source = ContactsList;
        }

        public ICollectionView View { get => cvs.View; }
        private CollectionViewSource cvs = new CollectionViewSource();
        public ObservableCollection<ContactModel> ContactsList { get; set; } = new ObservableCollection<ContactModel>();

        public ContactModel Contact { get; set; } = new ContactModel();

        #region Commands

        public DelegateCommand Cmd { get => new DelegateCommand(CanExec , CmdExec); }

        private AddContactView addContactView;
        private bool? diagResult;

        private void CmdExec(object parameter)
        {
            switch (parameter.ToString())
            {
                case "Add":
                    addContactView = new AddContactView();
                    AddContactViewModel vm = new AddContactViewModel();
                    addContactView.DataContext = vm;
                    diagResult = addContactView.ShowDialog();
                    if(diagResult.HasValue && diagResult.Value)
                    {
                        db.tblContact.Add(Contact);
                        db.SaveChanges();
                    }
                    break;
                case "Edit":
                    if (Contact != null)
                    {
                        addContactView = new AddContactView() { DataContext = this };
                        diagResult = addContactView.ShowDialog();
                        if (diagResult.HasValue && diagResult.Value)
                        {
                            db.SaveChanges();
                        }
                        else
                        {
                            db.Entry(Contact).State = EntityState.Unchanged;
                            onPropertyChanged(nameof(View));
                        }
                    }
                    break;
                case "Delete":
                    if (Contact != null)
                    {
                        db.tblContact.Remove(Contact);
                        db.SaveChanges();
                    }
                    break;
                default:
                    break;
            }
        }

        private bool CanExec(object parameter)
        {
            bool res = false;
            switch (parameter.ToString())
            {
                case "Add":
                    res = true;
                    break;
                case "Edit":
                    res = (Contact != null);
                    break;
                case "Delete":
                    res = (Contact != null);
                    break;
                default:
                    break;
            }
            return res;
        }

        public bool CanCreateContact => HasContact;

        private bool HasContact => (Contact != null);

        #endregion Commands

        #region Filter
        private string filterContact;
        public string FilterContact
        {
            get
            {
                return filterContact;
            }

            set
            {
                filterContact = value;
                onPropertyChanged();
                cvs.View.Refresh();
            }
        }

        private void ContactFilter(object sender, FilterEventArgs e)
        {
            if (string.IsNullOrWhiteSpace(filterContact))
            {
                //no filter when no search text is entered
                e.Accepted = true;
            }
            else
            {
                ContactModel contact = (ContactModel)e.Item;
                if (contact.Firstname.StartsWith(filterContact, true, null) || 
                    contact.Firstname.Contains(filterContact, StringComparison.InvariantCultureIgnoreCase) ||
                    contact.Lastname.StartsWith(filterContact, true, null) ||
                    contact.Lastname.Contains(filterContact, StringComparison.InvariantCultureIgnoreCase)
                    )
                {
                    e.Accepted = true;
                }
                else
                {
                    e.Accepted = false;
                }
            }
        }
        #endregion Filter

        #region ErrorInfo
        public Dictionary<string, string> ErrorCollection { get; private set; } = new Dictionary<string, string>();
        public string Error { get => String.Empty; }
        public string this[string propertyName]
        {
            get
            {
                string result = null;

                switch (propertyName)
                {
                    case "FilterContact":
                        if (string.IsNullOrWhiteSpace(Contact.Firstname))
                            result = "Please enter firstname or lastname";

                        else if (Contact.Firstname.Length < 5)
                            result = "Firstname must be a minimum of 5 characters.";

                        break;
                }

                if (ErrorCollection.ContainsKey(propertyName))
                {
                    ErrorCollection[propertyName] = result;
                }
                else if (result != null)
                {
                    ErrorCollection.Add(propertyName, result);
                }

                onPropertyChanged("ErrorCollection");

                return result;
            }
        }
        #endregion ErrorInfo

    }
}

BaseViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace cManagement.ViewModels
{
    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void onPropertyChanged([CallerMemberName] string propertyName = "")
        {
            if (!string.IsNullOrEmpty(propertyName))
                this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

ContactModel.cs

namespace cManagement.Models
{
    public class ContactModel
    {
        public int Id { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
    }
}

DelegateCommand.cs

using System;
using System.Windows.Input;

namespace cManagement.Commands
{
    public class DelegateCommand : ICommand
    {
        readonly Action<object> execute;

        readonly Predicate<object> canExecute;

        public DelegateCommand(Predicate<object> canExecute, Action<object> execute) =>
            (this.canExecute, this.execute) = (canExecute, execute);

        public DelegateCommand(Action<object> execute) : this(null, execute) { }

        public void onCanExecuteChanged() => this.CanExecuteChanged?.Invoke(this, EventArgs.Empty);

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter) => this.canExecute?.Invoke(parameter)?? true; 

        public void Execute(object parameter) => this.execute?.Invoke(parameter);
    }
}
Developer technologies Windows Presentation Foundation
{count} votes

1 answer

Sort by: Most helpful
  1. Peter Fleischer (former MVP) 19,341 Reputation points
    2022-03-09T13:28:38.253+00:00

    Hi Sarah,
    I correct and change your code (without Filter):

    <Window x:Class="WpfApp007.MainWindow"
            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:WpfApp007"
            mc:Ignorable="d"
            Title="Sarah-3412_220306" Height="450" Width="800">
      <Window.DataContext>
        <local:ContactListingViewModel/>
      </Window.DataContext>
      <Grid Margin="10" >
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="100"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
          <RowDefinition Height="30"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <local:LimitedTextBox Grid.Row="0" Grid.Column="0" Height="25" Text="{Binding FilterContact, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
        <ListView x:Name="View" Grid.Row="1" Grid.Column="0" ItemsSource="{Binding View}"  ScrollViewer.HorizontalScrollBarVisibility="Disabled" IsSynchronizedWithCurrentItem="True" SelectedItem="{Binding Contact}">
          <ListView.ItemTemplate>
            <DataTemplate>
              <StackPanel Orientation="Vertical" HorizontalAlignment="Left">
                <TextBlock Width="200" Text="{Binding Firstname}" TextWrapping="Wrap" />
                <TextBlock Width="200" Text="{Binding Lastname}" TextWrapping="Wrap" />
              </StackPanel>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
        <StackPanel Grid.Row="1" Grid.Column="1" Orientation="Vertical" VerticalAlignment="Top" HorizontalAlignment="Center" >
          <Button Height="30" Width="80" Content="Add" Margin="0,5,0,10" Command="{Binding Cmd}" CommandParameter="Add"/>
          <Button Height="30" Width="80" Content="Edit" Margin="10" Command="{Binding Cmd}" CommandParameter="Edit"/>
          <Button Height="30" Width="80" Content="Delete" Margin="10" Command="{Binding Cmd}" CommandParameter="Delete"/>
        </StackPanel>
      </Grid>
    </Window>
    
    <Window x:Class="WpfApp007.Views.AddEditContactView"
            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:WpfApp007"
            mc:Ignorable="d"
            Title="AddEditContact"
            Height="200" 
            Width="450" 
            WindowStartupLocation="CenterOwner"
            local:AddEditContactViewModel.AttProp = "True">
      <Window.Resources>
        <!-- Error Template to change the default behaviour -->
        <ControlTemplate x:Key="ErrorTemplate">
          <DockPanel LastChildFill="True">
            <Border BorderBrush="Red" BorderThickness="1">
              <AdornedElementPlaceholder />
            </Border>
          </DockPanel>
        </ControlTemplate>
        <!-- To display tooltip with the error-->
        <Style TargetType="TextBox">
          <Style.Triggers>
            <Trigger Property="Validation.HasError" Value="true">
              <Setter Property="ToolTip"
               Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
            </Trigger>
          </Style.Triggers>
        </Style>
      </Window.Resources>
      <Grid Margin="0,10,0,0">
        <StackPanel DataContext="{Binding Contact}">
          <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" >
            <Label Content="Firstname:"  FontWeight="SemiBold"/>
            <TextBox Height="25" Width="290" MaxLength="91" Margin="0,0,5,0" Text="{Binding Firstname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Validation.ErrorTemplate="{StaticResource ErrorTemplate}" HorizontalContentAlignment="left" VerticalContentAlignment="Center"/>
            <Label Content="Lastname:" FontWeight="SemiBold"/>
            <TextBox Height="25" Width="290" MaxLength="91" Margin="0,0,5,0" Text="{Binding Lastname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" Validation.ErrorTemplate="{StaticResource ErrorTemplate}" HorizontalContentAlignment="left" VerticalContentAlignment="Center"/>
            <Label Content="{Binding Error}" Margin="5" Foreground="Red" />
          </StackPanel>
        </StackPanel>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Right" Margin="10,0,10,10">
          <Button Height="30" Width="80" Content="Cancel" Margin="10,0,0,0" Command="{Binding Cmd}" CommandParameter="Cancel"/>
          <Button Height="30" Width="80" Content="Save" Margin="10,0,5,0" Command="{Binding Cmd}" CommandParameter="Save"/>
        </StackPanel>
      </Grid>
    </Window>
    
    using Microsoft.EntityFrameworkCore;
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Linq;
    using System.Runtime.CompilerServices;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Input;
    using System.Windows.Media;
    using WpfApp007.Views;
    
    namespace WpfApp007
    {
      public class ContactListingViewModel : BaseClassPropertyChanged
      {
        AppDbContext db = new AppDbContext();
    
        public ContactListingViewModel()
        {
          db.tblContact.Load();
          ContactsList = db.tblContact.Local.ToObservableCollection();
          cvs.Source = ContactsList;
        }
    
        public ICollectionView View { get => cvs.View; }
        private CollectionViewSource cvs = new CollectionViewSource();
        public ObservableCollection<ContactModel> ContactsList { get; set; } = new ObservableCollection<ContactModel>();
    
        public ContactModel Contact { get; set; }
    
        #region Commands
    
        public ICommand Cmd { get => new DelegateCommand(CmdExec, CanExec); }
    
        private AddEditContactView addEditContactView;
        private bool? diagResult;
    
        private void CmdExec(object parameter)
        {
          switch (parameter.ToString())
          {
            case "Add":
              AddEditContactViewModel addEditContactViewModel = new AddEditContactViewModel() { Contact = new ContactModel() };
              addEditContactView = new AddEditContactView() { DataContext = addEditContactViewModel };
              diagResult = addEditContactView.ShowDialog();
              if (diagResult.HasValue && diagResult.Value)
              {
                db.tblContact.Add(addEditContactViewModel.Contact);
                db.SaveChanges();
              }
              break;
            case "Edit":
              if (Contact != null)
              {
                addEditContactView = new AddEditContactView() { DataContext = new AddEditContactViewModel() { Contact = Contact } };
                diagResult = addEditContactView.ShowDialog();
                if (diagResult.HasValue && diagResult.Value)
                {
                  db.SaveChanges();
                }
                else
                {
                  db.Entry(Contact).State = EntityState.Unchanged;
                  cvs.View.Refresh();
                  OnPropertyChanged(nameof(View));
                }
              }
              break;
            case "Delete":
              if (Contact != null)
              {
                db.tblContact.Remove(Contact);
                db.SaveChanges();
              }
              break;
            default:
              break;
          }
        }
    
        private bool CanExec(object parameter)
        {
          switch (parameter.ToString())
          {
            case "Add":
              return true;
            default:
              return (Contact != null);
          }
        }
    
        #endregion Commands
    
        #region Filter
        private string filterContact;
        public string FilterContact
        {
          get
          {
            return filterContact;
          }
    
          set
          {
            filterContact = value;
            OnPropertyChanged();
            cvs.View.Refresh();
          }
        }
    
        private void ContactFilter(object sender, FilterEventArgs e)
        {
          if (string.IsNullOrWhiteSpace(filterContact))
          {
            //no filter when no search text is entered
            e.Accepted = true;
          }
          else
          {
            ContactModel contact = (ContactModel)e.Item;
            if (contact.Firstname.StartsWith(filterContact, true, null) ||
                contact.Firstname.Contains(filterContact, StringComparison.InvariantCultureIgnoreCase) ||
                contact.Lastname.StartsWith(filterContact, true, null) ||
                contact.Lastname.Contains(filterContact, StringComparison.InvariantCultureIgnoreCase)
                )
            {
              e.Accepted = true;
            }
            else
            {
              e.Accepted = false;
            }
          }
        }
        #endregion Filter
      }
    
      public class AddEditContactViewModel : BaseClassPropertyChanged
      {
        public ContactModel Contact { get; set; }
    
        public ICommand Cmd { get => new DelegateCommand(CmdExec, CanCmdExec); }
        public void CmdExec(Object parameter)
        {
          switch (parameter.ToString())
          {
            case "Save":
              var count = (new AppDbContext()).tblContact.Count(t => t.Firstname == Contact.Firstname && t.Lastname == Contact.Lastname);
              if (count == 0) Contact.SetAddionalError("dublicate", string.Empty); else Contact.SetAddionalError("dublicate", "Dublicate Contact");
              OnPropertyChanged(nameof(Cmd));
              if (count == 0) wnd.DialogResult = true;
              break;
            case "Cancel":
              wnd.DialogResult = false;
              break;
            default:
              break;
          }
        }
    
        public bool CanCmdExec(object parameter) =>
          parameter.ToString() != "Save" || (Contact != null && string.IsNullOrEmpty(Contact.Error));
    
        #region attached property
        public Window wnd;
    
        public static readonly DependencyProperty AttPropProperty =
            DependencyProperty.RegisterAttached("AttProp", typeof(bool), typeof(AddEditContactViewModel), new PropertyMetadata(false, OnPropChanged));
        public static bool GetAttProp(DependencyObject obj) => (bool)obj.GetValue(AttPropProperty);
        public static void SetAttProp(DependencyObject obj, bool par) => obj.SetValue(AttPropProperty, par);
        private static void OnPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
          Window wnd = d as Window;
          if (wnd == null) return;
          wnd.Loaded += (s, e) => ((AddEditContactViewModel)(wnd.DataContext)).wnd = wnd;
        }
        #endregion
      }
    
      public class BaseClassPropertyChanged : INotifyPropertyChanged
      {
        public event PropertyChangedEventHandler PropertyChanged;
    
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
        {
          if (!string.IsNullOrEmpty(propertyName))
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
      }
    
      public class LimitedTextBox : TextBox
      {
        public LimitedTextBox()
        {
          this.PreviewKeyDown += LimitedTextBox_PreviewKeyDown;
          DataObject.AddPastingHandler(this, OnPaste);
        }
        private KeyConverter kc = new KeyConverter();
        private void LimitedTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
          string sText = this.Text;
          string sKey = kc.ConvertToString(e.Key);
          bool bCapsLock = Keyboard.IsKeyToggled(Key.CapsLock);
          bool bShift = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
          bool bCharacter = false;
          if ((!bCapsLock && !bShift) || (bCapsLock && bShift)) sKey = sKey.ToLower();
          if (sKey != null && sKey.Length == 1 && char.IsLetterOrDigit(sKey[0]))
          {
            sText += sKey[0];
            bCharacter = true;
          }
          // System.Windows.Controls.TextBoxView
          var nActualWidthView = this.ActualWidth;
          var sz = MeasureString(sText);
          if (bCharacter && sz.Width >= nActualWidthView)
          {
            e.Handled = true;
            Console.Beep(1000, 10);
          }
        }
    
        private void OnPaste(object sender, DataObjectPastingEventArgs e)
        {
          e.Handled = true;
          e.CancelCommand();
        }
    
        public Size MeasureString(string s)
        {
          FormattedText formattedText = new FormattedText(
            s,
            System.Globalization.CultureInfo.CurrentCulture,
            FlowDirection.LeftToRight,
            new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch),
            this.FontSize,
            new SolidColorBrush(Colors.Black),
            VisualTreeHelper.GetDpi(this).PixelsPerDip);
          return new Size(formattedText.Width, formattedText.Height);
        }
      }
    
      public class AppDbContext : DbContext
      {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
          optionsBuilder.UseSqlServer(WpfApp1.Properties.Settings.Default.cnSQL);
    
        public DbSet<ContactModel> tblContact { get; set; }
      }
    
      public class ContactModel : BaseClassPropertyChanged, IDataErrorInfo
      {
        public int Id { get; set; }
    
        private string _firstname = string.Empty;
        public string Firstname
        {
          get => this._firstname;
          set { this._firstname = value; ErrorCollection[nameof(Firstname)] = this[nameof(Firstname)]; OnPropertyChanged(); }
        }
    
        private string _lastname = string.Empty;
        public string Lastname
        {
          get => this._lastname;
          set { this._lastname = value; ErrorCollection[nameof(Lastname)] = this[nameof(Lastname)]; OnPropertyChanged(); }
        }
    
        #region ErrorInfo
        [NotMapped]
        public Dictionary<string, string> ErrorCollection { get; private set; } = new Dictionary<string, string>();
        [NotMapped]
        public string Error
        {
          get
          {
            String result = String.Empty;
            foreach (var item in ErrorCollection)
              if (!string.IsNullOrEmpty(item.Value)) result += (string.IsNullOrEmpty(result)) ? item.Value : Environment.NewLine + item.Value;
            return result;
          }
        }
    
        public void SetAddionalError(string errKey, string errMessaage)
        {
          if (string.IsNullOrEmpty(errMessaage)) ErrorCollection.Remove(errKey); else ErrorCollection[errKey] = errMessaage;
          OnPropertyChanged(nameof(Error));
        }
    
        [NotMapped]
        public string this[string propertyName]
        {
          get
          {
            string result = String.Empty;
            switch (propertyName)
            {
              case "Firstname":
                if (string.IsNullOrWhiteSpace(Firstname))
                  result = "Firstname cannot be empty";
                else if (Firstname.Length < 5)
                  result = "Firstname must be a minimum of 5 characters.";
                break;
              case "Lastname":
                if (string.IsNullOrWhiteSpace(Lastname))
                  result = "Lastname cannot be empty";
                else if (Firstname.Length < 5)
                  result = "Lastname must be a minimum of 5 characters.";
                break;
            }
            return result;
          }
        }
        #endregion ErrorInfo
    
      }
    
      public class DelegateCommand : ICommand
      {
    #pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
    #pragma warning disable CS8612 // Nullability of reference types in type doesn't match implicitly implemented member.
        private readonly Predicate<object> _canExecute;
        private readonly Action<object> _action;
        public DelegateCommand(Action<object> action) : this(action, null) { }
        public DelegateCommand(Action<object> action, Predicate<object> canExecute) { _action = action; _canExecute = canExecute; }
        public void Execute(object o) => _action(o);
        public bool CanExecute(object o) => _canExecute == null ? true : _canExecute(o);
        public event EventHandler CanExecuteChanged
        {
          add { CommandManager.RequerySuggested += value; }
          remove { CommandManager.RequerySuggested -= value; }
        }
    #pragma warning restore CS8612 // Nullability of reference types in type doesn't match implicitly implemented member.
    #pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
      }
    }
    

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.