WPF Data Annotation Validation issue

Asked at 2017-01-04 19:27:19Z
  • 5 Subscribers
  • 114 Views
0

I am mostly web developer and I am super new to WPF. I am trying to work on one of the WPF project. I have hard time validating a model using a Data Annotations and MVVM. Can someone please point me what I might be doing wrong. Let me start with my model:

public class FamilyMember : PropertyChangedNotification
    {
        public long FamilyMemberId { get; set; }
        [Required(ErrorMessage = "First Name is required")]
        [RegularExpression(@"^[a-zA-Z''-'\s]{2,40}$")]
        public string FirstName { get; set; }

       [Required(ErrorMessage = "Last Name is required")]
        public string LastName { get; set; }
    }

Here is my PropertyChangedNotification class

public abstract class PropertyChangedNotification : INotifyPropertyChanged, IDataErrorInfo
    {
        #region Fields

        private readonly Dictionary<string, object> _values = new Dictionary<string, object>();

        #endregion

        #region Protected

        /// <summary>
        /// Sets the value of a property.
        /// </summary>
        /// <typeparam name="T">The type of the property value.</typeparam>
        /// <param name="propertySelector">Expression tree contains the property definition.</param>
        /// <param name="value">The property value.</param>
        protected void SetValue<T>(Expression<Func<T>> propertySelector, T value)
        {
            string propertyName = GetPropertyName(propertySelector);

            SetValue<T>(propertyName, value);
        }

        /// <summary>
        /// Sets the value of a property.
        /// </summary>
        /// <typeparam name="T">The type of the property value.</typeparam>
        /// <param name="propertyName">The name of the property.</param>
        /// <param name="value">The property value.</param>
        protected void SetValue<T>(string propertyName, T value)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            _values[propertyName] = value;
            NotifyPropertyChanged(propertyName);
        }

        /// <summary>
        /// Gets the value of a property.
        /// </summary>
        /// <typeparam name="T">The type of the property value.</typeparam>
        /// <param name="propertySelector">Expression tree contains the property definition.</param>
        /// <returns>The value of the property or default value if not exist.</returns>
        protected T GetValue<T>(Expression<Func<T>> propertySelector)
        {
            string propertyName = GetPropertyName(propertySelector);

            return GetValue<T>(propertyName);
        }

        /// <summary>
        /// Gets the value of a property.
        /// </summary>
        /// <typeparam name="T">The type of the property value.</typeparam>
        /// <param name="propertyName">The name of the property.</param>
        /// <returns>The value of the property or default value if not exist.</returns>
        protected T GetValue<T>(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            object value;
            if (!_values.TryGetValue(propertyName, out value))
            {
                value = default(T);
                _values.Add(propertyName, value);
            }

            return (T)value;
        }

        /// <summary>
        /// Validates current instance properties using Data Annotations.
        /// </summary>
        /// <param name="propertyName">This instance property to validate.</param>
        /// <returns>Relevant error string on validation failure or <see cref="System.String.Empty"/> on validation success.</returns>
        protected virtual string OnValidate(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            string error = string.Empty;
            var value = GetValue(propertyName);
            var results = new List<System.ComponentModel.DataAnnotations.ValidationResult>(1);
            var result = Validator.TryValidateProperty(
                value,
                new ValidationContext(this, null, null)
                {
                    MemberName = propertyName
                },
                results);

            if (!result)
            {
                var validationResult = results.First();
                error = validationResult.ErrorMessage;
            }

            return error;
        }

        #endregion

        #region Change Notification

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected void NotifyPropertyChanged(string propertyName)
        {
            this.VerifyPropertyName(propertyName);

            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                handler(this, e);
            }
        }

        protected void NotifyPropertyChanged<T>(Expression<Func<T>> propertySelector)
        {
            var propertyChanged = PropertyChanged;
            if (propertyChanged != null)
            {
                string propertyName = GetPropertyName(propertySelector);
                propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion // INotifyPropertyChanged Members

        #region Data Validation

        string IDataErrorInfo.Error
        {
            get
            {
                throw new NotSupportedException("IDataErrorInfo.Error is not supported, use IDataErrorInfo.this[propertyName] instead.");
            }
        }

        string IDataErrorInfo.this[string propertyName]
        {
            get
            {
                return OnValidate(propertyName);
            }
        }

        #endregion

        #region Privates

        private string GetPropertyName(LambdaExpression expression)
        {
            var memberExpression = expression.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new InvalidOperationException();
            }

            return memberExpression.Member.Name;
        }

        private object GetValue(string propertyName)
        {
            object value;
            if (!_values.TryGetValue(propertyName, out value))
            {
                var propertyDescriptor = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
                if (propertyDescriptor == null)
                {
                    throw new ArgumentException("Invalid property name", propertyName);
                }

                value = propertyDescriptor.GetValue(this);
                _values.Add(propertyName, value);
            }
            var propertyDescriptor1 = TypeDescriptor.GetProperties(GetType()).Find(propertyName, false);
            if (propertyDescriptor1 == null)
            {
                throw new ArgumentException("Invalid property name", propertyName);
            }

            value = propertyDescriptor1.GetValue(this);
            return value;
        }

        #endregion

        #region Debugging

        /// <summary>
        /// Warns the developer if this object does not have
        /// a public property with the specified name. This
        /// method does not exist in a Release build.
        /// </summary>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void VerifyPropertyName(string propertyName)
        {
            // Verify that the property name matches a real, 
            // public, instance property on this object.
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            {
                string msg = "Invalid property name: " + propertyName;

                if (this.ThrowOnInvalidPropertyName)
                    throw new Exception(msg);
                else
                    Debug.Fail(msg);
            }
        }

        /// <summary>
        /// Returns whether an exception is thrown, or if a Debug.Fail() is used
        /// when an invalid property name is passed to the VerifyPropertyName method.
        /// The default value is false, but subclasses used by unit tests might
        /// override this property's getter to return true.
        /// </summary>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

        #endregion // Debugging Aides
    }

