11
votes

I'm trying to do something that I previously assumed would be quite easy: use the value from one control in the validation rule of another. My application has a variety of parameters that the user can enter, the specific parameters in question here define the start and end points of a range, and the user sets the values through a textbox.

The two controls in question are the start and end textboxes, and the following conditions should be checked in validation:

  1. Start value must be greater than or equal to some arbitrary value
  2. End value must be less than or equal to some arbitrary value
  3. Start value must be less than or equal to end value

The first two conditions I have already accomplished. The third is far more difficult to implement, because I cannot access the end textbox's value from the validator. Even if I could, there are five different ranges (each with their own start and end textbox) I'm trying to validate, and there must be some solution more elegant than creating a validation rule for each one.

Here is the relevant XAML code:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:validators="clr-namespace:CustomValidators"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <TextBox Name="textboxStart" Grid.Row="0">
        <TextBox.Text>
            <Binding Path="Start" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <validators:MeasurementRangeRule Min="1513" Max="1583"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>

    <TextBox Name="textboxEnd" Grid.Row="1">
        <TextBox.Text>
            <Binding Path="End" UpdateSourceTrigger="PropertyChanged">
                <Binding.ValidationRules>
                    <validators:MeasurementRangeRule Min="1513" Max="1583"/>
                </Binding.ValidationRules>
            </Binding>
        </TextBox.Text>
    </TextBox>
</Grid>

And here is the relevant C# code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.CompilerServices;
using System.ComponentModel;
using System.Globalization;

namespace WpfApplication1 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow () {
            InitializeComponent();
        }

        private decimal _start;
        private decimal _end;
        public event PropertyChangedEventHandler PropertyChanged;

        public decimal Start {
            get { return _start; }
            set {
                _start = value;
                RaisePropertyChanged();
            }
        }

        public decimal End {
            get { return _end; }
            set {
                _end = value;
                RaisePropertyChanged();
            }
        }

        private void RaisePropertyChanged ([CallerMemberName] string propertyName = "") {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

namespace CustomValidators {

    public class MeasurementRangeRule : ValidationRule {
        private decimal _min;
        private decimal _max;

        public decimal Min {
            get { return _min; }
            set { _min = value; }
        }

        public decimal Max {
            get { return _max; }
            set { _max = value; }
        }

        public override ValidationResult Validate (object value, CultureInfo cultureInfo) {
            decimal measurementParameter = 0;

            try {
                if (((string) value).Length > 0)
                    measurementParameter = Decimal.Parse((String) value);
            } catch (Exception e) {
                return new ValidationResult(false, "Illegal characters or " + e.Message);
            }

            if ((measurementParameter < Min) || (measurementParameter > Max)) {
                return new ValidationResult(false,
                  "Out of range. Enter a parameter in the range: " + Min + " - " + Max + ".");
            } else {
                return new ValidationResult(true, null);
            }
        }
    }
}

The question linked here seems to be relevant, but I cannot understand the answers provided.

Thanks...

1
Have you looked at IDataErrorInfo or a BindingGroup?James Sampica
@Jim I looked at IDataErrorInfo, restructured my code to encapsulate the relevant properties (start, end, min and max) in their own class, and then implemented IDataErrorInfo. That setup worked like a charm, so much appreciated for the pointer. I'll be posting the answer tomorrow.Tristan Latchu

1 Answers

6
votes

For any who might face this problem, it is far easier to implement IDataErrorInfo to validate errors in general, and to accomplish validation against other controls in some logical grouping. I encapsulated the relevant properties (start, end, min and max) in a single class, bound the controls to those properties, and then used the IDataErrorInfo interface for validation. Relevant code is below...

XAML:

    <TextBox Name="textboxStart" Grid.Row="0" Text="{Binding Path=Start, ValidatesOnDataErrors=True}" Margin="5"/>
    <TextBox Name="textboxEnd" Grid.Row="1" Text="{Binding Path=End, ValidatesOnDataErrors=True}" Margin="5"/>
</Grid>

C#:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.CompilerServices;
using System.ComponentModel;

namespace WpfApplication1 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow () {
            InitializeComponent();

            Parameter testParameter = new Parameter(0, 10);
            testGrid.DataContext = testParameter;
        }
    }

    public class Parameter: INotifyPropertyChanged, IDataErrorInfo {
        private decimal _start, _end, _min, _max;
        public event PropertyChangedEventHandler PropertyChanged;

        public Parameter () { }

        public Parameter (decimal min, decimal max) {
            this.Min = min;
            this.Max = max;
        }

        public decimal Start {
            get { return _start; }
            set {
                _start = value;
                //RaisePropertyChanged for both Start and End, because one may need to be marked as invalid because of the other's current setting.
                //e.g. Start > End, in which case both properties are now invalid according to the established conditions, but only the most recently changed property will be validated
                RaisePropertyChanged();
                RaisePropertyChanged("End");
            }
        }

        public decimal End {
            get { return _end; }
            set {
                _end = value;
                //RaisePropertyChanged for both Start and End, because one may need to be marked as invalid because of the other's current setting.
                //e.g. Start > End, in which case both properties are now invalid according to the established conditions, but only the most recently changed property will be validated
                RaisePropertyChanged();
                RaisePropertyChanged("Start");
            }
        }

        public decimal Min {
            get { return _min; }
            set { _min = value; }
        }

        public decimal Max {
            get { return _max; }
            set { _max = value; }
        }

        private void RaisePropertyChanged ([CallerMemberName] string propertyName = "") {
            if (PropertyChanged != null) {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public string Error {
            get { return string.Empty; }
        }

        public string this[string columnName] {
            get {
                string result = string.Empty;

                switch (columnName) {
                    case "Start":
                        if (Start < Min || Start > Max || Start > End) {
                            result = "Out of range. Enter a value in the range: " + Min + " - " + End + ".";
                        }
                        break;
                    case "End":
                        if (End < Min || End > Max || End < Start) {
                            result = "Out of range. Enter a value in the range: " + Start + " - " + Max + ".";
                        }
                        break;
                };

                return result;
            }
        }
    }
}