implementing IMultiValueConverter to convert between units
Asked Answered
P

2

7

I'm working on an invoicing system, and am using a DataGrid for item entry. I want users to be able to enter the quantities in different units (e.g. inches, feet, meters) and for that entered quantity to be converted to the item's stocking units. My first thought was to implement an IMultiValueConverter, but I've been trying since yesterday and cannot figure it out.

My Convert method works for sure, I have actually taken the body, stuck it in another method and tested its output. My program is crashing in the ConvertBack method, I'm new to C# and I've never seen anyone actually implement that method, so I'm wondering why It's forcing me to implement it (I'm guessing it's because my Quantity binding is not oneway), but I actually need the converted value to go back to the source objects Quantity property.

I don't really have a problem implementing the ConvertBack method, but the value parameter that is passed to the method is the value the user entered, and not the converted value, why is that? Should it not be the converted value, for that reason I can't really figure out how to go backwards, because I only have access to what the user is entering, and not the desired units in that method.

I realize my understanding of converters is probably way off, but if someone can help me understand where my thinkings off here, I'd appreciate that as well as any potential workarounds/solutions for my problem.

Thanks in advance!

Pages/PurchaseInvoicePage.xaml

<DataGrid x:Name="ItemsGrid" Grid.Row="2" Grid.ColumnSpan="3" PreviewKeyDown="ItemsGrid_PreviewKeyDown" ItemsSource="{Binding Items}" FontSize="11" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" RowHeaderWidth="0" GridLinesVisibility="None" CanUserResizeRows="False" CanUserResizeColumns="False" CanUserReorderColumns="False">
    <DataGrid.Columns>

        <DataGridTemplateColumn x:Name="ItemNoColumn" Width="150" Header="Item No." IsReadOnly="True">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBox Cursor="Arrow" Width="130" BorderThickness="0" Background="Transparent" IsReadOnly="True" Text="{Binding Number}" />
                        <Image Cursor="Hand" MouseDown="ItemSelectionButton_Click" Width="12" Source="/Images/Icons/SearchBlack.png" />
                    </StackPanel>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>

        <!-- question relative xaml starts here -->
        <DataGridTextColumn x:Name="QuantityColumn" Width="70" Header="Quantity">
            <DataGridTextColumn.Binding>
                <MultiBinding Converter="{StaticResource unitConverter}">
                    <Binding Path="Quantity" />
                    <Binding Path="Units" Mode="OneWay" />
                </MultiBinding>
            </DataGridTextColumn.Binding>
        </DataGridTextColumn>
        <!-- question relative xaml ends here -->

        <DataGridTextColumn x:Name="OrderColumn" Width="70" Header="Order" Binding="{Binding QuantityOrdered}" />
        <DataGridTextColumn x:Name="BackOrderColumn" Width="70" Header="B/O" Binding="{Binding QuantityBackOrdered}" />
        <DataGridTextColumn x:Name="UnitsColumn" Width="60" Header="Units" Binding="{Binding Units}" IsReadOnly="True" />
        <DataGridTextColumn x:Name="DescriptionColumn" Width="200" Header="Description" Binding="{Binding Description}" />
        <DataGridTextColumn x:Name="PriceColumn" Width="90" Header="Price" Binding="{Binding Price}" />
        <DataGridComboBoxColumn x:Name="TaxColumn" Width="50" Header="Tax" SelectedValueBinding="{Binding TaxCodeID}" DisplayMemberPath="Code" SelectedValuePath="ID" />
        <DataGridTextColumn x:Name="AmountColumn" Width="90" Header="Amount" Binding="{Binding Amount}" IsReadOnly="True" />
        <DataGridTextColumn x:Name="LinkedColumn" Width="90" Header="Linked" Binding="{Binding SalesOrderID}" />
    </DataGrid.Columns>
</DataGrid>

Fx/IMultiValueConverter.cs

