0

I am trying to create a re-usable user control (for data entry) in which there are two text boxes and they are linked to each by an IValueConvertor.

The following XAML is the original, normal code. This is what I am trying to reproduce in a user control.

<WrapPanel>
    <TextBlock Text="Length of Fence"/>
    <TextBox Name="Metric" Width="50" Text="{Binding Path=LengthFence, Mode=TwoWay}"/>
    <TextBlock Text="Meters"/>
    <TextBox Text="{Binding ElementName=Metric, Path=Text, Converter={StaticResource MetersToInches}, StringFormat=N8}"/>
    <TextBlock Text="Inches"/>
</WrapPanel>

and the code-behind for the IValueConvertor (in MainWindow.xaml) is

    public class MetersToInches : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value.ToString() == "")
                return 0.0;
            try
            {
                double meters = System.Convert.ToDouble(value);
                var result = meters * 39.3701;
                return result;
            }
            catch
            {
                // Catch errors when users type invalid expressions.
                return 0.0;
            }

        }

        public object ConvertBack(object value, Type targettype, object parameter, CultureInfo culture)
        {
            if (value.ToString() == "")
                return 0.0;
            try
            {
                double inches = System.Convert.ToDouble(value);
                var result = inches * 0.0254;
                return result;
            }
            catch
            {
                // Catch errors when users type invalid expressions.
                return 0.0;
            }
        }

    }

This is what this XAML looks like: Original Input field without any re-usable user control

Now I have made a re-usable UserControl with three dependency properties Label for label string, Value for binding a property inside the ViewModel, and Units - a string property to show the input units.

<UserControl ...
             x:Name="parent">
    <StackPanel DataContext="{Binding ElementName=parent}">
        <TextBlock Text="{Binding Path=Label}"/>
        <TextBox Text="{Binding Path=Value}"/>
        <TextBlock Text="{Binding Path=Units}"/>
    </StackPanel>

However, this re-usable control can only tackle the first TextBox of the input. I do not know how to bind the IValueConvertor in the second TextBox. I need to do this because I want to bind other converters such as meters to feet, kg to pound, etc.

I have read that ConvertorParameter cannot be bound because it is not a dependency property and I am not sure if I can use multi-binding, mostly because I do not know how to use it properly Binding ConverterParameter.

I would be very grateful if you could show me how to do this or direct me to the appropriate link on StackOverflow or elsewhere that solves this problem. Or if there is a better way of doing this.

Many many thanks in advance.

Keith Stein
  • 6,235
  • 4
  • 17
  • 36
moostang
  • 73
  • 7
  • Instead of keeping the converters in the mainwindow.xaml code behind, keep them in a seperate .cs file. Your point 6 is correct, you cannot bind to converter parameter and multibinding is the answer. – Lingam Jun 13 '20 at 21:30
  • Once again, Thanks. I have moved them to a separate class. – moostang Jun 14 '20 at 19:00

2 Answers2

1

First, don't bind the TextBoxes to each other (as in your original code at the begining of the question), instead, bind each TextBox to the same backing property, which, in your UserControl, is Value.

As for how to implement multiple bindings, you probably don't need a MultiBinding.

We have to pick a "standard" unit of measure to begin with- this will be the unit that will be actually stored in the property and in any database or file. I'll assume this standard unit will be meters (m). An IValueConverter can be used to convert between meters and some other unit of distance and back, using the ConverterParameter to specify which other unit to convert to/from.

Here's a good example to get you started.

public enum DistanceUnit { Meter, Foot, Inch, }

public class DistanceUnitConverter : IValueConverter
{
    private static Dictionary<DistanceUnit, double> conversions = new Dictionary<DistanceUnit, double>
    {
        { DistanceUnit.Meter, 1 },
        { DistanceUnit.Foot, 3.28084 },
        { DistanceUnit.Inch, 39.37008 }
    };

    //Converts a meter into another unit
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return conversions[(DistanceUnit)parameter] * (double)value;
    }

    //Converts some unit into a meter 
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) { return 0; }

        double v;

        var s = value as string;

        if (s == null)
        {
            v = (double)value;
        }
        else
        {
            if (s == string.Empty) { return 0; }
            v = double.Parse(s);
        }

        if (v == 0) { return 0; }

        return v / conversions[((DistanceUnit)parameter)];
    }
}

