Vishal
Vishal

Reputation: 6368

DataGrid Calculated column does not show any data

Let's have an example to understand my problem clearly :

I have three TextBox Controls as below :

<TextBox x:Name="Quantity" />
<TextBox x:Name="Rate" />
<TextBox x:Name="Amount" />

So, I want to calculate Amount when Quantity or Rate Changes. Amount = Quantity * Rate

Similarly I want to change Rate if user intends to change Amount. So, formula for Rate will be Rate = Amount / Quantity

Actual Problem:

I have a DataGrid with the three Columns :

<DataGrid ItemsSource="{Binding OpeningBalances}">

    <DataGrid.Columns>

        <DataGridTemplateColumn Header="Quantity" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Quantity}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Quantity}" Loaded="TextBox_Loaded"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Rate" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Rate}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Rate}" Loaded="TextBox_Loaded"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Amount" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock>
                        <TextBlock.Text>
                            <MultiBinding Converter="{StaticResource amountFromQuantityAndRateConverter}">
                                <Binding Path="Quantity" />
                                <Binding Path="Rate" />
                            </MultiBinding>
                        </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Loaded="TextBox_Loaded">
                        <TextBox.Text>
                            <MultiBinding Converter="{StaticResource amountFromQuantityAndRateConverter}">
                                <Binding Path="Quantity" />
                                <Binding Path="Rate" />
                            </MultiBinding>
                        </TextBox.Text>
                    </TextBox>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

    </DataGrid.Columns>

</DataGrid>

Property declaration in ViewModel :

public MainWindowViewModel()
{
    OpeningBalances = new ObservableCollection<ItemOpeningBalanceRow>();
}

private ObservableCollection<ItemOpeningBalanceRow> _openingBalances;
public ObservableCollection<ItemOpeningBalanceRow> OpeningBalances
{
    get
    {
        return _openingBalances;
    }
    set
    {
        _openingBalances = value;
        OnPropertyChanged("OpeningBalances");
    }
}

ItemOpeningBalnceRow.cs :

public class ItemOpeningBalanceRow 
{
    public double Quantity { get; set; }
    public double Rate { get; set; }
    public double Amount { get; set; }
}

Converter - AmountFromQuantityAndRateConverter.cs

public class AmountFromQuantityAndRateConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int _quantity = 0;
        int _rate = 0;

        if (int.TryParse(values[0].ToString(), out _quantity) && int.TryParse(values[1].ToString(), out _rate))
        {
            return _quantity * _rate;
        }

        return 0;
    }

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

I have used above mentioned Multi Value converter but still Amount column always remains null. I mean it does not display any data.

Update:

The answer given by @sky-dev is the best possible answer and is working fine. But I would like to get the same approach using converters.

What I have tried?:

I have created two converters as follows:

RateFromAmountAndQuantityConverter.cs

public class RateFromAmountAndQuantityConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int _quantity = 0;
        int _amount = 0;
        int _rate = 0;

        if (int.TryParse(values[0].ToString(), out _quantity) && int.TryParse(values[1].ToString(), out _amount) && int.TryParse(values[2].ToString(), out _rate))
        {
            if (_quantity != 0 && _amount != 0)
            {
                if (_rate == _amount / _quantity)
                    return _rate.ToString();
                else
                    return (_amount / _quantity).ToString();
            }
        }

        return "0";
    }

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

And other is AmountFromQuantityAndRateConverter.cs

public class AmountFromQuantityAndRateConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int _quantity = 0;
        int _rate = 0;
        int _amount = 0;

        if (int.TryParse(values[0].ToString(), out _quantity) && int.TryParse(values[1].ToString(), out _rate) && int.TryParse(values[2].ToString(), out _amount))
        {
            if (_rate != 0)
            {
                if (_amount == _quantity * _rate)
                    return _amount.ToString();
                else
                    return (_quantity * _rate).ToString();
            }
        }

        return "0";
    }

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

I call them as follows:

<DataGridTemplateColumn Header="Quantity" >
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}" Loaded="TextBox_Loaded"/>
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>

