locked
DataAnnotations RRS feed

  • Question

  • Hi to all

    So I have just finished reading about IDataErrorInfo, when I came upon DataAnnotations.  Seem useful, a logical idea to have something like that implemented, but the problem is, they don't seem to work for me.  I created a classic desktop WPF application and did the following to try to get them to work...they don't work...can anyone see why?

    Thanks

    <Window x:Class="Example01.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:Example01"
            mc:Ignorable="d"
            Title="MainWindow" Height="346" Width="248">
        <Window.DataContext>
            <local:ViewModel x:Name="viewModel"/>
        </Window.DataContext>
        <Grid>
            <GroupBox x:Name="groupBox" Header="Customer" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Height="295" Width="220">
                <Grid>
                    <Label x:Name="lblEmail" Content="Email" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.921,-0.538" Width="102"/>
                    <TextBox x:Name="tbEmail" HorizontalAlignment="Left" Height="23" Margin="10,41,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="188" Text="{Binding Email, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"/>
                    <Label x:Name="lblAge" Content="Age" HorizontalAlignment="Left" Margin="10,80,0,0" VerticalAlignment="Top" Width="102"/>
                    <TextBox x:Name="tbAge" HorizontalAlignment="Left" Height="23" Margin="10,111,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="188" Text="{Binding Age}"/>
                    <Label x:Name="lblPhone" Content="Phone Number" HorizontalAlignment="Left" Margin="10,155,0,0" VerticalAlignment="Top" Width="102"/>
                    <TextBox x:Name="tbPhone" HorizontalAlignment="Left" Height="23" Margin="10,186,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="188" Text="{Binding PhoneNumber}"/>
    
                </Grid>
            </GroupBox>
        </Grid>
    </Window>
    

    and the ViewModel looks as follows:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.ComponentModel.DataAnnotations; 
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
    namespace Example01
    {
        class ViewModel : INotifyPropertyChanged
        {
            private string _Email = "";
    
            [EmailAddress(ErrorMessage ="Email address is invalid")]
            [Required(ErrorMessage ="Email address is required")]
            public string Email {
                get
                {
                    return _Email;
                }
                set
                {
                    _Email = value;
                    FirePropertyChanged();
                }
            }
        
            [Range(1,150,ErrorMessage ="Age is out of range")]
            [Required]
            public int Age { get; set; }
    
            [Phone]
            [Required(ErrorMessage = "Invalid Phone Number")]
            public long PhoneNumber { get; set; }
    
            public ViewModel()
            {
                if (true)
                {
                    Email = "JohnDoe@gmail.com";
                   
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
            public void FirePropertyChanged([CallerMemberName] string property = "")
            {
                if (PropertyChanged != null)
                    PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
    }
    


    MarcinMR

    Friday, February 12, 2016 4:49 PM

Answers

  • Or if you particularly want to stick with idataerrorinfo then use one of the solutions explained here:

    http://stackoverflow.com/questions/7071595/combining-dataannotations-and-idataerrorinfo-for-wpf

    public abstract class ViewModelBase<TViewModel> : IDataErrorInfo
        where TViewModel : ViewModelBase<TViewModel>
    {
        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, propertyGetters.Result[propertyName]((TViewModel)this)); } 
        }
    
        private static Task<Dictionary<string, Func<TViewModel, object>>> propertyGetters = Task.Run(() =>
        {
            return typeof(TViewModel).GetProperties()
                .Select(propertyInfo =>
                {
                    var viewModel = Expression.Parameter(typeof(TViewModel));
                    var property = Expression.Property(viewModel, propertyInfo);
                    var castToObject = Expression.Convert(property, typeof(object));
                    var lambda = Expression.Lambda(castToObject, viewModel);
    
                    return new
                    {
                        Key = propertyInfo.Name,
                        Value = (Func<TViewModel, object>)lambda.Compile()
                    };
                })
                .ToDictionary(pair => pair.Key, pair => pair.Value);
        });
    
        protected virtual string OnValidate(string propertyName, object propertyValue)
        {
            var validationResults = new List<ValidationResult>();
    
            var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
    
            if (!Validator.TryValidateProperty(propertyValue, validationContext, validationResults))
            {
                return validationResults.First().ErrorMessage;
            }
    
            return string.Empty;
        }
    }


    Hope that helps.

    Technet articles: WPF: Layout Lab; All my Technet Articles

    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 5:39 PM
  • >>I've found the EmailAddressAttribute definition when I peaked inside of it (F12), and funny thing, the class overides the IsValid method with nothing???  Where is the method body?

    In the System.ComponentModel.DataAnnotations assembly: http://referencesource.microsoft.com/#System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs,c3ae85dfd8a9f58c

    When you press F12 to go the definition in Visual Studio, you only see the metadata of the class and not the actual implementation.

    Regarding the data annotations, you need to provide some piece of code that actually evaluates the attributes. Otherwise the attributes are pointless. Please refer to my blog post (last section) for more information and an example: http://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/ 

    Hope that helps.

    Please remember to close your threads by marking helpful posts as answer and then start a new thread if you have a new question. Please don't ask several questions in the same thread.

    • Edited by Magnus (MM8)MVP Friday, February 12, 2016 7:50 PM
    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 7:48 PM
  • I am glad that it works but again: Please don't ask several questions in one thread. Please mark all helpful posts as answer once your original question has been answered and then start a new thread if you have a new question.

    >>BUT I don't have INotifyPropertyChanged implemented in my ViewModel, and yet all still works?  How?

    The INotifyPropertyChanged has nothing to do with validation. It is used to notify the view that a data bound property, typically in a view model, has changed dynamically.

    Please close this thread by marking all helpful posts as answer.

    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 8:48 PM

All replies

  • When I say, they don't work, what I mean is I can type anything into the Email address textbox and it won't complain?  It could even be empty.  Although the Age and Phone number do complain when empty?

    MarcinMR

    Friday, February 12, 2016 4:51 PM
  • You've not implemented InotifyDataError or done any validation, so they will do nothing.

    Personally, I use dataannotations like this with entity framework and you can see a complete working solution here:

    https://gallery.technet.microsoft.com/WPF-Entity-Framework-MVVM-78cdc204

    BaseEntity implements the interface:

        public class BaseEntity : NotifyUIBase, INotifyDataErrorInfo
        {
            // From Validation Error Event
            private RelayCommand<PropertyError> conversionErrorCommand;
            public RelayCommand<PropertyError> ConversionErrorCommand
            {
                get
                {
                    return conversionErrorCommand
                        ?? ( conversionErrorCommand = new RelayCommand<PropertyError>
                            (PropertyError =>
                           {
                               if (PropertyError.Added)
                               {
                                   AddError(PropertyError.PropertyName, PropertyError.Error, ErrorSource.Conversion);
                               }
                               FlattenErrorList();
                           }));
                }
            }
    
            // From Binding SourceUpdate Event
            private RelayCommand<string> sourceUpdatedCommand;
            public RelayCommand<string> SourceUpdatedCommand
            {
                get
                {
                    return sourceUpdatedCommand
                        ?? (sourceUpdatedCommand = new RelayCommand<string>
                            (Property =>
                            {
                                ValidateProperty(Property);
                            }));
                }
            } 
    
            private ObservableCollection<PropertyError> errorList = new ObservableCollection<PropertyError>();
            public ObservableCollection<PropertyError>  ErrorList
            {
                get
                {
                    return errorList;
                }
                set
                {
                    errorList = value;
                    RaisePropertyChanged();
                }
            }
    
            protected Dictionary<string, List<AnError>> errors = new Dictionary<string, List<AnError>>();
            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
            public IEnumerable GetErrors(string property)
            {
                if (string.IsNullOrEmpty(property))
                {
                    return null;
                }
                if (errors.ContainsKey(property) && errors[property] != null && errors[property].Count > 0)
                {
                    return errors[property].Select(x=>x.Text).ToList();
                }
                return null;
            }
            public bool HasErrors
            {
                get { return errors.Count > 0; }
            }
            public void NotifyErrorsChanged(string propertyName)
            {
                if (ErrorsChanged != null)
                {
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
                }
            }
            public bool IsValid()
            {
                // Clear only the errors which are from object Validation
                // Conversion errors won't be detected here
                RemoveConversionErrorsOnly();
    
                var vContext = new ValidationContext(this, null, null);
                List<ValidationResult> vResults = new List<ValidationResult>();
                Validator.TryValidateObject(this, vContext, vResults, true);
                TransformErrors(vResults);
                var propNames = errors.Keys.ToList();
                propNames.ForEach(pn => NotifyErrorsChanged(pn));
                NotifyPropertiesInError();
                FlattenErrorList();
    
                if (propNames.Count > 0)
                {
                    return false;
                }
                return true;
            }
            private void RemoveConversionErrorsOnly()
            {
                foreach(KeyValuePair<string, List<AnError> > pair in errors)
                {
                    List<AnError> _list = pair.Value;
                    _list.RemoveAll(x => x.Source == ErrorSource.Validation);
                }
    
                var removeprops = errors.Where(x => x.Value.Count == 0)
                    .Select(x => x.Key)
                    .ToList();
                foreach (string key in removeprops)
                {
                     errors.Remove(key);
                }
            }
            public void ValidateProperty(string propertyName)
            {
                // If validating a property then there can be no conversion error
                errors.Remove(propertyName);
    
                var vContext = new ValidationContext(this, null, null);
                vContext.MemberName = propertyName;
                List<ValidationResult> vResults = new List<ValidationResult>();
                Validator.TryValidateProperty(this.GetType().GetProperty(propertyName).GetValue(this, null), vContext, vResults);
    
                TransformErrors(vResults);
                FlattenErrorList();
    
                NotifyErrorsChanged(propertyName);
            }
            private void TransformErrors(List<ValidationResult> results)
            {
                foreach (ValidationResult r in results)
                {
                    foreach (string ppty in r.MemberNames)
                    {
                        AddError(ppty, r.ErrorMessage, ErrorSource.Validation);
                    }
                }
            }
            private void AddError(string ppty, string err, ErrorSource source)
            {
                List<AnError> _list;
                if (!errors.TryGetValue(ppty, out _list))
                {
                    errors.Add(ppty, _list = new List<AnError>());
                }
                if (!_list.Any(x => x.Text == err))
                {
                    _list.Add(new AnError { Text = err, Source = source });
                }
            }
            private void FlattenErrorList()
            {
                ObservableCollection<PropertyError> _errorList = new ObservableCollection<PropertyError>();
                foreach (var prop in errors.Keys)
                {
                    List<AnError> _errs = errors[prop];
                    foreach (AnError err in _errs)
                    {
                        _errorList.Add(new PropertyError { PropertyName = prop, Error = err.Text });
                    }
                }
                ErrorList = _errorList;
            }
            private void NotifyPropertiesInError()
            {
                foreach (var prop in errors.Keys)
                {
                    NotifyErrorsChanged(prop);
                }
            }
            public void ClearErrors()
            {
                errors.Clear();
                ErrorList.Clear();
                NotifyErrorsChanged("");
            }
        }


    Hope that helps.

    Technet articles: WPF: Layout Lab; All my Technet Articles

    Friday, February 12, 2016 4:59 PM
  • Or if you particularly want to stick with idataerrorinfo then use one of the solutions explained here:

    http://stackoverflow.com/questions/7071595/combining-dataannotations-and-idataerrorinfo-for-wpf

    public abstract class ViewModelBase<TViewModel> : IDataErrorInfo
        where TViewModel : ViewModelBase<TViewModel>
    {
        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, propertyGetters.Result[propertyName]((TViewModel)this)); } 
        }
    
        private static Task<Dictionary<string, Func<TViewModel, object>>> propertyGetters = Task.Run(() =>
        {
            return typeof(TViewModel).GetProperties()
                .Select(propertyInfo =>
                {
                    var viewModel = Expression.Parameter(typeof(TViewModel));
                    var property = Expression.Property(viewModel, propertyInfo);
                    var castToObject = Expression.Convert(property, typeof(object));
                    var lambda = Expression.Lambda(castToObject, viewModel);
    
                    return new
                    {
                        Key = propertyInfo.Name,
                        Value = (Func<TViewModel, object>)lambda.Compile()
                    };
                })
                .ToDictionary(pair => pair.Key, pair => pair.Value);
        });
    
        protected virtual string OnValidate(string propertyName, object propertyValue)
        {
            var validationResults = new List<ValidationResult>();
    
            var validationContext = new ValidationContext(this, null, null) { MemberName = propertyName };
    
            if (!Validator.TryValidateProperty(propertyValue, validationContext, validationResults))
            {
                return validationResults.First().ErrorMessage;
            }
    
            return string.Empty;
        }
    }


    Hope that helps.

    Technet articles: WPF: Layout Lab; All my Technet Articles

    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 5:39 PM
  • Going over your example will take me some time...in the mean time, I've found the EmailAddressAttribute definition when I peaked inside of it (F12), and funny thing, the class overides the IsValid method with nothing???  Where is the method body?

    namespace System.ComponentModel.DataAnnotations
    {
        //
        // Summary:
        //     Validates an email address.
        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
        public sealed class EmailAddressAttribute : DataTypeAttribute
        {
            //
            // Summary:
            //     Initializes a new instance of the System.ComponentModel.DataAnnotations.EmailAddressAttribute
            //     class.
            public EmailAddressAttribute();
    
            //
            // Summary:
            //     Determines whether the specified value matches the pattern of a valid email address.
            //
            // Parameters:
            //   value:
            //     The value to validate.
            //
            // Returns:
            //     true if the specified value is valid or null; otherwise, false.
            public override bool IsValid(object value);
        }
    }


    MarcinMR

    Friday, February 12, 2016 7:23 PM
  • >>I've found the EmailAddressAttribute definition when I peaked inside of it (F12), and funny thing, the class overides the IsValid method with nothing???  Where is the method body?

    In the System.ComponentModel.DataAnnotations assembly: http://referencesource.microsoft.com/#System.ComponentModel.DataAnnotations/DataAnnotations/EmailAddressAttribute.cs,c3ae85dfd8a9f58c

    When you press F12 to go the definition in Visual Studio, you only see the metadata of the class and not the actual implementation.

    Regarding the data annotations, you need to provide some piece of code that actually evaluates the attributes. Otherwise the attributes are pointless. Please refer to my blog post (last section) for more information and an example: http://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/ 

    Hope that helps.

    Please remember to close your threads by marking helpful posts as answer and then start a new thread if you have a new question. Please don't ask several questions in the same thread.

    • Edited by Magnus (MM8)MVP Friday, February 12, 2016 7:50 PM
    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 7:48 PM
  • Is there a way to peak into the implementation from VS?

    MarcinMR

    Friday, February 12, 2016 7:56 PM
  • Please don't ask several questions in one thread.

    >>Is there a way to peak into the implementation from VS?

    No. You need to download the source code:
    http://referencesource.microsoft.com/download.html
    http://referencesource.microsoft.com/setup.html

    Hope that helps.

    Please remember to close this threads by marking all helpful posts as answer and then start a new thread if you have a new question.

    Friday, February 12, 2016 8:06 PM
  • I just copied the parts of your code to make the DataAnnotations work, and voila, it works, BUT I don't have INotifyPropertyChanged implemented in my ViewModel, and yet all still works?  How?


    MarcinMR

    My Code is below:

    <Window x:Class="Example02.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:Example02"
            mc:Ignorable="d"
            Title="MainWindow" Height="237.5" Width="514.5">
        <Window.DataContext>
            <local:ViewModel x:Name="viewModel"/>
        </Window.DataContext>
        <Grid Margin="0,0,2,-21">
            <Label x:Name="lblUserName" Content="User Name:" HorizontalAlignment="Left" Margin="10,44,0,0" VerticalAlignment="Top" Width="108"/>
            <Label x:Name="lblName" Content="Name:" HorizontalAlignment="Left" Margin="10,104,0,0" VerticalAlignment="Top" Width="108"/>
            <TextBox x:Name="tbUserName" HorizontalAlignment="Left" Height="23" Margin="123,47,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="201" Text="{Binding Username, UpdateSourceTrigger=PropertyChanged}">
                <Validation.ErrorTemplate>
                    <ControlTemplate>
                        <StackPanel>
                            <!-- Placeholder for the TextBox itself -->
                            <AdornedElementPlaceholder x:Name="tbUserName"/>
                            <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
                        </StackPanel>
                    </ControlTemplate>
                </Validation.ErrorTemplate>
            </TextBox>
            <TextBox x:Name="tbName" HorizontalAlignment="Left" Height="23" Margin="123,107,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="201" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}">
                <Validation.ErrorTemplate>
                    <ControlTemplate>
                        <StackPanel>
                            <!-- Placeholder for the TextBox itself -->
                            <AdornedElementPlaceholder x:Name="tbName"/>
                            <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
                        </StackPanel>
                    </ControlTemplate>
                </Validation.ErrorTemplate>
            </TextBox>
    
        </Grid>
    </Window>
    

    and the ViewModel:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace Example02
    {
        class ViewModel : INotifyDataErrorInfo
        {
            #region Model
            private string _Username;
            private string _Name;
    
            [Required(ErrorMessage = "You must enter a username.")]
            [StringLength(10, MinimumLength = 4,
            ErrorMessage = "The username must be between 4 and 10 characters long")]
            [RegularExpression(@"^[a-zA-Z]+$", ErrorMessage = "The username must only contain letters (a-z, A-Z).")]
            public string Username
            {
                get { return _Username; }
                set
                {
                    _Username = value;
                    ValidateModelProperty(value, "Username");
                }
            }
    
            [Required(ErrorMessage = "You must enter a name.")]
            public string Name
            {
                get { return _Name; }
                set
                {
                    _Name = value;
                    ValidateModelProperty(value, "Name");
                }
            }
            #endregion
    
            private readonly Dictionary<string, ICollection<string>> _validationErrors = new Dictionary<string, ICollection<string>>();
    
            protected void ValidateModelProperty(object value, string propertyName)
            {
                if (_validationErrors.ContainsKey(propertyName))
                    _validationErrors.Remove(propertyName);
    
                ICollection<ValidationResult> validationResults = new List<ValidationResult>();
                ValidationContext validationContext =
                    new ValidationContext(this, null, null) { MemberName = propertyName };
                if (!Validator.TryValidateProperty(value, validationContext, validationResults))
                {
                    _validationErrors.Add(propertyName, new List<string>());
                    foreach (ValidationResult validationResult in validationResults)
                    {
                        _validationErrors[propertyName].Add(validationResult.ErrorMessage);
                    }
                }
                RaiseErrorsChanged(propertyName);
            }
    
            #region INotifyDataErrorInfo members
            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
            private void RaiseErrorsChanged(string propertyName)
            {
                if (ErrorsChanged != null)
                    ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
    
            public System.Collections.IEnumerable GetErrors(string propertyName)
            {
                if (string.IsNullOrEmpty(propertyName)
                    || !_validationErrors.ContainsKey(propertyName))
                    return null;
    
                return _validationErrors[propertyName];
            }
    
            public bool HasErrors
            {
                get { return _validationErrors.Count > 0; }
            }
            #endregion
    
    
        }
    }
    

    • Edited by MarcinMR Friday, February 12, 2016 8:44 PM
    Friday, February 12, 2016 8:43 PM
  • I am glad that it works but again: Please don't ask several questions in one thread. Please mark all helpful posts as answer once your original question has been answered and then start a new thread if you have a new question.

    >>BUT I don't have INotifyPropertyChanged implemented in my ViewModel, and yet all still works?  How?

    The INotifyPropertyChanged has nothing to do with validation. It is used to notify the view that a data bound property, typically in a view model, has changed dynamically.

    Please close this thread by marking all helpful posts as answer.

    • Marked as answer by MarcinMR Friday, February 12, 2016 8:49 PM
    Friday, February 12, 2016 8:48 PM