How can I create a WPF TextBox that conditionally converts user input?

1.2k views Asked by At

I want to create a TextBox that can take measurement and convert it to different units if necessary (the end result being of type double). The conversion will be controlled by a value IsMetric. If IsMetric == true then "36.5 in" would turn into 927.1 (a double representing millimeters). Conversely, if IsMetric == false then "927.1 mm" would turn into 36.5.

I thought to use an IValueConverter on a regular TextBox, but the ConverterParameter is not a DependencyProperty and therefore I can't bind IsMetric to it.

I tried IMultiValueConverter but the ConvertBack function only receives the current value of the TextBox and not all the bound values. This means I don't know IsMetric when converting the user input.

Have I missed something with the ConvertBack function? If not, then do I need to create a class derived from TextBox?

3

There are 3 answers

0
Ben Zuill-Smith On

I ended up with something along these lines for now. Would still enjoy a solution that doesn't require a DataTrigger for every possible value.

It's a bit different than the answer posted by @SamTheDev but along the same lines.

xaml

<UserControl x:Class="MyNamespace.Controls.MeasurementTextBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:c="clr-namespace:MyNamespace.Converters"
             xmlns:b="clr-namespace:MyNamespace.Behaviors"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d"
             x:Name="root">
    <UserControl.Resources>
        <c:MeasurementUnitConverter x:Key="muc"/>
        <c:MeasurementConverter2 x:Key="mc"/>
        <sys:Boolean x:Key="BooleanFalse">False</sys:Boolean>
        <sys:Boolean x:Key="BooleanTrue">True</sys:Boolean>
    </UserControl.Resources>
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="30"/>
        </Grid.ColumnDefinitions>
        <TextBox Margin="0" VerticalContentAlignment="Center" HorizontalAlignment="Stretch" HorizontalContentAlignment="Right" VerticalAlignment="Stretch"
                 b:AutoSelectBehavior.AutoSelect="True">
            <TextBox.Style>
                <Style TargetType="{x:Type TextBox}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding UseMetric, ElementName=root}" Value="True">
                            <Setter Property="Text" Value="{Binding Measurement, Mode=TwoWay, ElementName=root, Converter={StaticResource mc}, ConverterParameter={StaticResource BooleanTrue}}"></Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding UseMetric, ElementName=root}" Value="False">
                            <Setter Property="Text" Value="{Binding Measurement, Mode=TwoWay, ElementName=root, Converter={StaticResource mc}, ConverterParameter={StaticResource BooleanFalse}}"></Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>
        <!-- in or mm label -->
        <Label VerticalAlignment="Center" Padding="0" Margin="5" HorizontalAlignment="Left" Grid.Column="1"
                Content="{Binding UseMetric, ElementName=root, Converter={StaticResource muc}}"/>
    </Grid>
</UserControl>

xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyNamespace.Controls
{
    /// <summary>
    /// Interaction logic for MeasurementTextBox.xaml
    /// </summary>
    public partial class MeasurementTextBox : UserControl
    {
        public MeasurementTextBox()
        {
            // This call is required by the designer.
            InitializeComponent();
        }

        public bool UseMetric {
            get { return Convert.ToBoolean(GetValue(UseMetricProperty)); }
            set { SetValue(UseMetricProperty, value); }
        }


        public static readonly DependencyProperty UseMetricProperty = DependencyProperty.Register("UseMetric", typeof(bool), typeof(MeasurementTextBox), new PropertyMetadata(MeasurementTextBox.UseMetricChanged));
        private static void UseMetricChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }

        public double Measurement {
            get { return (double)GetValue(MeasurementProperty); }
            set { SetValue(MeasurementProperty, value); }
        }


        public static readonly DependencyProperty MeasurementProperty = DependencyProperty.Register("Measurement", typeof(double), typeof(MeasurementTextBox), new PropertyMetadata(MeasurementTextBox.MeasurementPropertyChanged));
        private static void MeasurementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }
    }
}

Converter

using System;
using System.Windows;
using System.Windows.Data;

namespace MyNamespace.Converters
{
    class MeasurementConverter : IValueConverter
    {

        const double MILLIMETERS_IN_ONE_INCH = 25.4;
        const string INCHES_ABBREVIATION = "in";
        const string MILLIMETERS_ABBREVIATION = "mm";

        const double ONE_THIRTY_SECOND = 0.03125;
        const double ONE_SIXTEENTH = 0.0625;
        const double ONE_EIGHTH = 0.125;
        const double ONE_FOURTH = 0.25;
        const double ONE_HALF = 0.5;

