Tax Rates
Introduction
By now we have spent a fair amount of time laying the foundations for our project, but don't really have anything to show for it. Now is the time we start building the visible structure and we will start with pages to allow the user to maintain the semi-static data, such as tax rates, suppliers and products
Tax Rates will be the simplest as it only has a couple of columns that will need maintaining. Below is an Excel mock up of a rough idea of how I would like the page to look. We will be using the Syncfusion DataGrid and I like the idea of using the built-in toolbar and amending it to use a popup modal dialog box for the user to enter/edit data.

Warning - Error CS0266
Before we get into the design of the page, it would be worth mentioning that I spent the best part of a couple of days chasing my tail with the above error. The error message is in the form of "CS0266 - Cannot implicitly convert type 'type1' to 'type2'. An explicit conversion exists (are you missing a cast?)" and the Microsoft documentation is no help at all.
The table that holds the tax rate data is called simply 'Tax', and I (innocently, and I thought logically) decided to call the Blazor page 'Tax.razor'. This turned out to be the cause of my problem; it would appear that as I had also called the model class for the Tax table 'Tax.cs' a name conflict occurred and the error was thrown.
I suggest that to avoid this error that all razor pages are called 'XxxxPage.razor'.
Adding Tax Rates Page
We will be building up the page to maintain tax rates step by step.
In Visual Studio, open the project and right-click on the Pages folder icon in Solution Explorer and select 'Add > Razor Component'. Enter a name; Blazor pages must begin with an uppercase letter, and following my suggestion about adding 'Page', I suggest calling this page 'TaxPage.razor'
Copy the following code and paste it over the code automatically added when the file was created.
@page "/tax"
@using BlazorPurchaseOrders.Data
@inject ITaxService TaxService
@using Syncfusion.Blazor.Navigations
<h3>Tax Rates</h3>
<br />
<SfGrid DataSource="@tax"
Toolbar="@Toolbaritems">
<GridColumns>
<GridColumn Field="@nameof(Tax.TaxDescription)"
HeaderText="Description"
TextAlign="TextAlign.Left"
Width="60">
<GridColumn Field="@nameof(Tax.TaxRate)"
HeaderText="Rate %"
TextAlign="TextAlign.Right"
Format="p2"
Width="40">
</GridColumn>
</GridColumn>
</GridColumns>
</SfGrid>
@code {
IEnumerable<Tax> tax;
private List<ItemModel> Toolbaritems = new List<ItemModel>();
protected override async Task OnInitializedAsync()
{
//Populate the list of Tax objects from the Tax table.
tax = await TaxService.TaxList();
Toolbaritems.Add(new ItemModel() { Text = "Add", TooltipText = "Add a new Tax Rate", PrefixIcon = "e-add" });
Toolbaritems.Add(new ItemModel() { Text = "Edit", TooltipText = "Edit selected Tax Rate", PrefixIcon = "e-edit" });
Toolbaritems.Add(new ItemModel() { Text = "Delete", TooltipText = "Delete selected Tax Rate", PrefixIcon = "e-delete" });
}
}- The first four lines are
- a directive that routes the page to "/tax", meaning we can access the page by entering 'tax' after the root page.
- a directive that allows access to the Data folder.
- injects the TaxService interface and service (allowing access to methods contained in them.
- a directive that will allow us to use Syncfusion Balzor Navigation methods.
- The <h3> line is a heading that will be displayed at the top of the page, and <br /> adds a blank row below the heading to make it more pleasing to the eye.
- Everything within the <SfGrid></SfGrid> tags refers to the Syncfusion DataGrid.
- DataSource points to the 'tax' object defined in the @code section.
- Toolbar points to the 'Toolbar' items defined in the @code section.
<GridColumns></GridColumns> tags enclose the column definitions. - The first <GridColumn> has a 'Field' that points to the 'TaxDescription' from the Tax object, had 'HeaderText' of 'Description', the text will be aligned to the left and the width is set to '60'. (Width seems to be in ratio proportions.)
- The second <GridColumn> follows the same pattern, this time displaying the 'TaxRate' and having the additional property of 'Format = "p2"' that should display the rate as a percentage showing 2 decimal places.
- The @code section contains the C# code.
- 'IEnumerable<Tax> tax;' declares the tax variable
- 'private List<ItemModel>
Toolbaritems = new List<ItemModel ();' declares the Toolbar variable as a list. The 'protected override async Task OnInitializedAsync()' method defines the code that should be run when the page is initially opened. In this case it should Populate the list of Tax objects from the Tax table Add the items that should be added to the Toolbars list
Save the file.
At this stage it would also be worth making a change to the navigation menu to allow us to see the Tax Rates page without having to type '/tax' into the browser's address bar.
Open NavMenu.razor in the Shared folder and overwrite the last <li></li> item with the following:
<li class="nav-item px-3">
<NavLink class="nav-link" href="tax">
<span class="oi oi-list-rich" aria-hidden="true"></span> Tax Rates
</NavLink>
</li>This will overwrite the Weather Forecast menu item with 'Tax Rates' and will point it to href="tax".
Save all the files and run the application. We should see the following. Note that we have no data yet, and the buttons do not doing anything either yet.
Adding the 'Add' function
The 'Add' button will open a modal dialog to allow the user to enter details about a Tax Rate and then to save the record (or cancel).
Copy the code from the following and completely replace the existing code in TaxPages.razor.
@page "/tax"
@using BlazorPurchaseOrders.Data
@inject ITaxService TaxService
@using Syncfusion.Blazor.Navigations
<h3>Tax Rates</h3>
<br />
<SfGrid DataSource="@tax"
Toolbar="@Toolbaritems">
<GridColumns>
<GridColumn Field="@nameof(Tax.TaxDescription)"
HeaderText="Description"
TextAlign="TextAlign.Left"
Width="60">
<GridColumn Field="@nameof(Tax.TaxRate)"
HeaderText="Rate %"
TextAlign="TextAlign.Right"
Format="p2"
Width="40">
</GridColumn>
</GridColumn>
</GridColumns>
<GridEvents OnToolbarClick="ToolbarClickHandler" TValue="Tax"></GridEvents>
</SfGrid>
<SfDialog @ref="DialogAddEditTax" IsModal="true" Width="500px" ShowCloseIcon="true" Visible="false">
<DialogTemplates>
<Header> @HeaderText </Header>
</DialogTemplates>
<EditForm Model="@addeditTax" OnValidSubmit="@TaxSave">
<div>
<SfTextBox Enabled="true" Placeholder="Description"
FloatLabelType="@FloatLabelType.Always"
@bind-Value="addeditTax.TaxDescription"></SfTextBox>
<SfNumericTextBox Enabled="true" Placeholder="Tax Rate" Width="50"
Format="p2"
FloatLabelType="@FloatLabelType.Always"
@bind-Value="addeditTax.TaxRate"></SfNumericTextBox>
</div>
<br /><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>
@code {
IEnumerable<Tax> tax;
private List<ItemModel> Toolbaritems = new List<ItemModel>();
SfDialog DialogAddEditTax;
Tax addeditTax = new Tax();
string HeaderText = "";
protected override async Task OnInitializedAsync()
{
//Populate the list of VAT objects from the VAT table.
tax = await TaxService.TaxList();
Toolbaritems.Add(new ItemModel() { Text = "Add", TooltipText = "Add a new Tax Rate", PrefixIcon = "e-add" });
Toolbaritems.Add(new ItemModel() { Text = "Edit", TooltipText = "Edit selected Tax Rate", PrefixIcon = "e-edit" });
Toolbaritems.Add(new ItemModel() { Text = "Delete", TooltipText = "Delete selected Tax Rate", PrefixIcon = "e-delete" });
}
public async Task ToolbarClickHandler(Syncfusion.Blazor.Navigations.ClickEventArgs args)
{
if (args.Item.Text == "Add")
{
//Code for adding goes here
addeditTax = new Tax(); // Ensures a blank form when adding
HeaderText = "Add Tax Rate";
await this.DialogAddEditTax.Show();
}
if (args.Item.Text == "Edit")
{
//Code for editing goes here
}
if (args.Item.Text == "Delete")
{
//code for deleting goes here
}
}
protected async Task TaxSave()
{
if (addeditTax.TaxID == 0)
{
// Insert if TaxID is zero.
}
else
{
// Item is being edited
}
//Close Dialog
await CloseDialog();
//Reset addeditTax
addeditTax = new Tax();
//Refresh datagrid
tax = await TaxService.TaxList();
StateHasChanged();
}
private async Task CloseDialog()
{
await this.DialogAddEditTax.Hide();
}
}The first thing to note is that a <GridEvents> tag has been inserted in the <SfGrid>section, with an 'OnToolbarClick' event that will trigger 'ToolbarClickHandler'; it also has a TValue of "Tax".
The next major addition is the Syncfusion Dialog, <SfDialog>. It has the following features:
- A @ref that will allow us to refer to the dialog. It is also modal and is initially set to visible = "false"
- Within <DialogTemplates> tags there is a <Header> tae which, in turn, has a variable of @HeaderText. This will be given a value in the @code section to be displayed when the dialog is made visible.
- There is an <EditForm> section which uses an @addeditTax model and triggers the code '@TaxSave' when a valid submission is made.
- There are then two text boxes to record the TaxDescription and TaxRate.
- Lastly, within the Dialog are two buttons, a 'Cancel' one that triggers the @CloseDialog method, and a Save button that triggers a valid submission.
The @code section has the following changes:
- Declarations for the SfDialog, the addeditTax data model, and the HeaderText string.
- A ToolbarClickHandler method. This uses the text value of the toolbar item to determine the action to take. The code currently handles 'Add' and assigns a value to HeaderText and then makes the SfDialog visible.
- A new method to handle a valid submit from the SfDialog called 'TaxSave'. It checks the value of TaxID, and if it is 0 it must be an addition and inserts a new record into the database by calling the TaxInsert service from TaxService.
- Having performed the insert, the TaxSave method closes the SfDialog, resets the addeditTax object and refreshes the DataGrid.
Save the files and run the application. The image below shows the SfDialog open in the foreground with three records having been added shown in the DataGrid in the background.
Improving the 'Add' function
A potential problem with the Add function, as we have it, is that there is nothing to prevent the user from adding the same 'Tax Rate' twice. The question also arises as to whether we should prevent duplicate TaxDescriptions or TaxRates. In the UK there are two 0% rates, one for zero-rated supplies and another for exempt supplies. In this example I will therefore be checking for duplicate Descriptions, but in your location it might make more sense to prevent duplicate rates.
The method we will adopt for checking for duplicates will be at the SQL level. The stored procedure for inserting a record will test for whether a duplicate exists; if no duplicate is found the insert will take place and the stored procedure will return a 'success', otherwise if a duplicate is found the insert will fail and a 'failure' will be returned to the C# code.
Tax_Insert SQL Stored Procedure
Open SQL Management Studio, select the PurchaseOrders database and open a new query window and paste in the following code. It will drop the existing stored procedure and create a new one.
DROP PROCEDURE [dbo].[spTax_Insert]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[spTax_Insert]
(
@TaxDescription nvarchar(50),
@TaxRate decimal(6, 4)
)
AS
DECLARE @ResultValue int
BEGIN TRAN
IF EXISTS
(
SELECT * FROM Tax
WHERE TaxDescription = @TaxDescription
)
BEGIN
SET @ResultValue = 99
END
ELSE
BEGIN
INSERT INTO Tax(TaxDescription, TaxRate) VALUES (@TaxDescription, @TaxRate)
set @ResultValue = @@ERROR
END
IF @ResultValue <> 0
BEGIN
ROLLBACK TRAN
END
ELSE
BEGIN
COMMIT TRAN
END
RETURN @ResultValue
GOThe stored procedure has two parameters passed in, the @TaxDescription and @TaxRate. Beginning at line 19 a test is carried out to see if a record already exists where the description is the same as the parameter being passed in by the C# code. If a match is found it sets the @ResultValue to 99 and ends. If no match is found it will insert the new record (line 29) and set the @ResultValue to @@ERROR (which, rather misleadingly will be 0 to indicate that no error was found!)
Execute the query to change the stored procedure. Close the query window - there is no need to save the query.
Whilst we have SQL Management Studio open we should consider the sequence in which the Tax Rates are displayed. A present they are shown in descending order in which they were added.
- Open Programmability > Stored Procedures and right-click spTax_List.
- Select 'Modify' and change 'ORDER BY TaxId DESC' to 'ORDER BY TaxRate DESC, TaxDescription ASC' and execute the query. This will now show the Tax Rates by descending Rate, and then Description if there are two rates with the same value. (Change according to your own needs.)
TaxService.cs
Replace the whole of the TaxInsert method with the following.
public async Task<int> TaxInsert(string TaxDescription, Decimal TaxRate)
{
int Success = 0;
var parameters = new DynamicParameters();
parameters.Add("TaxDescription", TaxDescription, DbType.String);
parameters.Add("TaxRate", TaxRate, DbType.Decimal);
parameters.Add("@ReturnValue", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
using (var conn = new SqlConnection(_configuration.Value))
{
// Stored procedure method
await conn.ExecuteAsync("spTax_Insert", parameters, commandType: CommandType.StoredProcedure);
Success = parameters.Get<int>("@ReturnValue");
}
return Success;
}Note that Task has been changed from a boolean to an integer and that two parameters are being passed into the method (TaxDescription and TaxRate), rather than the whole Tax object. Note also that there is also a return parameter (Success) that will either be 0 for success or 1 for failure (i.e. a duplicate has been found.)
ITaxService.cs
To bring ITaxService into line with TaxService replace the TaxInsert line with the following:
Task<int> TaxInsert(string TaxDescription, Decimal TaxRate);TaxPages.razor
We need to amend the 'protected async Task TaxSave()' method, replacing the existing code with the following:
protected async Task TaxSave()
{
if (addeditTax.TaxID == 0)
{
int Success = await TaxService.TaxInsert(addeditTax.TaxDescription, addeditTax.TaxRate);
if (Success != 0)
{
//Tax Rate already exists
//WarningHeaderMessage = "Warning!";
//WarningContentMessage = "This Tax Description already exists; it cannot be added again.";
//Warning.OpenDialog();
// Data is left in the dialog so the user can see the problem.
}
else
{
// Clears the dialog and is ready for another entry
// User must specifically close or cancel the dialog
addeditTax = new Tax();
}
}
else
{
// Item is being edited
}
//Always refresh datagrid
tax = await TaxService.TaxList();
StateHasChanged();
}
The new TaxService returns an integer (assigned to a variable called Success). We can now test that value and react according to whether it is 0 (the insert has been successful) or not 0 (the stored procedure should return 99) in the case that the insert has failed because a duplicate would be created.
If the insert has been successful we can just close the dialog and reset the addeditTax object variable.
However, if a duplicate would have been added we need to tell the user, and to do this we can open a further dialog. One of the features of a Blazor application is that it is composed of a number of 'components' which can be re-used and inserted elsewhere in the application. A warning dialog is a good example of such a component and, as we are likely to need a similar warning for a number of reasons, we can create a 'Warning' component for this purpose. This will be a new Razor component for use by a number of other components, so we should create this in the Shared folder.
We will return to the TaxPage, but we need to create the Warning component.
WarningPage.razor
Right-click on the Shared folder in the object explorer and select Add > Razor Component and call it 'WarningPage'. Copy the code below into WarningPage.razor, replacing the automatically generated code.
@using Syncfusion.Blazor.Popups;
<SfDialog @ref="DialogWarning" @bind-Visible="@IsVisible" IsModal="true" Width="300px" ShowCloseIcon="true">
<DialogTemplates>
<Header> @WarningHeaderMessage </Header>
<Content>@WarningContentMessage</Content>
</DialogTemplates>
<DialogButtons>
<DialogButton Content="OK" IsPrimary="true" OnClick="@CloseDialog" />
</DialogButtons>
</SfDialog>
@code {
SfDialog DialogWarning;
public bool IsVisible { get; set; } = false;
[Parameter] public string WarningHeaderMessage { get; set; }
[Parameter] public string WarningContentMessage { get; set; }
public void OpenDialog()
{
this.IsVisible = true;
this.StateHasChanged();
}
public void CloseDialog()
{
this.IsVisible = false;
this.StateHasChanged();
}
}
The WarningPage consists of a Syncfusion Dialog within which is a <DialogTemplate> tag surrounding a <Header> and <Content> tags, both of which will display messages passed to the component. It is also bound to the IsVisible variable. The code section declares the name of the Dialog, the IsVisible varaible and the parameters for the warning messages, together with methods to open and close the dialog.
Back to TaxPage.razor
To incorporate the new warning component, copy and paste the following after the closing </SfDialog> tag. Note that <WarningPage must match the shared component file name, and the two variables must also match the parameters within the shared component.
<WarningPage @ref="Warning" WarningHeaderMessage="@WarningHeaderMessage" WarningContentMessage="@WarningContentMessage" />
Within the @code section we need to declare the component and the two variables for the header and content. Copy and paste the following, placing it under the other declarations.
WarningPage Warning;
string WarningHeaderMessage = "";
string WarningContentMessage = "";Lastly we need to un-comment the section of the method that deals with the situation where a duplicate is found.
if (Success != 0)
{
//Tax Rate already exists
WarningHeaderMessage = "Warning!";
WarningContentMessage = "This Tax Description already exists; it cannot be added again.";
Warning.OpenDialog();
// Data is left in the dialog so the user can see the problem.
}Save all the files and run the application. Try adding a new Tax Rate with a description that already exists.
Adding the 'Edit' function
Editing a Tax Rate will follow a similar pattern to adding, but with a few minor differences. The first difference is that the user will have to select a record from the DataGrid for editing. If the user doesn't select a record before clicking the 'Edit; button we will use the same Warning Dialog as we used previously to warn of a duplicate Tax Description, but with a different message.
Once a record has been selected we must determine the ID of the record and populate the add/edit dialog with the data to be edited. The possibility still remains that a duplicate record could be created and this must be prevented and the user warned.
Before tackling the TaxPage.razor, we will start by changing the SQL stored procedure for updating a tax record to prevent duplicates and report back to the user.
Tax_Update Stored Procedure
Open SQL Server Management Studio, select the PurchaseOrders database and open a New Query window and copy and paste the following code into it.
DROP PROCEDURE [dbo].[spTax_Update]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-------------- Stored Proc for UPDATE
CREATE PROCEDURE [dbo].[spTax_Update]
-- Parameters for Update stored procedure.
@TaxID int,
@TaxDescription nvarchar(50),
@TaxRate decimal(6, 4),
@TaxIsArchived bit
AS
DECLARE @ResultValue int
BEGIN TRAN
IF EXISTS
(
SELECT * FROM Tax
WHERE (TaxDescription = @TaxDescription) AND TaxID <> @TaxID
)
BEGIN
SET @ResultValue = 99
END
ELSE
BEGIN
UPDATE Tax SET TaxDescription = @TaxDescription, TaxRate = @TaxRate, TaxIsArchived = @TaxIsArchived WHERE TaxId = @TaxID
set @ResultValue = @@ERROR
END
IF @ResultValue <> 0
BEGIN
ROLLBACK TRAN
END
ELSE
BEGIN
COMMIT TRAN
END
RETURN @ResultValue
GOExecute the query to drop and re-create the stored procedure. Close the Query window; there is no need to save the query.
TaxService.cs
Replace the whole of the TaxUpdate method with the code below:
public async Task<int> TaxUpdate(string TaxDescription, decimal TaxRate, int TaxID, bool TaxIsArchived)
{
int Success = 0;
var parameters = new DynamicParameters();
parameters.Add("TaxDescription", TaxDescription, DbType.String);
parameters.Add("TaxRate", TaxRate, DbType.Decimal);
parameters.Add("TaxId", TaxID, DbType.Int32);
parameters.Add("TaxIsArchived", TaxIsArchived, DbType.Boolean);
parameters.Add("@ReturnValue", dbType: DbType.Int32, direction: ParameterDirection.ReturnValue);
using (var conn = new SqlConnection(_configuration.Value))
{
await conn.ExecuteAsync("spTax_Update", parameters, commandType: CommandType.StoredProcedure);
Success = parameters.Get<int>("@ReturnValue");
}
return Success;
}As for TaxInsert, note that Task has been changed from a boolean to an integer and that four parameters are being passed into the method (TaxDescription, TaxRate, TaxId and TaxIsArchived), rather than the whole Tax object. Note also that there is also a return parameter (Success) that will either be 0 for success or 1 for failure (i.e. a duplicate has been found.)
ITaxService.cs
Replace the TaxUpdate line with the following:
Task<int> TaxUpdate(string TaxDescription, Decimal TaxRate, int TaxID, bool TaxIsArchived);TaxPages.razor
We need to be able to identify which record a user has selected for editing from the DataGrid. To do this we need to amend the <GridEvents> to include a RowSelectHandler. Replace the existing <Grid Events> with this:
<GridEvents RowSelected="RowSelectHandler" OnToolbarClick="ToolbarClickHandler" TValue="Tax"></GridEvents>And add the following method to the end of the @code section.
public void RowSelectHandler(RowSelectEventArgs<Tax> args)
{
//{args.Data} returns the current selected records.
SelectedTaxId = args.Data.TaxID;
}We need to declare SelectedTaxId, so add the following to the declarations section.
public int SelectedTaxId { get; set; } = 0;The next thing to be changed is the ToolbarHandler to handle to user clicking 'Edit'. In the ToolBarHandler code insert the following code under the '//Code for editing goes here' comment.
//Check that a Tax Rate has been selected
if (SelectedTaxId == 0)
{
WarningHeaderMessage = "Warning!";
WarningContentMessage = "Please select a Tax Rate from the grid.";
Warning.OpenDialog();
}
else
{
//populate addeditTax (temporary data set used for the editing process)
HeaderText = "Edit Tax Rate";
addeditTax = await TaxService.Tax_GetOne(SelectedTaxId);
await this.DialogAddEditTax.Show();
}
This checks if SelectedTaxId is 0, i.e. the user hasn't selected a row from the DataGrid, and if so uses the Warning dialog to display a message informing the user to select a record.
If a user has selected a record, it provides header text for the addeditTax dialog and populates the addeditTax object with data for the relevant record, and lastly displays the dialog ready for editing.
We now need to change the TaxSave method to handle the edit situation. Enter the following code under the '//Item is being edited' comment
int Success = await TaxService.TaxUpdate(addeditTax.TaxDescription, addeditTax.TaxRate, SelectedTaxId, addeditTax.TaxIsArchived);
if (Success != 0)
{
//Tax Rate already exists
WarningHeaderMessage = "Warning!";
WarningContentMessage = "This Tax Description already exists; it cannot be added again.";
Warning.OpenDialog();
}
else
{
await this.DialogAddEditTax.Hide();
this.StateHasChanged();
addeditTax = new Tax();
SelectedTaxId = 0;
}TaxService.TaxUpdate attempts the update, but if Success is anything but 0 it warns the user that the TaxDescription already exists. Otherwise it updates the record, closes the dialog and resets addeditTax and SelectedTaxId.
Adding the 'Delete' function
As we have previously mentioned, we won't be actually deleting records from the SQL tables, but rather marking them as archived. To 'delete' a record we will be checking that the user has selected a row from the grid, and if they have we will then display a new dialog showing details of the selected item and requesting confirmation that they wish to continue to delete the record.
I have agonised over whether to use DialogAddEditTax and try to adapt it so that the text boxes displaying Tax Description and Rate are not enabled and to do something similar to hide/display different buttons, which in turn would pop up another dialog asking for confirmation of deletion. In the end it got complicated and difficult to read the code to determine exactly what was happening. I have therefore decided to add a new dialog to the form and abandon (possibly just for the moment) any idea of a reusable component asking for confirmation of deletion.
Add the new Syncfusion dialog, code below, immediately under the existing dialog (and before the <WarningPage> component
<SfDialog @ref="DialogDeleteTax" IsModal="true" Width="500px" ShowCloseIcon="true" Visible="false">
<DialogTemplates>
<Header> Confirm Delete </Header>
<Content>
<SfTextBox Enabled="false" Placeholder="Description"
FloatLabelType="@FloatLabelType.Always"
@bind-Value="addeditTax.TaxDescription"></SfTextBox>
<SfNumericTextBox Enabled="false" Placeholder="Tax Rate" Width="50"
Format="p2"
FloatLabelType="@FloatLabelType.Always"
@bind-Value="addeditTax.TaxRate"></SfNumericTextBox>
<br />
<br />
<span class="text-danger">Please confirm that you want to delete this record</span>
</Content>
</DialogTemplates>
<DialogButtons>
<DialogButton Content="Delete" IsPrimary="true" OnClick="@ConfirmDeleteYes" />
<DialogButton Content="Cancel" IsPrimary="false" OnClick="@ConfirmDeleteNo" />
</DialogButtons>
</SfDialog>This is basically the same as the DialogAddEditTax, except that I have added <Content> tags around the TextBoxes (I seemed to need this to add text at the bottom for the confirmation message) and different buttons with new OnClick events.
Add the declaration for the new dialog.
SfDialog DialogDeleteTax;Add the code in the ToolbarClickHandler for the Delete option.
//code for deleting goes here
if (SelectedTaxId == 0)
{
WarningHeaderMessage = "Warning!";
WarningContentMessage = "Please select a Tax Rate from the grid.";
Warning.OpenDialog();
}
else
{
//populate addeditTax (temporary data set used for the editing process)
HeaderText = "Delete Tax Rate";
addeditTax = await TaxService.Tax_GetOne(SelectedTaxId);
await this.DialogDeleteTax.Show();
}It checks that a row has been selected from the DataGrid, using the Warning component, as for Edit, if the user hasn't selected a record.
Assuming a record has been selected the new DialogDeleteTax is opened, using the existing addeditTax object and the TaxService Tax_GetOne.
Add the two new methods to handle the button click events in DialogDeleteTax.
public async void ConfirmDeleteNo()
{
await DialogDeleteTax.Hide();
SelectedTaxId = 0;
}
public async void ConfirmDeleteYes()
{
int Success = await TaxService.TaxUpdate(addeditTax.TaxDescription, addeditTax.TaxRate, SelectedTaxId, addeditTax.TaxIsArchived=true);
if (Success != 0)
{
//Tax Rate already exists
WarningHeaderMessage = "Warning!";
WarningContentMessage = "Unknown error has occurred - the record has not been deleted!";
Warning.OpenDialog();
}
else
{
await this.DialogDeleteTax.Hide();
tax = await TaxService.TaxList();
this.StateHasChanged();
addeditTax = new Tax();
SelectedTaxId = 0;
}
}Clicking 'Cancel' closes the dialog without any further action, and resets the SelectedTaxId variable. (This is to clear it from memory, to ensure a new row is selected if the user wants to Edit a record, for example.)
If the user confirms the deletion we use the existing TaxUpdate method, passing to it the final parameter that 'addeditTax.TaxIsArchived=true'. The situation where this creates a duplicate record should never occur, but just in case I have added a warning. The record update should succeed, so the final code closes the dialog, refreshes TaxList and resets the addeditTax object and SelectedTaxId.
Lastly, having deleted a Tax Rate we don't want it appearing in the DataGrid of tax rates. This is most easily achieved by modifying the Tax_List SQL stored procedure so that it only includes records where IsArchived = 0 (false). The SQL script to drop and recreate the stored procedure is shown below:
USE [PurchaseOrders]
GO
DROP PROCEDURE [dbo].[spTax_List]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[spTax_List]
--No parameters required.
AS
BEGIN
--SQL for Select stored procedure.
SELECT TaxID, TaxDescription, TaxRate, TaxIsArchived FROM Tax
WHERE TaxIsArchived = 0
ORDER BY TaxRate DESC, TaxDescription ASC
END
GOMore Dilemmas
Whilst testing the application a situation arose where I tried to add a new Tax record with the same Description as a record I had just deleted. In practise this might actually occur and without a mechanism to resurrect an archived record it would be wise to allow this kind of duplicate to be added. I have therefore modified both the Tax_Insert and Tax_Update stored procedures to prevent blocking duplicates being added where the original has been archived. The scripts to revise the stored procedures are shown below.
DROP PROCEDURE [dbo].[spTax_Insert]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[spTax_Insert]
(
@TaxDescription nvarchar(50),
@TaxRate decimal(6, 4)
)
AS
DECLARE @ResultValue int
BEGIN TRAN
IF EXISTS
(
SELECT * FROM Tax
WHERE TaxDescription = @TaxDescription and TaxIsArchived = 0
)
BEGIN
SET @ResultValue = 99
END
ELSE
BEGIN
INSERT INTO Tax(TaxDescription, TaxRate) VALUES (@TaxDescription, @TaxRate)
set @ResultValue = @@ERROR
END
IF @ResultValue <> 0
BEGIN
ROLLBACK TRAN
END
ELSE
BEGIN
COMMIT TRAN
END
RETURN @ResultValue
GO
DROP PROCEDURE [dbo].[spTax_Update]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-------------- Stored Proc for UPDATE
CREATE PROCEDURE [dbo].[spTax_Update]
-- Parameters for Update stored procedure.
@TaxID int,
@TaxDescription nvarchar(50),
@TaxRate decimal(6, 4),
@TaxIsArchived bit
AS
DECLARE @ResultValue int
BEGIN TRAN
IF EXISTS
(
SELECT * FROM Tax
WHERE (TaxDescription = @TaxDescription) AND TaxID <> @TaxID AND TaxIsArchived = 0
)
BEGIN
SET @ResultValue = 99
END
ELSE
BEGIN
UPDATE Tax SET TaxDescription = @TaxDescription, TaxRate = @TaxRate, TaxIsArchived = @TaxIsArchived WHERE TaxId = @TaxID
set @ResultValue = @@ERROR
END
IF @ResultValue <> 0
BEGIN
ROLLBACK TRAN
END
ELSE
BEGIN
COMMIT TRAN
END
RETURN @ResultValue
GOSave all files and run the application...
Project Code
The code for the stored procedures and C# for Tax Rates can be found here.
YouTube Videos
Blazor Project - Purchase Orders - Part 5 Create the TaxPage and programming the 'Add' function.
Blazor Project - Purchase Orders - Part 6 Completing the TaxPage by programming the 'Edit' and 'Delete' functions.