public class UnitConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameters, CultureInfo culture)
    {
        double num = 0;
        // get the quantity value, and try parsing it
        string str = values[0].ToString().ToLower();
        bool parsed = double.TryParse(str, out num);

        // if it parses, no need to convert, return the value
        if (parsed)
            return num.ToString();

        // if it doesnt parse, get the last character in the value
        // this character indicates the units being entered
        // this will be either "(inches), f(eet), or m(eters) 
        string suffix = str.Substring(str.Length - 1);
        // get the value, without thhe last character
        str = str.Substring(0, str.Length - 1);
        // try parsing the value now
        parsed = double.TryParse(str, out num);

        // if it doesn't parse, the formats incorrect, return 0
        if (!parsed)
            return (0).ToString();

        // get the desired units, (the second value in my multibinding)
        string units = values[1].ToString().ToLower();

        // if either the entry suffix or the desired units are empty, just return
        // the number without converting
        if (string.IsNullOrEmpty(suffix) || string.IsNullOrEmpty(units))
            return num;

        // convert from inches to feet
        if (suffix == "\"" && units == "feet")
            return (num / 12).ToString();
        // convert from inches to meters
        else if (suffix == "\"" && units == "meters")
            return (num * 0.0254).ToString();
        // convert from feet to meters
        else if (suffix == "f" && units == "meters")
            return (num * 0.3048).ToString();
        // convert from meters to feet
        else if (suffix == "m" && units == "feet")
            return (num / 0.3048).ToString();

        // if we reachd this far, the user probably entered something random,
        // show an error and return 0
        MessageBox.Show("Failed to convert between the two units.");
        return (0).ToString();
    }

    public object[] ConvertBack(object value, Type[] targetType, object parameter, CultureInfo culture)
    {
        // now this is where my program is crashing, 
        // for some reason I need to actually implement the ConvertBack function
        // but when I try to popup the value being passed to this function,
        // it is the value the user entered e.g "20f" and not the converted value
        // should it not be the converted value? Why do I even need to implement this
        // how should i go backwards from here...
        string str = value.ToString();
        // MessageBox.Show(str);
        return new object[] { 0 }; // btw, I'm only returning one value, because the second binding is oneway

    }
Photosynthesis answered 16/8, 2011 at 13:19 Comment(0)
D
10

Funny, I implemented a unit converter using IMultiValueConverter just a few weeks ago. While I go dig it up to see if I can help you, Let me propose a theory:

Maybe you have flipped Convert and ConvertBack:

Convert: Converts the value from your model to the binding source (your data grid)

ConvertBack: Converts the input from the user to the model

So you can expect the user input to be provided in the parameters of the ConvertBack function.

Now let me see if I can find that converter...

Here is the MSDN documentation

UPDATE: So I found my converter, and the memories came rushing back. I encountered the same problem you just did: how would I pass the desired units to the convert back function. Honestly I am not 100% pleased with my solution, but I will share it with you.

Most implementation of ValueConverter that I have seen are stateless, therefore you can use them as a StaticResource for multiple bindings. I added a units field to my implementation to keep track of the units - which sacrifices its statelessness:

public class UnitConverter : IMultiValueConverter
{
    private string _units;

    public object Convert(object[ ] values, Type targetType, object parameter, CultureInfo culture)
    {
         //Initialize
         _units = values[1];
         //Convert
    }

    public object Convert(object[ ] values, Type targetType, object parameter, CultureInfo culture)
    {
         //Use _units to convert back
    }
}

Unfortunately, you have to create a converter for every binding that you use:

<DataGridTextColumn.Binding>
   <MultiBinding>
      <Binding Path="Quantity" />
      <Binding Path="Units" Mode="OneWay" />
      <MultiBinding.Converter>
         <yourNameSpace:UnitConverter/>
      </MultiBinding.Converter>
   </MultiBinding>
</DataGridTextColumn.Binding>

I would love to see a better solution myself to the problem, but this has worked for me.

Disapproval answered 16/8, 2011 at 14:14 Comment(4)
Ahh right, that makes sense now. I had it flipped around, umm, now how would I pass the desired units to the convert back function. Maybe you implementation will shed some light for me, thanks for the help!Photosynthesis
Umm, not a bad idea, solves my problem for now, so I'm happy :) Will let you know if I get a revelation an come up with a better solution. Thanks again.Photosynthesis
AFAIK, WPF doesn't have an elegant way to deal with this. The above is not a bad example, provided it is feasible. Though, in this particular example using IValueConverter and setting the units via the ConverterParameter is probably better. If the conversion depends on another source binding though, you can't do it either the above or using ConverterParameter (since that property isn't a dependency property). Yet another inelegant alternative is to set ConverterParameter={x:Reference someObject} where someObject is the object with the source property you want to use.Interatomic
Came here after literally having the exact same problem.Lint
D
6

I came across this post while trying to debug my own units converter which used a similar technique to Gene C's for "remembering" what the units were in order to convert back. In my case, I am converting angles (RADIANS, DEGREES) and distances (NATIVE_DIST,MM,INCHES). Things worked fine most of the time but other times the conversion would be totally wrong.

It turns out that value converters are shared by default. That means the "_units" in Gene's example will be set to whatever the units was used by the last call to "Convert". In my case, if I converted a distance from NATIVE_DIST->MM (remembering that the original units were "NATIVE_DIST") and then immediately called "ConvertBack" for a another control associated with an angle, the "remembered" units would be "NATIVE_DIST" instead of the angle units I'd intended.

The solution consisted of changing the .xaml declaration for the converter:

Change this

<local:UnitsConverter x:Key="UnitsConverter"/>

to this

<local:UnitsConverter x:Key="UnitsConverter" x:Shared="false"/>
Degression answered 6/2, 2012 at 15:44 Comment(1)
Yes, yes, yes, yes... thanks a lotOkeefe

© 2022 - 2024 — McMap. All rights reserved.