Orders - Part 2
Introduction
A quick recap on where we are and what is coming in this part. The target for the purchase order input form is this:
And at present this is what we have:
In this part I aim to add the order lines grid and a dialog to add the individual purchase order lines.
Purchase Order Lines Grid
Looking at the proposed order lines grid we have columns for:
- Product Code
- Description
- Quantity
- Unit Price
- Net Total
- Tax Rate
- Tax Amount
- Line total
Of these, at present, only Description, Quantity, Unit Price and Tax Rate are defined in the POLine model class. Product Code, Description and Unit Price will be derived from a 'Product' drop-down list and similarly Tax Rate will be derived from a 'Tax Rate' drop-down list. For the Syncfusion data grid we will need to add the missing items to the model. These are:
- Product Code
- Net Total
- Tax Amount
- Line Total
It's important to realise that these are only being added to the model class, they will not be added to SQL table.
One other potential peculiarity is that we are not saving the TaxID in the POLine database table. It would normally be more conventional to save the TaxID rather than the Tax Rate, but by saving the Tax Rate it will allow the same 'Tax Rate' to be changed in future without historic purchase order lines being recalculated with the current tax rate. An example of this is that in the UK the 'Standard Rate' of VAT is, at the time of writing, 20%, but in the past it has been 17.5%, 15.0% 10.0% and even 8.0%. However, we will need the TaxID in the model class for POLine so that the TaxRate drop-down list can function correctly.
Taking all the above into account, the revised model class for POLine is as shown below:
using System;
using System.ComponentModel.DataAnnotations;
// This is the model for one row in the database table.
namespace BlazorPurchaseOrders.Data
{
public class POLine
{
[Required]
public int POLineID { get; set; }
[Required]
public int POLineHeaderID { get; set; }
[Required]
public int POLineProductID { get; set; }
[Required ]
[StringLength(50)]
public string POLineProductDescription { get; set; }
[Required]
public decimal POLineProductQuantity { get; set; }
[Required]
public decimal POLineProductUnitPrice { get; set; }
[Required]
public decimal POLineTaxRate { get; set; }
//The folowing are not saved to database - just for the DataGrid
public decimal? POLineNetPrice { get; set; }
public decimal POLineTaxAmount { get; set; }
public decimal POLineGrossPrice { get; set; }
public string POLineProductCode { get; set; }
//POLIneTaxID is not saved to the database, but is needed for the Tax Rate drop-down list
//It would be more usual to save the TaxID to POLine in the database, but because
//tax rate percentages might change in future for a particular 'rate' we don't want
//historic tax amounts to be recalculated if re-displayed in the future.
public int POLineTaxID { get; set; }
}
}Having modified the POLine model class, the next stage is to add a Syncfusion DataGrid below the header section. Insert the following code in the html section just after closing <div/> for the header.
<SfGrid @ref="OrderLinesGrid"
DataSource="@orderLines"
Toolbar="@Toolbaritems"
AllowResizing="true">
<GridEvents OnToolbarClick="ToolbarClickHandler" TValue="POLine"></GridEvents>
</SfGrid>
This only inserts the 'outline' of the data-grid; we will add the columns later.
To declare the variables, enter the following in a suitable place at the start of the @code section.
SfGrid<POLine> OrderLinesGrid;
public List<POLine> orderLines = new List<POLine>();
private List<ItemModel> Toolbaritems = new List<ItemModel>();In the OnInitializedAsync method add the following to define the toolbar items we want displaying.
Toolbaritems.Add(new ItemModel() { Text = "Add", TooltipText = "Add a new order line", PrefixIcon = "e-add" });
Toolbaritems.Add(new ItemModel() { Text = "Edit", TooltipText = "Edit selected order line", PrefixIcon = "e-edit" });
Toolbaritems.Add(new ItemModel() { Text = "Delete", TooltipText = "Delete selected order line", PrefixIcon = "e-delete" });
We also need to add the code to handle a toolbar click. To add the skeleton for this, add the following method at the end of the code section.
public async Task ToolbarClickHandler(Syncfusion.Blazor.Navigations.ClickEventArgs args)
{
if (args.Item.Text == "Add")
{
//Code for adding goes here
}
if (args.Item.Text == "Edit")
{
//Code for adding goes here
}
if (args.Item.Text == "Delete")
{
//Code for adding goes here
}
}Save and run the project. From the Orders List select 'Add'; the purchase order form should look like this:
Now to add the columns to the grid. Insert the following code under the <GridEvents> section. Each column is being defined with its associated field from the model class, a column header text, alignment, format and column width.
<GridColumns>
<GridColumn Field="@nameof(POLine.POLineProductCode)"
HeaderText="Product"
TextAlign="@TextAlign.Left"
Width="20">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineProductDescription)"
HeaderText="Description"
TextAlign="@TextAlign.Left"
Width="30">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineProductQuantity)"
HeaderText="Quantity"
TextAlign="@TextAlign.Right"
Format="n0"
Width="10">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineProductUnitPrice)"
HeaderText="Unit Price"
TextAlign="@TextAlign.Right"
Format="C2"
Width="10">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineNetPrice)"
HeaderText="Net Price"
TextAlign="@TextAlign.Right"
Format="C2"
Width="10">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineTaxRate)"
HeaderText="Tax Rate"
TextAlign="@TextAlign.Right"
Format="p2"
Width="10">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineTaxAmount)"
HeaderText="Tax"
TextAlign="@TextAlign.Right"
Format="c2"
Width="10">
</GridColumn>
<GridColumn Field="@nameof(POLine.POLineGrossPrice)"
HeaderText="Total"
TextAlign="@TextAlign.Right"
Format="C2"
Width="10">
</GridColumn>
</GridColumns>Run the project again; this time the 'Add an Order' form should now have the correct columns.
Dialog to add order lines
We are going to use a Syncfusion Dialog control to enter the purchase order lines. We'll start by adding a very basic dialog with buttons to handle saving data or cancelling, its model and methods to handle saving a record or cancelling. Add the following to create the dialog after the closing </EditForm> for the header part of the form
<SfDialog @ref="DialogAddEditOrderLine" IsModal="true" Width="600px" ShowCloseIcon="true" Visible="false">
<DialogTemplates>
<Header> Add Order Line </Header>
</DialogTemplates>
<EditForm Model="@addeditOrderLine" OnValidSubmit="@OrderLineSave">
<br />
<div class="e-footer-content">
<div class="button-container">
<button type="button" class="e-btn e-normal" @onclick="@CloseDialog">Cancel</button>
<button type="submit" class="e-btn e-normal e-primary">Save</button>
</div>
</div>
</EditForm>
</SfDialog>To declare the Syncfusion dialog and the data model to be used by the dialog add the following in the top section of the @code.
SfDialog DialogAddEditOrderLine;
public POLine addeditOrderLine = new POLine();Add the code that will handle the save order line and cancel. At this stage they are simply placeholders for code we will add later, but are necessary to run the project. This can go anywhere in the code block, but towards the end will be fine.
private void OrderLineSave()
{
if (addeditOrderLine.POLineID == 0)
{
//Code to save order line goes here
}
}
private async Task CloseDialog()
{
await this.DialogAddEditOrderLine.Hide();
}Finally, insert the code in the toolbar handler so that the order line dialog opens when the user clicks 'Add'.
//Code for adding goes here
addeditOrderLine = new POLine(); // Ensures a blank form when adding
addeditOrderLine.POLineNetPrice = 0;
addeditOrderLine.POLineTaxID = 0;
addeditOrderLine.POLineProductID = 0;
await this.DialogAddEditOrderLine.Show();This adds a new record to the 'addeditOrderLine' collection, and sets some defaults before finally opening the (empty) dialog.
Save the project and run it; add a new order and then try adding a new order line. It's not very exciting (yet!) but it should look like this:
The next task is to add the controls to the dialog. We will start with the drop-down list to allow the user to select a product. The controls that rely on product selection are the product description and unit price, so we will add these. Once the user has selected a product we will derive the description and unit price and populate the relevant text box and numeric text box.
Add the following code immediately after the <EditForm> tag in the SfDialog block.
<DataAnnotationsValidator />
<SfDropDownList DataSource="@product"
TItem="Product"
TValue="int"
Text="ProductID"
@bind-Value="addeditOrderLine.POLineProductID"
FloatLabelType="@FloatLabelType.Always"
Placeholder="Select a Product"
Enabled="true">
<DropDownListFieldSettings Text="ProductCode" Value="ProductID"></DropDownListFieldSettings>
<DropDownListEvents TItem="Product" TValue="int" OnValueSelect="OnChangeProduct"></DropDownListEvents>
</SfDropDownList>
<SfTextBox Enabled="true" Placeholder="Product Description"
FloatLabelType="@FloatLabelType.Always"
@bind-Value="addeditOrderLine.POLineProductDescription"></SfTextBox>
<SfNumericTextBox Enabled="true" Placeholder="Unit Price"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="c2"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineProductUnitPrice"></SfNumericTextBox> To populate the product drop-down list we need to add the following to the top of the code to inject the ProductService.
@inject IProductService ProductServiceFollowed by the IEnumerable for product, the source for the product drop-down list. Insert this immediately after the IEnumerable for supplier.
IEnumerable<Product> product;Complete this process by adding this to the OnInitializedAsyn method to get the data when the form is initialised.
product = await ProductService.ProductList();There is also an event triggered on the ValueChange of the drop-down list. Add the method shown below at the end of the code section. It will derive the product code, description and unit price and populate the relevant controls.
private void OnChangeProduct(Syncfusion.Blazor.DropDowns.SelectEventArgs<Product> args)
{
this.addeditOrderLine.POLineProductCode = args.ItemData.ProductCode;
this.addeditOrderLine.POLineProductDescription = args.ItemData.ProductDescription;
this.addeditOrderLine.POLineProductUnitPrice = args.ItemData.ProductUnitPrice;
}Save the project and run it; add a new order and then try adding a new order line. Select a product; product description and unit price should be populated from the product record. The 'Save' button doesn't do anything yet, so click 'Cancel' to close the dialog.
There will also be a drop-down list for Tax Rate, and the process for adding this and the numeric text box for tax rate closely follows the above procedure for the product drop-down list. Insert the following code in the dialog block, under the existing the product code. Note that the Numeric text box used to display the tax rate is not enabled - we don't want the user to be able to amend the rate.
<SfDropDownList DataSource="@tax"
TItem="Tax"
TValue="int"
Text="TaxID"
@bind-Value="addeditOrderLine.POLineTaxID"
FloatLabelType="@FloatLabelType.Always"
Placeholder="Tax Rate"
Enabled="true">
<DropDownListFieldSettings Text="TaxDescription" Value="TaxID"></DropDownListFieldSettings>
<DropDownListEvents TItem="Tax" TValue="int" OnValueSelect="OnChangeTax"></DropDownListEvents>
</SfDropDownList>
<SfNumericTextBox Enabled="false" Placeholder="Tax Rate %"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="p2"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineTaxRate">
</SfNumericTextBox>Add the code to inject the TaxService.
@inject ITaxService TaxServiceAdd the IEnumerable line for Tax
IEnumerable<Tax> tax;Add this to the OnInitilaizedAsync to populate the list when the form is opened.
tax = await TaxService.TaxList();As for the product drop-down, an event triggered on the ValueChange of the drop-down list. Add the method shown below at the end of the code section. It will derive the tax rate and complete the tax rate numeric text box.
private void OnChangeTax(Syncfusion.Blazor.DropDowns.SelectEventArgs<Tax> args)
{
int testTaxId = args.ItemData.TaxID;
this.addeditOrderLine.POLineTaxRate = args.ItemData.TaxRate;
}Save and run the project again. Select a product and tax rate; it should look similar to this:
The other fields we need to add to the dialog are:
- Quantity
- Net Price (Quantity * Unit Price)
- Tax (Net Price * Tax Rate)
- Total (Net Price + Tax) or (Net Price * (1+Tax Rate))
We will add these numeric text boxes and the code to do the calculations, we will then improve the look of the dialog, and finally add the code to save the order line.
To add the missing text boxes, insert the following into the dialog block.
Quantity - immediately after 'Product Description'.
<SfNumericTextBox Enabled="true" Placeholder="Quantity"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="n0"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineProductQuantity"
@onfocusout='@POLineCalc'>
</SfNumericTextBox>Net Price - immediately after 'Unit Price'.
<SfNumericTextBox Enabled="false" Placeholder="Net Price"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="c2"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineNetPrice">
</SfNumericTextBox>Tax Amount and Total Price - immediately after 'Tax Rate'.
<SfNumericTextBox Enabled="false" Placeholder="Tax Amount"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="c2"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineTaxAmount">
</SfNumericTextBox>
<SfNumericTextBox Enabled="false" Placeholder="Total Price"
FloatLabelType="@FloatLabelType.Always"
ShowSpinButton="false"
Format="c2"
EnableRtl="true"
@bind-Value="addeditOrderLine.POLineGrossPrice">
</SfNumericTextBox>To carry out the calculations I have added a new method 'POLineCalc' and this is triggered by the @onfocusout event on the 'Quantity' text box. This means that whenever a user enters a quantity the calculation will take place. The code to carry out the calculations is shown below. Add this to the end of the code section.
private void POLineCalc()
{
addeditOrderLine.POLineNetPrice = addeditOrderLine.POLineProductUnitPrice * addeditOrderLine.POLineProductQuantity;
addeditOrderLine.POLineTaxAmount = addeditOrderLine.POLineNetPrice.Value * addeditOrderLine.POLineTaxRate;
addeditOrderLine.POLineGrossPrice = addeditOrderLine.POLineNetPrice.Value * (1 + addeditOrderLine.POLineTaxRate);
}However, we ought to make sure that these calculations also take place if the user also changes the product (by implication changing the unit price) and/or the tax rate. To handle this we need to add 'POLIneCalc();' to both the OnChangeProduct and OnChangeTax methods. The full code for these methods is shown below, but at this stage it is only necessary to add the POLineCalc() statement:
private void OnChangeProduct(Syncfusion.Blazor.DropDowns.SelectEventArgs<Product> args)
{
this.addeditOrderLine.POLineProductCode = args.ItemData.ProductCode;
this.addeditOrderLine.POLineProductDescription = args.ItemData.ProductDescription;
this.addeditOrderLine.POLineProductUnitPrice = args.ItemData.ProductUnitPrice;
POLineCalc();
}
private void OnChangeTax(Syncfusion.Blazor.DropDowns.SelectEventArgs<Tax> args)
{
int testTaxId = args.ItemData.TaxID;
this.addeditOrderLine.POLineTaxRate = args.ItemData.TaxRate;
POLineCalc();
}Save the project and run it. Add a new order line, select a product, enter a quantity and tax rate. Change the quantity, tax rate (and product) and check the calculations are functioning correctly.
It works, but doesn't look great. To improve the aesthetics we can add some css styling. Add the following within the <style> tags at the foot of the html section (under the .grid-container section would be suitable).
.flex-container {
display: flex;
flex-direction: row; /* Causes tab to move along row and then onto following row */
justify-content: space-evenly; /* Equal space left and right margin and between elements */
margin: 10px; /* This appears to be vertical margin between rows */
column-gap: 10px; /* Tgap betwen columns */
}Wrapping controls within <div class=flex-container> tags will cause those controls to be laid out across the form with equal spacing with a 10px gap between controls and rows. (Adjust the css if you prefer different spacing.)
My preference would be to wrap the product drop-down and product description in separate divs, to wrap quantity, unit price and net price as a group, and the same for tax rate drop-down, tax rate % and tax amount, leaving total amount on a row by itself (but enclosed by the css class div to give the 10px margins).
Running the application should now look like this:
More or less the last thing we need to do with the order lines dialog is wire up the Save button. Copy and paste the code below into the OrderLineSave method, below the placeholder for 'Code to save order line goes here'
orderLines.Add(new POLine
{
POLineHeaderID = 0,
POLineProductID = addeditOrderLine.POLineProductID,
POLineProductCode = addeditOrderLine.POLineProductCode,
POLineProductDescription = addeditOrderLine.POLineProductDescription,
POLineProductQuantity = addeditOrderLine.POLineProductQuantity,
POLineProductUnitPrice = addeditOrderLine.POLineProductUnitPrice,
POLineNetPrice = addeditOrderLine.POLineNetPrice,
POLineTaxRate = addeditOrderLine.POLineTaxRate,
POLineTaxAmount = addeditOrderLine.POLineTaxAmount,
POLineGrossPrice = addeditOrderLine.POLineGrossPrice
});
OrderLinesGrid.Refresh();
StateHasChanged(); //<----- THIS IS ABSOLUTELY ESSENTIAL
//addeditOrderLine = new POLine(); //<----- THIS gives errors (nulls)
addeditOrderLine.POLineProductID = 0;
addeditOrderLine.POLineProductCode = "";
addeditOrderLine.POLineProductDescription = "";
addeditOrderLine.POLineProductQuantity = 0;
addeditOrderLine.POLineProductUnitPrice = 0;
addeditOrderLine.POLineNetPrice = 0;
addeditOrderLine.POLineTaxID = 0;
addeditOrderLine.POLineTaxRate = 0;
addeditOrderLine.POLineTaxAmount = 0;
addeditOrderLine.POLineGrossPrice = 0;The code takes data from the dialog (model = addeditOrderLine) and adds it to a new orderLines object. We don't know the order HeaderID yet, so this is set to 0.
We haven't added much in the way of data validation yet, but running the application and adding a couple of order lines should work and give a result similar to:
Project Code
The C# code for the changes made for adding purchase order form lines are shown here.
YouTube Video
Blazor Purchase Orders - Part 11 - Adding order lines to the Purchase Order Form page.