<DataGridTemplateColumn Header="Rate" >
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding Converter="{StaticResource rateFromAmountAndQuantityConverter}">
                        <Binding Path="Quantity" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Amount" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Rate" UpdateSourceTrigger="PropertyChanged" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <TextBox Loaded="TextBox_Loaded">
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource rateFromAmountAndQuantityConverter}" >
                        <Binding Path="Quantity" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Amount" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Rate" UpdateSourceTrigger="PropertyChanged" />
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>

<DataGridTemplateColumn Header="Amount" >
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <TextBlock>
                <TextBlock.Text>
                    <MultiBinding Converter="{StaticResource amountFromQuantityAndRateConverter}">
                        <Binding Path="Quantity" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Rate" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Amount" UpdateSourceTrigger="PropertyChanged" />
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
    <DataGridTemplateColumn.CellEditingTemplate>
        <DataTemplate>
            <TextBox Loaded="TextBox_Loaded">
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource amountFromQuantityAndRateConverter}">
                        <Binding Path="Quantity" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Rate" UpdateSourceTrigger="PropertyChanged"/>
                        <Binding Path="Amount" UpdateSourceTrigger="PropertyChanged" />
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
        </DataTemplate>
    </DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>

Problems using above two converters :

They refer to each-other, like cyclic reference but I don't get stackOverflow exeption. Infact I don't get any errors but no matter whatever I set the value of rate or amount both remains 0. I also know why they remains 0. But don't know the solution.

Upvotes: 1

Views: 586

Answers (2)

Lee O.
Lee O.

Reputation: 3312

In order to use a converter, you will only use 1 converter. You will need to make the scope of the quantity field in your converter be at class level so that it can retain the value of quantity to calculate the new rate on the ConvertBack call. My model doesn't have an Amount property now but if you need it for any business logic, you would just have a getter that returns Quantity * Rate since Amount will always equal that result.

AmountConverter - I used decimals but you can change to int and handle rounding however you decide.

public class AmountConverter : IMultiValueConverter
{
    decimal quantity = 0m;
    decimal rate = 0m;

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (decimal.TryParse(values[0].ToString(), out quantity) && decimal.TryParse(values[1].ToString(), out rate))
        {
            return (quantity * rate).ToString();
        }

        return "0";
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        decimal amount = 0m;
        object[] values = new object[2];
        values[0] = quantity;
        values[1] = rate;

        if (decimal.TryParse(value.ToString(), out amount))
        {
            if (quantity != 0m)
                values[1] = amount / quantity;
        }

        return values;
    }
}

XAML - I simplified to use a few TextBlocks just to demo the functionality. If Quantity is 0 then entering a new Amount will reset the Amount to 0 since you can't divide by 0.

<StackPanel DataContext="{StaticResource MainViewModel}">
    <TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}" 
             Margin="4" />
    <TextBox Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"
             Margin="4" />
    <TextBox Margin="4">
        <TextBox.Text>
            <MultiBinding Converter="{StaticResource AmountConverter}"
                          UpdateSourceTrigger="PropertyChanged">
                <Binding Path="Quantity" />
                <Binding Path="Rate" />
            </MultiBinding>
        </TextBox.Text>
    </TextBox>
</StackPanel>

Upvotes: 1

sky-dev
sky-dev

Reputation: 6258

Good job including all the details. That really helps! There were a few trouble spots.

  1. I implemented INotifyPropertyChanged in the MainWindow code behind and the row object
  2. MainWindow needed DataContext = this to properly data bind
  3. Row updates needed to happen when numbers were typed rather than focus changed, hence "UpdateSourceTrigger=PropertyChanged"
  4. The converter needed to return (_quantity * _rate).ToString() to properly bind to the TextBlock.Text field. This surprised me as well.

Try this out.

