A few more wrinkles
Introduction
This application is probably proof that a software project is never finished; there is always another bug to fix, improvement to be carried out, or if one is brave, a review and revision of code. In this post we will:
- Provide a default tax code and tax rate for an order line - and in the process move some configuration data from appsettings.json to a table that can be maintained by the user.
- Use 'switch' in place of 'if'.
- Trap a flaw that currently allows an order to be emailed to a supplier with a blank email address.
YouTube Video
Configuration Settings
One of the comments to this YouTube Video (Blazor Purchase Orders - Part 15 - Editing and Deleting Order Lines) alighted on the fact that when an order is added the first order line is not provided with a default tax code; in fact the tax code is specifically set to 0 ("addeditOrderLine.POLineTaxID = 0;"). The suggestion was made that to get round this behaviour that POLineLineTaxID should be set to '1' (assuming that was the required tax code). However, that didn't solve the problem completely because simply setting the TaxID to '1' didn't populate the tax rate, and this also needed setting using "POLineTaxRate = (decimal) 0.2;".
Whilst applauding the commenter's observation and understanding of the problem, I think there are two problems here. The first is that I think setting values like this in code is too restricting; the end user has no control over the default tax code, it's set by the programmer. Secondly, I think it is potentially dangerous; suppose the tax rate changes, to say 25%. An admin user would, quite rightly go to the Tax Rates form and change the tax rate percentage, expecting it change the rate for new order lines, but it wouldn't, the first line would still be at 20%. The problem here is that when a tax rate change occurred the C# code would also need changing and a new release of the software required.
We can get round the second problem by reading the tax rate from the Tax table when initialising a new order, and we will come to that later, but the first part of the problem got me thinking.
My initial reaction was that we could put the default TaxID in appsettings.json, but on reflection that is just as inaccessible to the user as the C# code. It also occurred to me that much of the configuration data that we record in appsettings.json also falls into this category. I think some of these settings would be better placed in a SQL table and a user form provided to allow easy changes when required.
Adding a Control Table
The data I think we should move from appsettings.json to a Control table includes:
- SmtpHost
- SmtpPort
- SmtpUserFriendlyName
- SmtpUserEmailAddress
- SmtpPass
- SENDGRID_APIKEY
- SenderEmail
- SenderEmailFriendlyName
Plus
- DefaultTaxID
- EmailSendMethod (to allow the user to choose between SendGrid and MailKit email methods)
We could also add further information, such as company name and address that could be used on the emailed purchase order, but for the time being I will resist this temptation! I will also resist the temptation to change the names of some of these variables, (SmtpPass to SmtpPassword, for example).
Here is the SQL code to create the new ControlTable. Note that this script drops the table if it exists and adds a single record. We will only ever have one record in this table (so long as we aren't tempted to make the application multi-company or multi-tenancy - but that's a different story.)
We will use the code generator created by Alan Simpson to create the SQL stored procedures and C# code for using Dapper. (Model class, service and interface), as we did in this post, but with one major modification. ControlTable should only ever have one record, and it should always have ID = 1; however, to guard against this ever getting set to something other than 1 I have changed the stored procedure for 'GetOne' so that it gets the 'Top(1)' record, descending. This should ensure it always gets the most recently added record if anything goes wrong. The 'service' and 'interface' files are amended to handle this change. The files created by using the code generator are:
- SQL to create the stored procedures
- Model for ControlTable (amended to add DataAnnotations)
- Service for ContolTable. We only need methods to retrieve one record and to update it.
- Interface for ControlTable
Don't forget to add the following to Startup.cs.
services.AddScoped<IControlTableService, ControlTableService>();Control Table maintenance form
With the SQL table in place, we need a form to maintain ControlTable. The ControlTable has only one record, so it makes sense for the form to load that record and for the only action to be an update.
To make the form more of a challenge, the Default TaxID will be a combobox showing the Tax Description and Rate, and the choice of EmailSendMethod a drop-down list.
In Visual Studio, select the Pages folder, right-click, select add and then Razor Component. Call it ControlTablePage.razor. Copy and paste the code from here:
I have also amended NavMenu.razor to include access to the new form for users who are in the 'Admin' role.
The ControlTablePage should look like this, when logged in as an Admin user:
With regard to the code for this page the points to note are
- When the page is initialized, I use the "ControlTable_GetOne()" service to load the single ControlTable record.
- The 'Select Email Method' drop-down is populated by a list of hard-coded items (SMTP and SendGrid).
- The 'Default Tax Rate' drop-down is populated by declaring an IEnumerable for taxrate and using the TaxService.TaxList() to get all tax rates when the page is initiliazied.
- The Email password and SendGrid API Key fields are obscured using [Type="InputType.Password"] in the Syncfusion textbox definitions for these fields.
- Just in case anything goes wrong with the saving of the record, the Warning component is used to warn the user of the failure to save the record.
Adding the default Tax Rate
This is the full code for the modified PurchaseOrderPage. In brief, the changes required to populate both the tax description and the tax rate are:
- Inject the ControlTableService at the top of the page
- Declare variables for the ControlTable object, and default tax ID and rate
- On the page initialization
- we use the 'ControlTable_GetOne' to populate the 'controltable' variable with our one record
- read the default 'TaxID' from that record
- use the default 'TaxID' to look up the 'Tax Rate' from the list of 'tax' objects (already populated as part of the initialization).
- With the default 'TaxID' and 'TaxRate' variables populated, the 'ToolbarClickHandler' can be amended so that where a new order line is being added the 'addeditorderline' values for 'TaxID' and 'TaxRate' can be set to our variables.
Reading and using Email settings from Control Table
Now that we have moved the email settings from appsettings.json to a control table, we will need to make some fairly extensive changes to EmailSenderMailKit.cs and EmailSenderSendGrid.cs as well as Index.razor. Click on the links to see the complete code for these files.
EmailSenderMailKit
The principal changes to EmailSenderMailKit are that the variables for SmtpHost, SmtpPort, etc. are no longer being read from appsettings.json, but will be passed to the interface from the index page. There is therefore no need for the IConfiguration lines as well as the associated 'using' statements at the top of the file. Instead the 'Send' method is modified to accept the parameters being passed from the Index page.
EmailSenderSendGrid
The changes required for EmailSenderDendGrid are similar to those required for EmailSenderMailKit and are shown below.
Index.razor
The changes needed for Index.razor are:
- Inject the IControlTableService ControlTableService
- Declare the variables we will be using
- On the page initialization use the ControlTableService to get the control record and populate the variables
- On the ToolBarClickHandler for the Email event
- I have added a boolean called 'emailSent' and set it to 'false'
- Added a 'switch' statement to handle the 'EmailSendMethod', and for both 'SMTP' and 'SendGrid', I am calling the EmailService/EmailSender and setting the boolean to 'true' if the email is sent successfully. As a guard against the Control Table not having a setting for Email Type I have a 'default' section in the switch options which will display the Warning component.
- Depending of whether the email has been sent successfully, or not, the Toast component will be displayed with a suitable message.
I could have used an 'if' statement in place of the 'switch' but thought I would use 'switch' just for a change! A 'switch' statement would appear to be a better option where the number of possibilities is more than, say, two, as it avoids multiple 'elseif' statements.
Handling missing supplier email address
Whilst making the changes for ControlTable I came across a bug where I attempted to send an email where the supplier email address was missing. This was easily rectified by placing an 'if' statement at the top of the code to send the email. The code is shown below. However, I also decided, in the spirit of easy to read 'if' statements, that the new check for missing email address should simply stop the code after displaying an error warning, rather than having an 'else' statement following it. And having made that decision I also simplified the check that the user had selected an order from the grid.
And Finally...
There is no need for a number of lines in appsettings.json, so these can be removed; specifically all the 'Smtp' and 'SendGrid' settings.
Conclusion
The introduction of the Control Table seems to have been a lot of hard work for seemingly little gain. However, I am sure this has been the right decision; if something like a tax rate, email address or password changes it is much simpler for the user to update a control table rather than a programmer changing appsettings.json.
There are probably lots of places in my code where I could simplify the code by replacing 'if' statements with 'switch' statements, or by rephrasing an 'if' statement by adding a 'return' statement and following with a separate 'if' statement in place of 'elseif'. I haven't been through this exercise but feel I probably should...












