0

I'm using INotifyDataErrorInfo, and this implementation: https://kmatyaszek.github.io/wpf-validation/2019/03/13/wpf-validation-using-inotifydataerrorinfo.html

Code in case link dies:

public class MainViewModel : BindableBase, INotifyDataErrorInfo
{
    private string _userName;
    private readonly Dictionary<string, List<string>> _errorsByPropertyName = new Dictionary<string, List<string>>();

    public MainViewModel()
    {
        UserName = null;
    }

    public string UserName
    {
        get => _userName;
        set
        {
            _userName = value;
            ValidateUserName();
            RaisePropertyChanged();
        }
    }

    public bool HasErrors => _errorsByPropertyName.Any();

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public IEnumerable GetErrors(string propertyName)
    {
        return _errorsByPropertyName.ContainsKey(propertyName) ?
            _errorsByPropertyName[propertyName] : null;
    }

    private void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    private void ValidateUserName()
    {
        ClearErrors(nameof(UserName));
        if (string.IsNullOrWhiteSpace(UserName))
            AddError(nameof(UserName), "Username cannot be empty.");
        if (string.Equals(UserName, "Admin", StringComparison.OrdinalIgnoreCase))
            AddError(nameof(UserName), "Admin is not valid username.");
        if (UserName == null || UserName?.Length <= 5)
            AddError(nameof(UserName), "Username must be at least 6 characters long.");
    }

    private void AddError(string propertyName, string error)
    {
        if (!_errorsByPropertyName.ContainsKey(propertyName))
            _errorsByPropertyName[propertyName] = new List<string>();

        if (!_errorsByPropertyName[propertyName].Contains(error))
        {
            _errorsByPropertyName[propertyName].Add(error);
            OnErrorsChanged(propertyName);
        }
    }

    private void ClearErrors(string propertyName)
    {
        if (_errorsByPropertyName.ContainsKey(propertyName))
        {
            _errorsByPropertyName.Remove(propertyName);
            OnErrorsChanged(propertyName);
        }
    }
}

It works very well, but now i want to validate integer value, not string. Basically user shouldn't be able to input anything else than int, otherwise app will crash. But i have no idea what ValidateIntegerValue() method can i write, since i can't check if int is int, since on back end it's always integer.

  • can you clearify what your input for your CheckInt() function should be? is it string? or even object? you can try Int32.TryParse(), see https://learn.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netcore-3.1 – TinoZ May 22 '20 at 12:48
  • Thats the problem, i don't know. Property would be for example `public int Integer {get;set;}`. And i can't check if `int` is `int`. On back end it's always `int`, on front end it can be everything. And i want to make sure user can't input anything else than integer. –  May 22 '20 at 12:50
  • so, you simply cant use exact that property. you have 2 ways to solve that. Youse a more generic property (object) or a Setter Method like SetMyInt(object o) and use that, otherwise you have to check before setting the property. – TinoZ May 22 '20 at 12:53
  • So, for every `int` property (and basically everything thats not `string`) i need "helper" property? Thats problematic, but doable. –  May 22 '20 at 13:00
  • yes, its the only way. Types are strict. Pushing anything to int will result in an exception, thats good, so you can handle the exception exactly on that point where it it is caused. Or the soft, more user friendly way, take care – TinoZ May 22 '20 at 13:04
  • I'm not at my PC right now, but I think that when you use bind an integer property to a textbox, it doesn't allow any non-integer values by itself. No extra validation needed. – Michal Diviš May 22 '20 at 13:55
  • I'm still able to input anything i want. –  May 22 '20 at 14:08

1 Answers1

0

An int property can only be set an int. Period. And it is not the responsibility of the view model to validate that the view, or a control in the view, doesn't try to set it to any other value because this will always fail. The view model simply exposes the property and it's the responsibility of the view to set it.

So the first thing to understand is that this kind of validation should take place in the view or the control. Binding to a string property and then convert the value in the view model is a bad idea.

What you could do instead is to create a custom ValidationRule:

public class StringToIntValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        int i;
        if (int.TryParse(value.ToString(), out i))
            return new ValidationResult(true, null);

        return new ValidationResult(false, "Please enter a valid integer value.");
    }
}

...that you associate with the binding:

<TextBox>
    <TextBox.Text>
        <Binding Path="Age" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:StringToIntValidationRule ValidationStep="RawProposedValue"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Please refer to this blog post for more information.

If you want to prevent the user from being able to type in invalid values in the TextBox, you could handle the PreviewTextInput event or create a custom control. Again, this has nothing to do with the view model.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • Alright, but then why does `INotifyDataErrorInfo` exists? Or it's more about using both depending on situation. –  May 22 '20 at 14:41
  • @gaijn: It's used to implement *data* validation in the view model. It cannot be used to validate how controls in the view set source properties. – mm8 May 22 '20 at 14:43
  • Ok i have managed to make it work, but i don't get one thing. `INotifyDataErrorInfo` has this fancy `HasErrors` property, so i can easily block any control if there are errors. Can i do something similar here? If not, user still can break things. –  May 22 '20 at 15:01
  • You can't easily block any control unless you set some property of the view model somehow. You might be better of preventing specific characters from being entered into the `TextBox` depending on your exact requirements. – mm8 May 22 '20 at 15:04
  • Another issue - 0,5 turns into 5. Well, INotifyDataErrorInfo might not be perfect, but at least it works. –  May 22 '20 at 15:06
  • Issue with what? Your particular implementation? The key takeaway from this and the answer to your question is that you can only handle this in the view unless you choose to bind to a string property (which you shouldn't). – mm8 May 22 '20 at 15:07
  • Yeah it feels to be rather imperfect solution, but `ValidationRule` turns 0,5 into 5, so i can either choose ugly solution, or not working solution. It appears to work only for integers, not decimals. (I have made implementationf or decimal). –  May 22 '20 at 15:09
  • @gaijn: The validation rule just validatea the value and returns a `ValidationResult`. It doesn't set the source property to some other value. – mm8 May 22 '20 at 15:10
  • @gaijn: It's probably the `UpdateSourceTrigger="PropertyChanged"` that changes the value. Try to remove it. Then you should get the exact same behaviour as before as regards to how the property is being set. – mm8 May 22 '20 at 15:11
  • Never even used it in the first place. It removes 0, by default. I have found out you can adjust decimal.Parse, but it doesn't work on decimal.TryParse. –  May 22 '20 at 15:20
  • Again, the validation rule itself doesn't remove anything. And of course `decimal.TryParse` works. Please ask a new question if you have another issue. – mm8 May 22 '20 at 15:21
  • Kind of solved. It still removes `,` automatically, but this `decimal.TryParse(value.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out i)` at least accepts dot. And apparently WPF is smart enough to block exactly the control i wanted by default, if there are errors. Thanks for help, this is perfect implementation for non-strings. –  May 22 '20 at 15:26
  • Ok it does not block it. Alright, i'm going back to INotifyDataErrorInfo. ValidationRules do not provide any working way to block desired controls, which in my opinion renders them useless, unless we are sure that user won't break anything, but that we cannot assume. –  May 22 '20 at 15:57
  • They are not supposed to block. They are supposed to validate. Blocking is the responsibility of the control. – mm8 May 25 '20 at 13:41