Here is my ViewModel Class

class FamilyMemberViewModel : PropertyChangedNotification
    {
        private static FamilyMemberViewModel familyMemberViewModel;
        public RelayCommand SaveCommand { get; set; }
        public RelayCommand ClearCommand { get; set; }
        public RelayCommand SaveDataCommand { get; set; }
        public static int Errors { get; set; }
        private FamilyMemberViewModel()
        {
            FamilyMembers = new ObservableCollection<FamilyMember>();
            //TODO: Get all the family members and add it to the collection

            NewFamilyMember = new FamilyMember();
            SaveCommand = new RelayCommand(Save, CanSave);
            ClearCommand = new RelayCommand(Clear);
            SaveDataCommand = new RelayCommand(SaveData);
        }


        public ObservableCollection<FamilyMember> FamilyMembers
        {
            get { return GetValue(() => FamilyMembers); }
            set { SetValue(() => FamilyMembers, value); }
        }

        public FamilyMember NewFamilyMember
        {
            get { return GetValue(() => NewFamilyMember); }
            set { SetValue(() => NewFamilyMember, value); }
        }


        public static FamilyMemberViewModel SharedViewModel()
        {
            return familyMemberViewModel ?? (familyMemberViewModel = new FamilyMemberViewModel());
        }




        public void Save(object parameter)
        {
            FamilyMembers.Add(NewFamilyMember);
            Clear(this);
        }

        public bool CanSave(object parameter)
        {
            if (Errors == 0)
                return true;
            else
                return false;
        }

        public void Clear(object parameter)
        {
            NewFamilyMember = new FamilyMember();
        }

        public void SaveData(object parameter)
        {


        }
    }

Here is my User Control:

