Search code examples
wpfdata-bindingconverters

How can I read two values from TextBox separated by some separator using Converter and then bind them to two different properties?


I'm currently working on an interpolation project. I need the user to enter the interpolation boundaries (basically, 2 double values) in one TextBox separated by some separator.

In my MainWindow.xaml.cs I have created a class ViewData whose fields are controls in the user interface. And assigned my DataContext to it. Like this:

 public partial class MainWindow : Window
    {
        ViewData viewData = new();
        public MainWindow()
        {
            InitializeComponent();
            DataContext = viewData;
        }

    }

In particular, this class has two fields of type double: boundA and boundB. I'd like to be able to take users input from TextBox and bind first value to boundA, second one to boundB. My ViewData class:

using System;
using System.Collections.Generic;
using System.Windows;
using CLS_lib;

namespace Splines
{
    public class ViewData
    {
        /* RawData Binding */
        public double boundA {get; set;}
        public double boundB {get; set;}
        public int nodeQnt {get; set;}
        public bool uniform {get; set;}
        public List<FRaw> listFRaw { get; set; }
        public FRaw fRaw { get; set; }
        public RawData? rawData {get; set;}
        public SplineData? splineData {get; set;}

        /* --------------- */
        /* SplineData Binding */
        public int nGrid {get; set;}
        public double leftDer {get; set;}
        public double rightDer {get; set;}
        /* ------------------ */
        public ViewData() {
            boundA = 0;
            boundB = 10;
            nodeQnt = 15;
            uniform = false;
            nGrid = 20;
            leftDer = 0;
            rightDer = 0;
            listFRaw = new List<FRaw>
            {
                RawData.Linear,     
                RawData.RandomInit,  
                RawData.Polynomial3
            };       
            fRaw = listFRaw[0];
        }
        public void ExecuteSplines() {
            try {
                rawData = new RawData(boundA, boundB, nodeQnt, uniform, fRaw);
                splineData = new SplineData(rawData, nGrid, leftDer, rightDer);
                splineData.DoSplines();
            } 
            catch(Exception ex) {
                MessageBox.Show(ex.Message);
            }
        }

        public override string ToString()
        {
            return $"leftEnd = {boundA}\n" +
                   $"nRawNodes = {nodeQnt}\n" +
                   $"fRaw = {fRaw.Method.Name}" +
                   $"\n"; 
        }
    }
}

UPDATE I've tried using IMultiValueConverter + MultiBinding but I failed at making it work :( Here's my MainWindow.xaml:

<userControls:ClearableTextBox x:Name="bounds_tb" Width="250" Height="40" Placeholder="Nodes amount">
    <userControls:ClearableTextBox.Text>
        <MultiBinding Converter="{StaticResource boundConverter_key}" UpdateSourceTrigger="LostFocus" Mode="OneWayToSource">
            <Binding Path="boundA"/>
            <Binding Path="boundB"/>
        </MultiBinding>
    </userControls:ClearableTextBox.Text></userControls:ClearableTextBox>

And my Converter:

    public class BoundariesMultiConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            string boundaries;
            boundaries = values[0] + ";" + values[1];
            return boundaries;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            string[] splitValues = ((string)value).Split(';');
            return splitValues;
        }
    }

Solution

  • The first solution shows how to bind the TextBox to a string property and split the value in the data source (the ViewData class).
    The second solution binds the TextBox directly to both properties of type double. A IMultivalueConverter imlementation will first split the values and then convert them from string to double.

    It is recommended to implement data validation using the INotifyDataErrorInfo interface. This is pretty simple and allows you to give the user feedback when he enters invalid input (for example alphabetic input or invalid number of arguments or wrong separator). The error feedback usually is a red border around the input field and an error message that guides the user to fix the input.
    You can follow this example to learn how to implement the interface: How to add validation to view model properties or how to implement INotifyDataErrorInfo.

    Because of fragile nature of an expected text input that is subject to strict rules and constraints (e.g. input must be numeric, must use a particular set of delimiters, must contain only two values etc.), it is highly recommended to implement data validation.

    In terms of data validation, the first solution is recommended (split and convert and validate values in the data source).

    Solution 1

    Split the string in the binding source and convert it to double values.
    This solution is best suited for data validation.

    ViewData.cs

    class ViewData : INotifyPropertyChanged
    {
      public event PropertyChangedEventHandler? PropertyChanged;
    
      private string interpolationBoundsText;
      public string InterpolationBoundsText
      {
        get => this.interpolationBoundsText;
        set 
        { 
          this.interpolationBoundsText = value;
          OnPropertyChanged();
    
          // Split the input and update the lower 
          // and upper bound properties
          OnInterpolationBoundsTextChanged();
        }
      }
    
      private double lowerInterpolationBound;
      public double LowerInterpolationBound
      {
        get => this.lowerInterpolationBound;
        set
        {
          this.lowerInterpolationBound = value;
          OnPropertyChanged();
        }
      }
    
      private double upperInterpolationBound;
      public double UpperInterpolationBound
      {
        get => this.upperInterpolationBound;
        set
        {
          this.upperInterpolationBound = value;
          OnPropertyChanged();
        }
      }
    
      private void OnInterpolationBoundsTextChanged()
      {
        string[] bounds = this.InterpolationBoundsText.Split(new[] {';', ',', ' ', '/', '-'}, StringSplitOptions.RemoveEmptyEntries);
        if (bounds.Length > 2)
        {
          throw new ArgumentException("Found more than two values.", nameof(this.InterpolationBoundsText));
        }
    
        // You should implement data validation at this point to give
        // the user error feedback to allow him to correct the input.
        if (double.TryParse(bounds.First(), out double lowerBoundValue))
        {
          this.LowerInterpolationBound = lowerBoundValue;
        }
    
        // You should implement data validation at this point to give
        // the user error feedback to allow him to correct the input.
        if (double.TryParse(bounds.Last(), out double upperBoundValue))
        {
          this.LowerInterpolationBound = upperBoundValue;
        }
      }
    
      private void OnPropertyChanged([CallerMemberName] string propertyName = null)
        => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    

    MainWindow.xaml The DataContext is expected to of type ViewData.

    <Window>
      <ClearableTextBox Text="{Binding InterpolationBoundsText}" />
    </Window>
    

    Solution 2

    Example shows how to use a MultiBinding to split the input using a converter.
    The example is based on the above version of the ViewData class.

    InpolationBoundsInputConverter.cs

    class InpolationBoundsInputConverter : IMultiValueConverter
    {
      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) => values
        .Cast<double>()
        .Aggregate(
          string.Empty, 
          (newStringValue, doubleValue) => $"{newStringValue};{doubleValue}", 
          result => result.Trim(';'));
    
      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => ((string)value)
        .Split(new[] { ';', ',', ' ', '/', '-' }, StringSplitOptions.RemoveEmptyEntries)
        .Select(textValue => (object)double.Parse(textValue))
        .ToArray();
    }
    

    MainWindow.xaml.cs
    The DataContext is expected to be of type ViewData.

    <Window>
      <ClearableTextBox>
        <ClearableTextBox.Text>
          <MultiBinding UpdateSourceTrigger="LostFocus">
            <MultiBinding.Converter>
              <InpolationBoundsInputConverter />
            </MultiBinding.Converter>
    
            <Binding Path="LowerInterpolationBound" />
            <Binding Path="UpperInterpolationBound" />
          </MultiBinding>
        </ClearableTextBox.Text>
      </ClearableTextBox>
    </Window>