<Grid>
    <Grid.Resources>
        <converters:AmountFromQuantityAndRateConverter x:Key="amountFromQuantityAndRateConverter" />
    </Grid.Resources>

    <DataGrid ItemsSource="{Binding OpeningBalances}" AutoGenerateColumns="False">
        <DataGrid.Columns>
            <DataGridTemplateColumn Header="Quantity" >
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>

            <DataGridTemplateColumn Header="Rate" >
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>

            <DataGridTemplateColumn Header="Amount" >
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <TextBlock>
                            <TextBlock.Text>
                                <MultiBinding Converter="{StaticResource amountFromQuantityAndRateConverter}">
                                    <Binding Path="Quantity" UpdateSourceTrigger="PropertyChanged" />
                                    <Binding Path="Rate" UpdateSourceTrigger="PropertyChanged"  />
                                </MultiBinding>
                            </TextBlock.Text>
                        </TextBlock>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>
</Grid> 


public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();
        OpeningBalances = new ObservableCollection<ItemOpeningBalanceRow>(new [] { new ItemOpeningBalanceRow()});
        DataContext = this;
    }

    private ObservableCollection<ItemOpeningBalanceRow> _openingBalances;
    public ObservableCollection<ItemOpeningBalanceRow> OpeningBalances
    {
        get
        {
            return _openingBalances;
        }
        set
        {
            _openingBalances = value;
            OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ItemOpeningBalanceRow : INotifyPropertyChanged
{
    private double _quantity;
    private double _rate;

    public double Quantity
    {
        get { return _quantity; }
        set { _quantity = value;  OnPropertyChanged(); }
    }
    public double Rate {
        get { return _rate; }
        set { _rate = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class AmountFromQuantityAndRateConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int _quantity = 0;
        int _rate = 0;

        if (int.TryParse(values[0].ToString(), out _quantity) && int.TryParse(values[1].ToString(), out _rate))
        {
            return (_quantity * _rate).ToString();
        }

        return 0;
    }

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

The code above should work, but I would recommend another approach. Just ditch the converter and do it in your ViewModel (ItemOpeningBalanceRow). Much simpler. Here is what that would look like:

<DataGrid ItemsSource="{Binding OpeningBalances}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Quantity" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Rate" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Amount" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Amount}">
                    </TextBlock>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

public class ItemOpeningBalanceRow : INotifyPropertyChanged
{
    private double _quantity;
    private double _rate;

    public double Quantity
    {
        get { return _quantity; }
        set { _quantity = value; OnPropertyChanged(); OnPropertyChanged("Amount"); }
    }
    public double Rate {
        get { return _rate; }
        set { _rate = value; OnPropertyChanged(); OnPropertyChanged("Amount"); }
    }

    public double Amount
    {
        get { return Quantity*Rate; }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

* Update *

I've included the calculation of Rate from Amount.

<DataGrid ItemsSource="{Binding OpeningBalances}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTemplateColumn Header="Quantity" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Rate" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <TextBox Text="{Binding Rate, UpdateSourceTrigger=PropertyChanged}"/>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTemplateColumn Header="Amount" >
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                        <TextBlock Text="{Binding Amount, UpdateSourceTrigger=PropertyChanged}">
                    </TextBlock>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
                <DataGridTemplateColumn.CellEditingTemplate>
                    <DataTemplate>
                        <TextBox Text="{Binding Amount, UpdateSourceTrigger=PropertyChanged}"/>
                    </DataTemplate>
                </DataGridTemplateColumn.CellEditingTemplate>
            </DataGridTemplateColumn>
    </DataGrid.Columns>
</DataGrid>

public class ItemOpeningBalanceRow : INotifyPropertyChanged
{
    private double _quantity;
    private double _rate;
    private double _amount;

    public double Quantity
    {
        get { return _quantity; }
        set 
        { 
            _quantity = value; 
            OnPropertyChanged();
            Amount = Quantity*Rate;
        }
    }
    public double Rate {
        get { return _rate; }
        set
        {
            if (!(Math.Abs(_rate - value) > 0.0001)) return;
            _rate = value;
            OnPropertyChanged();
            Amount = Quantity * Rate;
        }
    }

    public double Amount
    {
        get { return Quantity*Rate; }
        set
        {
            if (!(Math.Abs(_amount - value) > 0.0001)) return;
            _amount = value;
            OnPropertyChanged();
            Rate = _amount / Quantity;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Upvotes: 1

Related Questions