        const double ONE = 1;
        public double RoundToNearest(double value, int unitPrecision)
        {
            double fraction = 0;
            int reciprocal = 0;

            switch (unitPrecision)
            {
                case 0:
                    fraction = ONE;
                    reciprocal = (int)ONE;
                    break;
                case 1:
                    fraction = ONE;
                    reciprocal = (int)ONE;
                    break;
                case 2:
                    fraction = ONE_HALF;
                    reciprocal = (int)(1 / ONE_HALF);
                    break;
                case 3:
                    fraction = ONE_FOURTH;
                    reciprocal = (int)(1 / ONE_FOURTH);
                    break;
                case 4:
                    fraction = ONE_EIGHTH;
                    reciprocal = (int)(1 / ONE_EIGHTH);
                    break;
                case 5:
                    fraction = ONE_SIXTEENTH;
                    reciprocal = (int)(1 / ONE_SIXTEENTH);
                    break;
                case 6:
                    fraction = ONE_THIRTY_SECOND;
                    reciprocal = (int)(1 / ONE_THIRTY_SECOND);
                    break;
            }

            return Math.Round(value * reciprocal, MidpointRounding.AwayFromZero) * fraction;

        }

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            string strValue = (string)value;
            bool isMetric = (bool)parameter;

            double enteredValue = 0;
            bool enteredValueIsImperial = false;

            if (strValue.EndsWith(INCHES_ABBREVIATION))
            {
                enteredValueIsImperial = true;
                strValue = strValue.Substring(0, strValue.Length - INCHES_ABBREVIATION.Length);
            }
            else if (strValue.EndsWith(MILLIMETERS_ABBREVIATION))
            {
                enteredValueIsImperial = false;
                strValue = strValue.Substring(0, strValue.Length - MILLIMETERS_ABBREVIATION.Length);
            }
            else if (isMetric)
            {
                enteredValueIsImperial = false;
            }
            else
            {
                enteredValueIsImperial = true;
            }

            try
            {
                enteredValue = double.Parse(strValue);
            }
            catch (FormatException)
            {
                return DependencyProperty.UnsetValue;
            }

            if (isMetric)
            {
                if (enteredValueIsImperial)
                {
                    //inches to mm
                    return RoundToNearest(enteredValue * MILLIMETERS_IN_ONE_INCH, 0);
                    //0 is mm
                }
                else
                {
                    //mm to mm
                    return RoundToNearest(enteredValue, 0);
                    //0 is mm
                }
            }
            else
            {
                if (enteredValueIsImperial)
                {
                    //inches to inches
                    return RoundToNearest(enteredValue, 5);
                }
                else
                {
                    //mm to inches
                    return RoundToNearest(enteredValue / MILLIMETERS_IN_ONE_INCH, 5);
                }
            }
        }
    }
}

Usage:

<mynamespace:MeasurementTextBox Measurement="{Binding SomeLength, Mode=TwoWay}"
                                UseMetric="{Binding IsMetric}"/>
0
Ben Cohen On

If thats the only thing you want to do, try other way to use converter parameter. But, and i would have choose this option - if your textbox has more logics in it, or tend to have more dependecie - Create custom control that inherits from textbox, and add your own dependecy properties. Then you can use your IsMetric and convert it as you want on propertychanged etc.

2
SamTh3D3v On

You could use two converters one to convert from Metric and another to Metric:

public class ToMetricConverter:IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return "(metric) value";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}
public class FromMetricConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return "(Inch) value";
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And use a DataTrigger in the UI to select the appropriate converter based on that bool value:

<Window.Resources>
    <wpfApplication13:ToMetricConverter x:Key="ToMetricConverter"/>
    <wpfApplication13:FromMetricConverter x:Key="FromMetricConverter"/>
</Window.Resources>
<Grid>
    <StackPanel>    
        <CheckBox IsChecked="{Binding IsMetric,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></CheckBox>
        <TextBox >
            <TextBox.Style>
                <Style TargetType="TextBox">                        
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding IsMetric,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Value="True">
                            <Setter Property="Text" Value="{Binding Val,Converter={StaticResource ToMetricConverter}}"></Setter>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding IsMetric,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" Value="False">
                            <Setter Property="Text" Value="{Binding Val,Converter={StaticResource FromMetricConverter}}"></Setter>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>
    </StackPanel>
</Grid>