<UserControl x:Class="MyApp.Resources.AddMember"
             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="300" d:DesignWidth="300">
    <UserControl.Resources>
        <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Foreground" Value="Red"></Setter>
        </Style>
        <Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
        </Style>
        <Style TargetType="{x:Type ComboBox}">
            <Setter Property="FontSize" Value="16"/>
            <Setter Property="Margin" Value="0,5,0,0"/>
        </Style>
    </UserControl.Resources>

    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Label Target="{Binding ElementName=FirstName}" Grid.Row="0" Grid.Column="0">First Name:</Label>
        <StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal">
            <TextBox  Margin="10,0,0,10" x:Name="FirstName" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}" Validation.Error="Validation_Error" Width="100"/>
            <TextBlock Style="{StaticResource TextBlockStyle}" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=FirstName}" Margin="10,0,0,10"/>
        </StackPanel>

        <Label Target="{Binding ElementName=LastName}" Grid.Row="1" Grid.Column="0">Last Name:</Label>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" x:Name="LastName" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}" Validation.Error="Validation_Error"/>
        <Button Content="Save" Grid.Column="1" HorizontalAlignment="Center" Grid.Row="2" VerticalAlignment="Top" Width="75" Margin="0,20,0,0" Command="{Binding SaveCommand}"/>
    </Grid>
</UserControl>

Here is the Code Behind for the user control:

public partial class AddMember : UserControl
    {
        private int _noOfErrorsOnScreen = 0;
        private FamilyMember _member = new FamilyMember();
        public AddMember()
        {
            InitializeComponent();
            //this.DataContext = _member;
            this.DataContext = FamilyMemberViewModel.SharedViewModel();

        }
        private void Validation_Error(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added) FamilyMemberViewModel.Errors += 1;
            if (e.Action == ValidationErrorEventAction.Removed) FamilyMemberViewModel.Errors -= 1;
        }      

    }

Now the problem is: if I set the dataContext for the user control to be a FamilyMemberViewModel.SharedViewModel(), validation is not showing.. As soon as I set the DataContext to be an instance of FamilyMember, it is showing up.

Since this is an MVVM pattern, I don't want to directly reference my model into view plus, I have a RelayCommand that enables and disables the Save button in case there are any validation error.

Here is a screenshot when I reference new instance of my model directly in a user control (pardon my ugly looking view) enter image description here

Here is the screenshot when I reference my ViewModel as DataContext: enter image description here

I am very close on correct way of validating the model (at least I think it is since I am coming from web development environment and used data annotation validation in the past) but can't figure out what is wrong with it. I will appreciate little more explanation since I am very new to WPF

Thanks


1 answers in total

0
mm8 Posted at 2017-01-04 22:16:09Z

Now the problem is: if I set the dataContext for the user control to be a FamilyMemberViewModel.SharedViewModel(), validation is not showing.. As soon as I set the DataContext to be an instance of FamilyMember, it is showing up.

That's because the FamilyMemberViewModel class has no properties named "FirstName" and "LastName" that you bind to in the view so the bindings will fail.

When you set up a binding in XAML, you bind to a property of the DataContext of the element.

If you bind to the FirstName and LastName properties of the NewFamilyMember property of the DataContext, i.e. the FamilyMemberViewModel object returned by the FamilyMemberViewModel.SharedViewModel() method, it should work:

<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal">
    <TextBox  Margin="10,0,0,10" x:Name="FirstName" Text="{Binding NewFamilyMember.FirstName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}" Validation.Error="Validation_Error" Width="100"/>
    <TextBlock Style="{StaticResource TextBlockStyle}" Text="{Binding (Validation.Errors)[0].ErrorContent, ElementName=FirstName}" Margin="10,0,0,10"/>
</StackPanel>

<Label Target="{Binding ElementName=LastName}" Grid.Row="1" Grid.Column="0">Last Name:</Label>
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" x:Name="LastName" Text="{Binding NewFamilyMember.LastName, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}" Validation.Error="Validation_Error"/>
<Button Content="Save" Grid.Column="1" HorizontalAlignment="Center" Grid.Row="2" VerticalAlignment="Top" Width="75" Margin="0,20,0,0" Command="{Binding SaveCommand}"/>

Answer this questsion