Hippie
Hippie

Reputation: 93

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>

Upvotes: 0

Views: 4020

Answers (3)

Xiaoy312
Xiaoy312

Reputation: 14477

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;
            }
        }

        UpdateTotalSale(this, default);
    };
}

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.

Upvotes: 0

Hippie
Hippie

Reputation: 93

enter image description here

Thanks, I have it working now!

I've modified/added the follow to the SalesOrder class

  public decimal TotalSale
  {
      get { return items.Sum(i => i.TotalPrice); }

  }

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

      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)
    {

        foreach (Window window in Application.Current.Windows)
        {
            if (window.GetType() == typeof(MainWindow))
            {
                (window as MainWindow).tbx_totalSale.Text =           CalculateTotalPrice().ToString();
            }
        }

    }

I also added the following to the xaml.

<StackPanel Orientation="Vertical"
                Grid.Row="7">
     <StackPanel Orientation="Horizontal">
            <Label Content="Total Sale: " />
            <TextBox x:Name="tbx_totalSale"
                     Text="{Binding TotalSale}"
                     Width="100"
                     x:FieldModifier="public"/>
</StackPanel>

And finally, this to the code behind:

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

 }

Upvotes: 0

ChrisF
ChrisF

Reputation: 137128

There's no binding in your text box:

<TextBox x:Name="tbx_totalSale"
                 Width="100"/>

It needs to be:

<TextBox x:Name="tbx_totalSale"
         Text="{Binding TotalSale}"
                 Width="100"/>

and you need to add a TotalSale property to your SaleOrder class that returns the sum of the lines:

public decimal TotalSale
{
    get { return items.Sum(i => i.TotalPrice);
}

You will need to publish an OnPropertyChanged("TotalSale") event when items are added to or removed from the collection or when then TotalPrice of a line changes. You are already updating TotalPrice when either the quantity or unit price changes, so all you need to do is propagate that change upwards through the items collection.

Upvotes: 1

Related Questions