-3

I am trying to bind a property of my model to a property on a TextBox control. When the property has a string value, the TextBox renders it like a validation message (red boarder and tool tip preferred). And when the property has a null or empty string, no validation message is rendered.

It would look something like this:

<TextBox Text="{Binding FirstName}" ValidationText="{Binding Errors.FirstName}" />

It seems like it should be as easy as binding the text property but I can't seem to get it.

Bill Heitstuman
  • 979
  • 1
  • 8
  • 23
  • 1
    You should implement the [INotifyDataErrorInfo](https://social.technet.microsoft.com/wiki/contents/articles/19490.wpf-4-5-validating-data-in-using-the-inotifydataerrorinfo-interface.aspx) interface. – mm8 Mar 26 '19 at 16:19
  • Thanks for the suggestion. I am familiar with how WPF does validation. Including Validation Rules, IDataErrorInfo, and INotifyDataErrorInfo. It's strongly opinionated on how it should be done. However, I just need to bind a string property to a validation message. – Bill Heitstuman Mar 26 '19 at 16:43
  • 2
    Please elaborate. What is `ValidationText` in this case? And what is `Errors.FirstName`? – mm8 Mar 27 '19 at 14:55
  • `ValidationText` is a string property of the control (similar to the `Text` property). And `Errors.FirstName` is a public property of the model; but it could also be any bindable public property (similar to how you would bind to the `Text` property). – Bill Heitstuman Mar 28 '19 at 16:54
  • As an idea you can create attached behavior (attached property + logic in callback), which will get current binding of `Text`, add custom validation rule to it, etc. It could be specific to `TextBox` or you can specify property to validate via another attached property. Another possibility is to inherit from `Binding` and create own binding with own rules and use pre-defined set of attached properties like `ValidationText`. – Sinatr Mar 29 '19 at 11:14
  • 3
    @BillHeitstuman: So what is your issue? Does the binding fail? Where are you supposed to see the value of the `ValidationText` property? Please provide a [MCVE](https://stackoverflow.com/help/mcve) of your issue. – mm8 Mar 29 '19 at 12:43
  • Accordingly to @mm8 comments, i'd suggest to read this too: [How to: Implement Binding Validation](https://stackoverflow.com/questions/19539492/implement-validation-for-wpf-textboxes) and [Implement Validation for WPF TextBoxes](https://stackoverflow.com/questions/19539492/implement-validation-for-wpf-textboxes) – Maciej Los Mar 29 '19 at 13:20
  • 1
    Is ```ValidationText``` a Dependency Property? If so, could you add the code. By default there is no ```ValidationText``` property on the basic TextBox control. I think adding some more details to your question would help you to get better answers. – Jason G. Apr 01 '19 at 16:16

3 Answers3

2

If you want to bind a Validation Message to a Property of your Model, I will suggest that you implement INotifyDataErrorInfo, you can then validate whenever you want, get the errors and show the specific message you want, for example, as follows:

//Model class
[Required]
[CustomValidation(typeof(CustomValidation), "ValidateBirthday")]
public string Birthday
{
    get => _birthday;
    set => Set(ref _birthday, value);
}

//Custom Validation Class
public static ValidationResult ValidateBirthday(object inObj, ValidationContext inContext)
{
     Model model = (Model) inContext.ObjectInstance;

     string text = model.Birthday;

     DateTime birthday = text.ToDate();

     if (birthday == default)
     {
         return new ValidationResult("Birthday is not valid", new List<string> {"Birthday"});
     }

     // Future
     if (birthday >= DateTime.Now.Date)
     {
         return new ValidationResult("Birthday is in the future", new List<string> {"Birthday"});
     }

     // Past
     return birthday <= DateTime.Now.Date.AddYears(-200)
         ? new ValidationResult("Birthday too old", new List<string> {"Birthday"})
         : ValidationResult.Success;
 }

The you just need to validate your model and get the messages to show in in the Textbox you want:

public void ValidateModel()
{
    ValidationContext context = new ValidationContext(this);
    List<ValidationResult> results = new List<ValidationResult>();

    Validator.TryValidateObject(this, context, results, true);

    foreach (KeyValuePair<string, List<string>> valuePair in _validationErrors.ToList())
    {
        if (!results.All(r => r.MemberNames.All(m => m != valuePair.Key)))
        {
            continue;
        }

        _validationErrors.TryRemove(valuePair.Key, out List<string> _);
        RaiseErrorChanged(valuePair.Key);
    }

    IEnumerable<IGrouping<string, ValidationResult>> q = from r in results
        from m in r.MemberNames
        group r by m
        into g
        select g;

    foreach (IGrouping<string, ValidationResult> prop in q)
    {
        List<string> messages = prop.Select(r => r.ErrorMessage).ToList();

        if (_validationErrors.ContainsKey(prop.Key))
        {
            _validationErrors.TryRemove(prop.Key, out List<string> _);
        }

        _validationErrors.TryAdd(prop.Key, messages);
        RaiseErrorChanged(prop.Key);
    }
}

You can use the Validation.ErrorTemplate to bind and show the message:

<TextBox Text="{Binding Birthday, UpdateSourceTrigger=PropertyChanged}">
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <StackPanel>
                <!-- Placeholder for the TextBox itself -->
                <AdornedElementPlaceholder x:Name="textBox"/>
                <TextBlock Text="{Binding [0].ErrorContent}" Foreground="Red"/>
            </StackPanel>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>

Hope it helps!

Nekeniehl
  • 1,633
  • 18
  • 35
0

There are several ways to validate in WPF.

I personally prefer reusable custom validation rules and I like to inherit from Binding instead of having to repeatedly write all those small things (like UpdateSourceTrigger) in xaml or use bloated full tag syntax to add validation rules.

You want to have a property to which you can bind custom validation error text. One possibility is to use attached property to which you can bind and which is then used by validation rule to return the error. We can encapsulate everything into custom binding class:

public class Bind : Binding
{
    // validation rule
    class Rule : ValidationRule
    {
        public Rule() : base(ValidationStep.RawProposedValue, true) { }

        public override ValidationResult Validate(object value, CultureInfo cultureInfo) => ValidationResult.ValidResult;
        public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
        {
            if (!string.IsNullOrEmpty((string)value))
                return new ValidationResult(false, GetError(owner.Target));
            return base.Validate(value, cultureInfo, owner);
        }
    }

    // attached property to hold error text
    public static string GetError(DependencyObject obj) => (string)obj.GetValue(ErrorProperty);
    public static void SetError(DependencyObject obj, string value) => obj.SetValue(ErrorProperty, value);
    public static readonly DependencyProperty ErrorProperty = DependencyProperty.RegisterAttached("Error", typeof(string), typeof(Bind));

    // custom binding
    public Bind() : base() => Init();
    public Bind(string path) : base(path) => Init();
    void Init()
    {
        UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
        ValidationRules.Add(new Rule());
    }
}

Usage in xaml:

<TextBox Text="{local:Bind FirstName}" local:Bind.Error="{Binding FirstNameError}" />

You have to name Bind class more properly to distinguish from other bindings if you decide to follow my preferences.

Few words about implementation. Notice validation rule constructor, it's needed to specify when we want to get control to validate. In this case: when raw value is changed in the view. Another thing is what we need to access binding target to retrieve attached property, this is why overload with BindingExpression is used and another one (required when inheriting from ValidationRule) simply returns.

red boarder and tool tip preferred

Default Validation.ErrorTemplate will provide red border and you can add tooltip easily:

ToolTip="{Binding RelativeSource={RelativeSource self}, Path=(Validation.Errors)[0].ErrorContent}"
Sinatr
  • 20,892
  • 15
  • 90
  • 319
-3

Hope this will help you

<p>FirstName <span style="color:red;">*</span></p>
@Html.TextBoxFor(model => model.FirstName, htmlAttributes: new { maxlength = "100", autocomplete = "off" })
@Html.ValidationMessageFor(model => model.FirstName)