2

If I have a WPF view with a textbox that has a binding to a decimal (or any other number format) I automatically get a visual hint if I enter a letter or any other invald character and the value is not transferred to the viewmodel (the breakpoint on the setter is never reached). If I enter a number, everything works fine. To disable my save-Button (ICommand), I would like to get the info in my viewmodel that there is an error in the view in a MVVM-like fashion. Hints to where this behavior is documented are very welcome!

So the target situation looks like this:

current situation as is by WPF standard

what I want would be a disable "save and close": enter image description here

XAML:

<TextBox Text="{Binding Path=SelectedItem.Punkte_Seite_max, UpdateSourceTrigger=PropertyChanged}"/>

ViewModel

public int Punkte_Seite_max
{
  get { return _punkte_Seite_max; }
  set
  {
    _punkte_Seite_max = value;
    Changed(); //INotifyPropertyChanged  call
  }
}
Jan
  • 3,825
  • 3
  • 31
  • 51
  • @SylF what do you refer to exactly? I know how to attach commands. (but I had the wrong property copied from my VM, edited my question just now) – Jan Sep 18 '18 at 19:38
  • That is not my problem. My problem is what condition i need to check to return false if the textbox has for example a value of "aaa". – Jan Sep 18 '18 at 19:47
  • You need to know if the text is numeric? – SylF Sep 18 '18 at 19:52
  • If it is a valid number but it is more a general question. There is some implicit Validation in place and i am interested in its code and result – Jan Sep 18 '18 at 19:55
  • I don't know about the implicit validation code. But for the validation you can do by yourself, there is https://stackoverflow.com/questions/894263/how-do-i-identify-if-a-string-is-a-number. – SylF Sep 18 '18 at 20:00
  • Bind to the Validation.HasError property: https://stackoverflow.com/questions/22827437/binding-validation-haserror-property-in-mvvm – mm8 Sep 19 '18 at 09:36

2 Answers2

1

What you want to be using is INotifyDataErrorInfo documentation found here. This lets you provide custom validation on the properties that you have bound to your ViewModel.

This is a sample I have shamelessly copied from CodeProject but I have done so to prevent any link rot. I have also tried to adapt it slightly to match your example.

ViewModel

public class ViewModel : INotifyDataErrorInfo
{
    // A place to store all error messages for all properties.
    private IDictionary<string, List<string>> propertyErrors = new Dictionary<string, List<string>>();

    public string Preis
    {
        get { return _preis; }
        set
        {
            // Only update if the value has actually changed.
            if (!string.Equals(_preis, value, StringComparison.Ordinal))
            {
                _preis = value;
                Changed();

                this.Validate();
            }
        }
    }

    // The event to raise when the error state changes.
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;     

    // A method of getting all errors for the given known property.
    public System.Collections.IEnumerable GetErrors(string propertyName)
    {
        if (propertyName != null)
        {
            if (propertyErrors.TryGetValue(propertyName, out var errors))
            {
                return errors;
            }
        }

        return null;
    }

    // Whether there are any errors on the ViewModel
    public bool HasErrors
    {
        get
        {    
            return propertyErrors.Values.Any(r =>r.Any());
        }
    }

    private void Validate()
    {
        // 1. HERE YOU CAN CHECK WHETHER Preis IS VALID AND ANY OTHER PROPERTIES
        // 2. Update the 'propertyErrors' dictionary with the errors
        // 3. Raise the ErrorsChanged event.
    }
}

XAML

You will need to change your XAML to something like this:

<TextBox>
    <Binding Path="Preis" UpdateSourceTrigger="PropertyChanged" ValidatesOnNotifyDataErrors="True"/>
</TextBox>
Bijington
  • 3,661
  • 5
  • 35
  • 52
  • Sorry, preis is an int. Copied the wrong property and overlooked in first correction. Problem with your approach is that the set of preis is never reached if the provided value is no int. – Jan Sep 18 '18 at 20:19
  • @Jan You are correct that if Preis were an int then you wouldn’t get the update. The only ways I know of are to 1) declare it as a string for the purposes of binding to the control and handle the conversion later on when needed or 2) write a custom control to do the validation for you – Bijington Sep 18 '18 at 20:23
  • @Jan as an expansion of my original idea you could wrap the validation logic up into a class and use that to bind to the control. Something like: ValidatedProperty where T in this case would be your int? – Bijington Sep 18 '18 at 20:28
  • The curious thing is that i get a red border (i.e. the default validation error style) when a wrong value is entered. So there must be an event or anything likewise already. Any ideas? – Jan Sep 18 '18 at 20:32
  • 1
    @jan a quick google found this https://stackoverflow.com/a/22830845 is that what you are looking for? – Bijington Sep 18 '18 at 20:38
1

Thanks to Bijington I got on the right track and found an answer which satisfies MVVM and also doesn't need code behind. In case someone is interested here's my solution to this issue.

The error shown above is created in the view because there is no converter in WPF from letters to int (how should there be one). To raise this issue the binding in needs to have NotifyOnValidationError=True.

 <TextBox Text="{Binding Path=SelectedItem.Punkte_Seite_max, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True}"

This raises a bubbling up Validation.Error event that can be captured anywhere in the tree. I decided to capture it via a routed event trigger like so: XAML:

<Window
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" >
    <i:Interaction.Triggers>
    <userInterface:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}" >
        <userInterface:ViewErrorCounterAction ViewErrorCounter="{Binding Path=ViewValidationErrorCount, Mode=TwoWay}"/>
    </userInterface:RoutedEventTrigger>
</i:Interaction.Triggers>

So the twoway-binding is the MVVM-okayish link to my viewmodel.

ViewErrorCounterAction is based on this SO answer:

public class ViewErrorCounterAction : TriggerAction<DependencyObject> {
public ViewErrorCounterAction()
{
    ViewErrorCounter = 0;  // initalize with 0 as there should not be such errors when the window is loaded
}

public int ViewErrorCounter
{
    get
    {
        return System.Convert.ToInt32(GetValue(ViewErrorCounterProperty));
    }
    set
    {
        SetValue(ViewErrorCounterProperty, value);
    }
}

public static readonly DependencyProperty ViewErrorCounterProperty = DependencyProperty.Register("ViewErrorCounter", typeof(int), typeof(ViewErrorCounterAction), new PropertyMetadata(null));

protected override void Invoke(object parameter)
{
    var e = (ValidationErrorEventArgs)parameter;
    if ((e.Action == ValidationErrorEventAction.Added))
        ViewErrorCounter = ViewErrorCounter + 1;
    else if ((e.Action == ValidationErrorEventAction.Removed))
        ViewErrorCounter = ViewErrorCounter - 1;
}
}

Finally routed Event Trigger is based on https://sergecalderara.wordpress.com/2012/08/23/how-to-attached-an-mvvm-eventtocommand-to-an-attached-event/

Hope this helps and I'd appreciate comments on how to better solve this issue if there are more elegant ways :)

Jan
  • 3,825
  • 3
  • 31
  • 51
  • While this works what kind of error display do you get inside the `ToolTip` of the `TextBox`? I am asking mostly out of intrigue but would like to point out that you really want to try and make it as easy to understand to the user as possible if it does show as invalid. – Bijington Sep 20 '18 at 18:09
  • For that I also use INotifyDataErrorInfo and a style trigger. Since my question was only about that one specific issue, i kept the example how I solved it brief to concentrate only on those necessary bits – Jan Sep 21 '18 at 04:58