Friday 9 December 2016

Create a ViewModel Base with validation for Model Errors in WPF

In this post we are going to see a sample ViewModel Base for WPF, which follows the MVVM pattern, and also along with we are checking the DataError.


    public class ViewModelBase : INotifyPropertyChanged
    {
        protected virtual void Set<T>(ref T member, T val,
            [CallerMemberName] string propertyName = null)
        {
            if (object.Equals(member, val)) return;

            member = val;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged = delegate { };

    }





   public class ValidatableViewModelBase : ViewModelBase, INotifyDataErrorInfo
    {
        private Dictionary<string, List<string>> _errorslist = 
                    new Dictionary<string, List<string>>();

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged = delegate { };

        public System.Collections.IEnumerable GetErrors(string prop)
        {
            if (_errorslist.ContainsKey(prop))
                return _errorslist[prop];
            else
                return null;
        }

        public bool HasErrors
        {
            get { return _errorslist.Count > 0; }
        }

        protected override void Set<T>(ref T member, T val, 
                 [CallerMemberName] string propertyName = null)
        {
            base.Set<T>(ref member, val, propertyName);
            ValidateProperty(propertyName, val);
        }

        private void ValidateProperty<T>(string propname, T value)
        {
            var res = new List<ValidationResult>();
            ValidationContext ctx = new ValidationContext(this);
            ctx.MemberName = propname;

            Validator.TryValidateProperty(value, ctx, res);

            if (res.Any())
            {           
                _errorslist[propname] = res.Select(c => c.ErrorMessage).ToList();
            }
            else
            {
                _errorslist.Remove(propname);
            }
            ErrorsChanged(this, new DataErrorsChangedEventArgs(propname));
        }


    }


Above code have two implementations which is normal model base another one is validate model base that is used for error notification in model.Then specify the Data Annotation above model



    class EmployeeEdit:ValidatableViewModelBase
    {
        private Guid _id;

        private string _firstname;

        private string _lastname;

        private string _phone;

        private string _email;

        public Guid Id
        {
            get { return _id; }
            set { Set<Guid>(ref _id, value); }
        }

        [Required]
        public string FirstName
        {
            get { return _firstname; }
            set { Set<string>(ref _firstname, value); }
        }

        [Required]
        public string LastName
        {
            get { return _lastname; }
            set { Set<string>(ref _lastname, value); }
        }

        [Phone]
        public string Phone
        {
            get { return _phone; }
            set { Set<string>(ref _phone, value); }
        }

        [EmailAddress]
        public string Email
        {
            get { return _email; }
            set { Set<string>(ref _email, value); }
        }


    }


Then Bind the Employee Edit or add view model with the save command where we can check whether there is a error in model , based on that we are disabling the command button.



    class AddEditEmployeeViewModel:ViewModelBase
    {
        private bool editMode;

        public bool EditMode
        {
            get { return editMode; }
            set
            {
                Set<bool>(ref editMode, value);
            }
        }

        private EmployeeEdit _employee;

        public EmployeeEdit Employee
        {
            get { return _employee; }
            set
            {
                Set<EmployeeEdit>(ref _employee, value);
            }
        }

        public RelayCommand SaveCommand;


        public AddEditEmployeeViewModel()
        {
            SaveCommand = new RelayCommand(OnSave, CanSave);
        }

        private Employee _editEmployee;

        public void BindEmployee(Employee employee)
        {
            _editEmployee = employee;
            Employee = new EmployeeEdit();
            Employee.ErrorsChanged += Employee_ErrorsChanged;
            Employee.Id = employee.Id;

            if (EditMode)
            {
                Employee.FirstName = employee.FirstName;
                Employee.LastName = employee.LastName;
                Employee.Email = employee.Email;
                Employee.Phone = employee.Phone;
            }

        }

        private void Employee_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
        {
            SaveCommand.RaiseCanExecuteChanged();
        }



        private async void OnSave()
        {

        }

        private bool CanSave()
        {
            return !Employee.HasErrors;
        }


    }



Create a Converter to make the visibility for Save Command button, Validates on NotifyDataErrors is the Property makes the notification on errors



    class EdittoVisiblityConvertor : IValueConverter
    {
        public object Convert(object value, Type targetType,
          object parameter, CultureInfo culture)
        {
            bool val;
            bool.TryParse(value.ToString(), out val);
            if (val)
                return Visibility.Visible;
            else
                return Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType,
           object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }



<UserControl
             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:Sample.Employee"
             xmlns:Data="clr-namespace:Zza.Data;assembly=Zza.Data"
             x:Class="Sample.Employee.AddEditEmployeeView"
             xmlns:conv="clr-namespace:Sample.Converters"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>

        <conv:EdittoVisiblityConvertor x:Key="EditConvertor" />
       
        <Style TargetType="{x:Type 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>
       
    </UserControl.Resources>
    <Grid x:Name="grid1" DataContext="{Binding Employee}"
          HorizontalAlignment="Left" Margin="30,30,0,0" VerticalAlignment="Top">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="17"/>
            <ColumnDefinition Width="93"/>
            <ColumnDefinition Width="16"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Content="First Name:" Grid.Column="0" HorizontalAlignment="Left" Margin="3,3,0,3" Grid.Row="0" VerticalAlignment="Center"/>
       
        <TextBox x:Name="firstNameTextBox" Grid.Column="1"
                 HorizontalAlignment="Left" Height="24" Margin="3,4,0,4"
                 Grid.Row="0"
                 Text="{Binding FirstName, ValidatesOnNotifyDataErrors=True}"
                 VerticalAlignment="Center" Width="120" Grid.ColumnSpan="3"/>
       
        <Label Content="Last Name:" Grid.Column="0" HorizontalAlignment="Left"            
                 Margin="3,3,0,3" Grid.Row="1" VerticalAlignment="Center"/>

        <TextBox x:Name="lastNameTextBox" Grid.Column="1" HorizontalAlignment="Left"  
                 Height="24" Margin="3,4,0,4" Grid.Row="1"
                 Text="{Binding LastName, ValidatesOnNotifyDataErrors=True}"
                 VerticalAlignment="Center" Width="120" Grid.ColumnSpan="3"/>
       
        <Label Content="Phone:" Grid.Column="0" HorizontalAlignment="Left"   
                 Margin="3,3,0,3" Grid.Row="2" VerticalAlignment="Center"/>
        <TextBox x:Name="phoneTextBox" Grid.Column="1"
                 HorizontalAlignment="Left" Height="24" Margin="3,4,0,4" Grid.Row="2"             
                 Text="{Binding Phone, ValidatesOnNotifyDataErrors=True}"
                 VerticalAlignment="Center" Width="120" Grid.ColumnSpan="3"/>
       
        <Button Content="Save" Command="{Binding SaveCommand}"
                Visibility="{Binding EditMode ,
                Converter={StaticResource EditConvertor}}"
                Width="75"></Button>
    </Grid>

</UserControl>




From this post you can learn how to create a ViewModelBase with Validation for Model Errors in WPF.