The above has a few problems. I never check if parameter really is a DistanceUnit before using it, for example. But it works.

Here's an example of how I used it:

<StackPanel>
    <StackPanel.Resources>
        <local:DistanceUnitConverter x:Key="DistCon"/>
    </StackPanel.Resources>

    <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Meter}}" MinWidth="20"/>
        <TextBlock>m</TextBlock>
    </StackPanel>

    <StackPanel Orientation="Horizontal">
        <TextBox Text="{Binding Distance, Converter={StaticResource DistCon}, ConverterParameter={x:Static local:DistanceUnit.Foot}}" MinWidth="20"/>
        <TextBlock>ft</TextBlock>
    </StackPanel>
</StackPanel>

The DistanceUnit enum and the internal conversions dictionary can be expanded with more units of measure. Alternatively, you can use a 3rd party library that already has all these included, like UnitsNet.

Keith Stein
  • 6,235
  • 4
  • 17
  • 36
  • Hi Keith, Thanks for the quick and detailed reply. This is awesome! Everyday I am learning new things and thanks for teaching me a new trick. This is great and greatly optimizes my code. Thanks for sending a link to UnitsNet as well. That is always helpful. – moostang Jun 14 '20 at 18:55
0

Not sure how you would like to bind mulitple converters in one single control. If i'm not wrong, you would like to build a control where when a user enters a particular value, you need to display it in different units. If this is the case, you can create a single converter with converterparameter as "m","cm","inch" etc and based on this you can return the result. Then in this case, you will have 4,5 controls and each will have same converter binding but different converter values. If this is not clear and you need further direction, please let know.

Multi Value binding

To answer your point 6, please see a sample multi binding converter and its implementation in xaml below. I have built a simple RolesFilter which will take different inputs from the xaml as object[] and since I already know what data is expected, i'm converting them in the converter.

public class RolesFilter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            FlipperObservableCollection<Role> _roles = (FlipperObservableCollection<Role>)values[0]; //Input
            Department _dept_param = values[1] as Department;
            bool _filter = (bool)values[2];
            string _id = "NA";
            if (values.Count() == 4 && values[3] is string) _id = (string)values[3] ?? "NA";

            //If we need a filter, then without department, it should return empty results
            if (!_filter) return _roles; //If no filter is required, then don't worry, go ahead with input values.
            if (_dept_param == null) return new FlipperObservableCollection<Role>(); //If department is null, then 
            List<Role> _filtered_list = _roles.ToList().Where(p => p.department.id == _dept_param.id && p.id != _id)?.ToList() ?? new List<Role>();
            return new FlipperObservableCollection<Role>(_filtered_list);
        }
        catch (Exception)
        {
            throw;
        }
    }

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

I'm using the multi value converter in the xaml as below. Here, i'm filtering an itemsource of a combo box based on another combobox and a check box. This is just an example and in your case, you can create a combo box with different Units values. Based on user selection, you can use the converter and return value to the textbox.

<ComboBox Height="30" SelectedItem="{Binding reports_to, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}">
                        <ComboBox.ItemsSource>
                            <MultiBinding Converter="{StaticResource roles_filter}">
                                <Binding Source="{StaticResource SingletonData__}" Path="roles" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
                                <Binding Path="department" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
                                <Binding ElementName="cbx_filter" Path="IsChecked"/>
                                <Binding Path="id" NotifyOnSourceUpdated="True" UpdateSourceTrigger="PropertyChanged"/>
                            </MultiBinding>
                        </ComboBox.ItemsSource>
                        <ComboBox.ItemTemplate>
                            <DataTemplate>
                                <WrapPanel>
                                    <TextBlock Text="{Binding department.name}"/>
                                    <TextBlock Text=" - "/>
                                    <TextBlock Text="{Binding name}"/>
                                </WrapPanel>
                            </DataTemplate>
                        </ComboBox.ItemTemplate>
                    </ComboBox>
Lingam
  • 609
  • 5
  • 16
  • Hi Lingam, Thank you for your answer. You have shown me a different approach. The thought of using a comboBox never crossed my mind. This is great. I like the comboBox approach as well. I definitely need to dig in more deeper on multi-binding. Thanks. – moostang Jun 14 '20 at 18:58