1
votes

I'm utilizing a DataGridTextColumn bound to a data object to display various measurements in an application. I'm trying to implement a feature that allows the measurements to be displayed in different units, and thought that a Converter in the binding might be my best option.

This works well for most of the application, but I have another field in the DataGrid where the user is able to edit the value of the field. In this case, I have a two way data binding that converts the inputted value from the user (let's say in Fahrenheit) back into the default units (Celsius) for the data object.

I would expect the Converter to retrieve the value in Celcius from the data object, do the conversion, and then send this value (in Fahrenheit) to the DataGridTextColumn. However, after setting the value on the data object and notifying PropertyChanged, the 'get' method for that property isn't being called. Instead, Convert seems to be getting the user inputted value (in Fahrenheit) from the DataGridTextColumn.

Is this normal behavior, and if so, is there a way to force Convert to always go back to the data object for its value?

A few snippets to help:

<Window.Resources>
    <myProject:ConvertMetricValuesToEnglish x:Key="ValueMToE" />
</Window.Resources>
.
.
.
<DataGridTextColumn x:Name="maxCol" Header="Maximum" Width="100">
    <DataGridTextColumn.Binding>
        <MultiBinding Mode="TwoWay" StringFormat='0.#####' Converter="{StaticResource ValueMToE}" >
            <Binding Path="UserMax" Mode="TwoWay" />
            <Binding Path="Unit" Mode="OneWay" />
        </MultiBinding>
    </DataGridTextColumn.Binding>
</DataGridTextColumn>

Converter methods:

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    if (values.Length != 2)
    {
        return null;
    }
    if (values[0].GetType() != typeof(double) || values[1].GetType() != typeof(string))
    {
        return values[0];
    }
    double d = (double)values[0];
    string s = (string)values[1];

    return ConvertValueToEnglish(d, s);
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
    return new object[] { StringToDouble((string)value), Binding.DoNothing };
}

I do the 'ConvertBack' conversion in the setter because of the limited info available in ConvertBack:

    private double? _userMax;
    public double UserMax
    {
        get
        {
            if (_userMax == null)
            {
                _userMax = Max;
            }

            return (double)_userMax;
        }
        set
        {
            if (Metric)
            {
                if (value == _userMax)
                {
                    return;
                }
                _userMax = value;
            }
            else
            {
                double num = ConvertValueToMetric(value, _unit);
                if (num == _userMax)
                {
                    return;
                }
                _userMax = num;
            }
            OnPropertyChanged("UserMax");
        }
    }

Edit: Here's a link to a sample project that demonstrates the problem. The left column is user editable, and represents the data as seen/edited by the user. The right column displays the underlying value which is actually stored when the user makes a change (this value shouldn't ever change).

If you modify a value in the left column, and 'Tab' over or select a different column in that same row, you can see the value change from what you entered. But it changes back when the row loses focus. By setting breakpoints, you can see that when the cell itself loses focus, the Convert method is called with DependencyProperty.UnsetValue's, then the get method for Unit, followed by the Convert method again with the value from the DataGrid.

TempConverter.zip

1
I put your code together and it is a bit confusing. It doesnt compile as is. You have a ConvertValueToMetric, _unit, Metric, Max, ConvertValueToEnglish and StringToDouble properties/methods that are undefined. Could I get a bit better code example? or help me put these items together?crthompson
@paqogomez - I uploaded a sample project to demonstrate the problem I'm having.aswanson
I'm running the project now. You are looking to have the right column reflect the changes in the left column?crthompson
Perhaps now that i see what you are doing, you could summarize the problem a little better?crthompson

1 Answers

2
votes

I was able to work out a solution on my own. The problem was that the source data object wasn't being updated until the entire row in the DataGrid lost focus. To fix this, I applied a TextBox as a ControlTemplate to the cell, and set up an event to trigger UpdateSource whenever the TextBox lost focus.

XAML:

<DataGridTextColumn x:Name="maxCol" Header="Maximum" Width="100">
    <DataGridTextColumn.CellStyle>
        <Style TargetType="DataGridCell">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <TextBox LostFocus="TextBlock_LostFocus">
                            <TextBox.Text>
                                <MultiBinding StringFormat='0.#####' Converter="{StaticResource ValueMToE}" UpdateSourceTrigger="Explicit">
                                    <Binding Path="UserMax" Mode="TwoWay" />
                                    <Binding Path="Unit" Mode="OneWay" />
                                </MultiBinding>
                            </TextBox.Text>
                        </TextBox>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGridTextColumn.CellStyle>
</DataGridTextColumn>

Event Handler:

private void TextBlock_LostFocus(object sender, RoutedEventArgs e)
{
    TextBox b = sender as TextBox;
    if (b == null)
    {
        return;
    }
    MultiBindingExpression mb = BindingOperations.GetMultiBindingExpression(b, TextBox.TextProperty);
    mb.UpdateSource();
}