0

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;
        }
    }
neijs
  • 3
  • 3
  • You should use a MultiBinding and implement IMultiValueConverter. – BionicCode Apr 10 '23 at 15:09
  • could you show your wpf source code ans show the property you want to bind? its not clear – Frenchy Apr 11 '23 at 09:58
  • You can also always bind to a single property and split/process the value after receiving it in the binding source. The single property could be the plain string or a collection of strings (which your converter would return). – BionicCode Apr 12 '23 at 06:27
  • @BionicCode Can you show how it's done? Assuming that I bind TextBox to some string (which I than split), how should I bind these 2 strings to ViewData fields? – neijs Apr 17 '23 at 21:09
  • Yes, I can give you an example. Does `ViewData` class implement `INotifyPropertyChanged` and do all properties that serve as a binding source (for example the properties `boundA` and `boundB`) raise the `PropertyChanged` event? This is crucial. Otherwise your code won't work. – BionicCode Apr 17 '23 at 21:31

3 Answers3

0

The proposed solution is based on a UserControl that is doing the work in its code behind. Lets start with how we use the UserControl called "TextBoxTwoDoubles" :

<local:TextBoxTwoDoubles
            NumberA="{Binding BoundA,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            NumberB="{Binding BoundB,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
            />

The UserControl XAML code is as following:

<UserControl x:Class="Problem30TwoDoubles.TextBoxTwoDoubles"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:Problem30TwoDoubles"
             mc:Ignorable="d"  Name="Parent"
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <Grid>
            <TextBox Text="{Binding ElementName=Parent,Path=Text,UpdateSourceTrigger=PropertyChanged,Mode=TwoWay}"   />
        </Grid>
    </Grid>
</UserControl>

The code behind is longer. It defines three Dependency Properties: "Text" which is the raw text entered. It defines also the "NumberA" and "NumberB" dependency Properties. The entered text is splitted to 2 numbers . Each of the numbers goes thru validation and can go thru enhancements. This is the code:

public partial class TextBoxTwoDoubles : UserControl
    {
        public TextBoxTwoDoubles()
        {
            InitializeComponent();
        }
        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set
            {

                SetValue(TextProperty, value);
                SetValue(TextProperty, value);
            }
        }
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof(string), typeof(TextBoxTwoDoubles),
              new PropertyMetadata(string.Empty, new PropertyChangedCallback(TextPropertyChanged)));



        public double NumberA
        {
            get { return (double)GetValue(NumberAProperty); }
            set
            {
                SetValue(NumberAProperty, value);
            }
        }
        public static readonly DependencyProperty NumberAProperty =
           DependencyProperty.Register("NumberA", typeof(double), typeof(TextBoxTwoDoubles),
             new PropertyMetadata(double.NaN, new PropertyChangedCallback(NumberAPropertyChanged)));

        public double NumberB
        {
            get { return (double)GetValue(NumberBProperty); }
            set
            {
                SetValue(NumberBProperty, value);
            }
        }
        public static readonly DependencyProperty NumberBProperty =
           DependencyProperty.Register("NumberB", typeof(double), typeof(TextBoxTwoDoubles),
             new PropertyMetadata(double.NaN, new PropertyChangedCallback(NumberBPropertyChanged)));

        private static void TextPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {

            TextBoxTwoDoubles ours = (TextBoxTwoDoubles)obj;
            if (e.NewValue == e.OldValue) return;
            string[] splitted = (e.NewValue as string).Split();
            if (splitted.Length!= 2)
            {
                ours.SetValue(TextProperty, ours.NumberA.ToString() + " " + ours.NumberB.ToString());
                return;
            }
            string stringA = splitted[0];
            string stringB = splitted[1];
            double _tempA;
            double _tempB;
            string examinedA = NormalizeToDouble(stringA);
            string examinedB = NormalizeToDouble(stringB);
            string toBexamined = (string)e.NewValue;
           
            if (!(double.TryParse(examinedA, out _tempA) && double.TryParse(examinedB, out _tempB)) )
            {
                ours.SetValue(TextProperty, ours.NumberA.ToString() + " " + ours.NumberB.ToString());
            }
            else
            {
                ours.SetValue(NumberAProperty, _tempA);
                ours.SetValue(NumberBProperty, _tempB);
            }
        }
        private static  string NormalizeToDouble(string text)
        {
            string toBeExamined = text;
            if (!toBeExamined.Contains("."))
            {
                toBeExamined += ".0";
            }
            if (toBeExamined.EndsWith("."))
            {
                toBeExamined += "0";
            }
            return toBeExamined;
        }
        private static void NumberAPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            TextBoxTwoDoubles ours = (TextBoxTwoDoubles)obj;
            if (e.NewValue == e.OldValue) return;
            ours.SetValue(TextProperty, ours.NumberA.ToString() + " " + ours.NumberB.ToString());
        }
        private static void NumberBPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            TextBoxTwoDoubles ours = (TextBoxTwoDoubles)obj;
            if (e.NewValue == e.OldValue) return;
            ours.SetValue(TextProperty, ours.NumberA.ToString()+ " " + ours.NumberB.ToString());
        }
    }

I have based it on another solution I have done few weeks ago that handled a single double parsing. This can be helpful if one wants to see a more simple solution first Hi I want validate textbox WPF MVVM pattern to allow number with decimal value

Gilad Waisel
  • 120
  • 4
0

As of the question update I am adding a solution for the converter that is not working. The ConvertBack should return the values as doubles:

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            string[] splitValues = ((string)value).Split(';');
            double doubleA=0.0;
            double doubleB=0.0;
            if (splitValues.Length == 2)
            {
                double.TryParse(splitValues[0], out doubleA);
                double.TryParse(splitValues[1], out doubleB);
            }
            object[] doubles =  { doubleA, doubleB };
            return doubles;
        }
Gilad Waisel
  • 120
  • 4
0

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>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Thank you a lot! Yes, my ViewData does implement INotifyPropertyChanged. But I have to specify Mode to be OneWayToSource, otherwise it won’t work. Maybe you can recommend some book or other material so that I don’t continue to study blindly? – neijs Apr 18 '23 at 07:00
  • I generally recommend to read the [Microsoft WPF documentation](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/?view=netdesktop-7.0). It gives you a good fundamental knowledge about WPF. Each article provides many links that allow to dive deeper into the topic. You will gain very deep knowledge if you don't shy away to follow those links. You are in full control about what degree of knowledge you want. Read the top level article and stop there or follow the cross-references to get deeper. The documentation is premium. – BionicCode Apr 18 '23 at 08:01
  • [Data binding overview (WPF .NET)](https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/?view=netdesktop-7.0&viewFallbackFrom=netdesktop-5.0) is a good example of the content found in the documentation. In terms of programming and software development I recommend to learn the SOLID principles and to lookup the term Clean Code (based on a book by Robert Martin - highly recommended read). He produces also some very entertaining videos that explain the book). SOLID principles are fundamental and help you to write clean code. – BionicCode Apr 18 '23 at 08:01
  • *"I have to specify Mode to be OneWayToSource, otherwise it won’t work."* - Why do you think the code doesn't work with a TwoWay binding? Or is this a requirement? Because it don#T see any reason why it wouldn't work in your case. – BionicCode Apr 18 '23 at 08:02