Hippie Hippie - 9 days ago 5
C# Question

Display total sale from sum of datagrid column in C# WPF application

enter image description here

I am having trouble understanding how to bind the total sale textbox on the UI to the sum of the total price in the datagrid. I have read several articles, tried to implement a sum procedure and bind it, tried using collection changed notification and I can't get the total sale to work. I have an item class that has a Quantity and UnitPrice properties with a calculated TotalPrice which automatically updates when quantity or unit price changes. The list of items is contained in a SalesOrder class. The ui is the main window which I am using to create the order.
My code is at: https://github.com/battondl/SalesOrder/tree/master/simpleShoppingListProgram
Any guidance on how to get to get the total price on the UI textbox to update automatically would be much appreciated.

Each line item in the datagrid has a total price based on quanity * unit price.

It is the Total Sale price that I am trying to update which is a sum of all the line items totalprice.

class Item : INotifyPropertyChanged
{
private string partNumber;
private int quantity;
private decimal unitPrice;
private DateTime orderDate;


public string PartNumber
{
get { return partNumber; }
set
{
partNumber = value;
OnPropertyChanged();
}
}

public int Quantity
{
get { return quantity; }
set
{
quantity = value;
OnPropertyChanged("Quantity");
OnPropertyChanged("TotalPrice");
}
}
public decimal UnitPrice
{
get { return unitPrice; }
set
{
unitPrice = value;
OnPropertyChanged("UnitPrice");
OnPropertyChanged("TotalPrice");
}
}

public DateTime OrderDate
{
get { return orderDate; }
set
{
orderDate = value;
OnPropertyChanged();
}
}

public decimal TotalPrice => Quantity * UnitPrice;

public Item()
{
PartNumber = "";
Quantity = 0;
UnitPrice = 0.00m;
OrderDate = DateTime.Now;
}


public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string caller = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(caller));
}
}


}



class SalesOrder
{

private string orderNumber;
public string OrderNumber
{
get { return orderNumber; }
set
{
orderNumber = value;
}
}

private ObservableCollection<Item> items;

public ObservableCollection<Item> Items
{
get { return items; }
}

public SalesOrder()
{
orderNumber = "";
items = new ObservableCollection<Item>();

}

public decimal CalculateTotalPrice()
{
decimal total = 0.00m;
foreach (Item item in items)
{
total += item.TotalPrice;
}
return total;
}
}

public partial class MainWindow : Window
{
private SalesOrder salesOrder;

public MainWindow()
{
InitializeComponent();
}

private void Window_Loaded(object sender, RoutedEventArgs e)
{
salesOrder = new SalesOrder();
dgSaleItems.ItemsSource = salesOrder.Items;

tbx_totalSale.Text = salesOrder.CalculateTotalPrice().ToString();
}
}


<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="6*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Vertical"
Grid.Row="0">
<StackPanel Orientation="Horizontal">
<Label Content="OrderNumber: " />
<TextBox Name="tbx_orderNumber"
Width="100"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Vertical"
Grid.Row="1">
<DataGrid Name="dgSaleItems"
Grid.Row="1"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn x:Name="partNumber"
Header="Part Number"
Binding="{Binding Path=PartNumber}" />
<DataGridTextColumn x:Name="quantity"
Header="Quantity"
Binding="{Binding Path=Quantity}" />
<DataGridTextColumn x:Name="unitPrice"
Header="Unit Price"
Binding="{Binding Path=UnitPrice}" />
<DataGridTextColumn x:Name="totalPrice"
Header="Total Price"
Binding="{Binding Path=TotalPrice}" />
<DataGridTemplateColumn x:Name="orderDate"
Header="Order Date">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=OrderDate}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate>
<DatePicker SelectedDate="{Binding Path=OrderDate}" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
<StackPanel Orientation="Vertical"
Grid.Row="7">
<StackPanel Orientation="Horizontal">
<Label Content="Total Sale: " />
<TextBox x:Name="tbx_totalSale"
Width="100"/>
</StackPanel>
</StackPanel>
</Grid>

Answer
private void Window_Loaded(object sender, RoutedEventArgs e)
{
    salesOrder = new SalesOrder();
    dgSaleItems.ItemsSource = salesOrder.Items;

    tbx_totalSale.Text = salesOrder.CalculateTotalPrice().ToString();
}

You are only updating the total sale once during the window load event, but you forget you also have to update it every time an item's quantity or price is changed.

Here's how you can do that:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    salesOrder = new SalesOrder();
    dgSaleItems.ItemsSource = salesOrder.Items;

    salesOrder.Items.CollectionChanged += (s, args) =>
    {
        if (args.Action == NotifyCollectionChangedAction.Remove)
        {
            foreach (Item item in args.OldItems)
            {
                item.PropertyChanged -= UpdateTotalSale;
            }
        }
        else if (args.Action == NotifyCollectionChangedAction.Add)
        {
            foreach (Item item in args.NewItems)
            {
                item.PropertyChanged += UpdateTotalSale;
            }
        }
    };
}

private void UpdateTotalSale(object sender, PropertyChangedEventArgs e)
{
    tbx_totalSale.Text = salesOrder.CalculateTotalPrice().ToString();
}

Ideally, you should implement INPC on SalesOrder, and move the above code inside SalesOrder, as the code behind should be kept as clean as possible.