Tidying-up

There are a few things we can do, relatively simply, to make the project marginally more visually appealing and to improve the data integrity.

Data Validation

The user can only enter data for country names, city names and city population, but so far we have no check that the user doesn't enter invalid data, such as names longer that the field length defined in the database, or populations outside a specified range (less than 1,000 or more than 25 million,for example)  

We could add 'business rules' as part of the logic of the data input forms, alternatively we can use the inbuilt server-side data validation offered by 'Data Annotations'; for our project we will use the 'Data Annotations' method.

Data validation using 'Data Annotations' has two basic steps:

  • Editing the Data Model
    • Adding 'using System.ComponentModel.DataAnnotations;' 
    • Placing data 'rules' in the Data Model class
    • Adding validation messages in the Data Model class
  • Editing the data entry form
    • Adding <DataAnnotationsValidator /> to the <EditForm> block
    • Adding a <ValidationMessage> to the EditForm> block

Data Model

The code generated by Alan Simpson's code generator automatically adds the following to data table model code (so we don't have to):

using System.ComponentModel.DataAnnotations;

The code generator also decorates the ID columns with [Required], although this is superfluous, as these are also set as Identity in the SQL database.

We don't want the CountryName or CityName to be left blank, so we will add the [Required] decorator above the code for these columns.  (Perhaps, we should have set the column to 'Not Null' when creating the SQL tables!)

The code generator added [StringLength (50)] above the name columns, but by adding a meaningful error message here it can be used in data entry forms.  Edit Countries.cs and Cities.cs as shown below.

[Required]
[StringLength(50, ErrorMessage = "Name is too long - it cannot be longer than 50 characters.")]

In the case of the cities model class we have a 'Population' column.  It makes sense to place a reasonableness test to this, checking that it is between, say, 10,000 and 25 million.  To add this restriction add the following to Population (we will also make it required to avoid the user leaving it blank.

[Required]
[Range(10000,25000000,ErrorMessage ="Population must be between 10,000 and 25 million")]

CountriesAddEdit.razor

Countries are added and edited using CountriesAddEdit.razor, and within that page is nested an EditForm within that  an SfDialog.  <DataAnnotationsValidator /> must be placed within the <EditForm> section.

To display the validation message the following needs to be placed in the <EditForm> section.  This will cater for both possible error conditions: the name is left blank or exceeds 50 characters.

<ValidationMessage For="@(() => countries.CountryName)" />

The code for the <EditForm> should look like this:

    <EditForm Model="@countries" OnValidSubmit="@CountriesSave">
        <DataAnnotationsValidator />
        <div>
            <SfTextBox Enabled="true" Placeholder="Country"
                       FloatLabelType="@FloatLabelType.Always"
                       @bind-Value="countries.CountryName"></SfTextBox>
            <ValidationMessage For="@(() => countries.CountryName)" />
        </div>
        <br /><br />
        <div class="e-footer-content">
            <div class="button-container">
                <button type="submit" class="e-btn e-normal e-primary">Save</button>
                <button type="button" class="e-btn e-normal" @onclick="@Cancel">Cancel</button>
            </div>
        </div>
    </EditForm>

CountriesAndCities.razor

Adding and editing cities uses the CountriesAndCities.razor, but unlike countries, uses two SfDialogs that get called depending on whether the user clicks the 'Add a City' or 'Edit a City' button.  We therefore need to add <DataAnnotataionsValidator /> and <ValidationMessage> to both dialogs.  In addition we need to add ValidationMessage for both the city name and population columns.  The code is shown below.  Note that the ValidationMessages are adapted for the mode being validated.

        <SfDialog @ref="DialogAddCity" IsModal="true" Width="500px" ShowCloseIcon="true" Visible="false">
            <EditForm Model="@addCities" OnValidSubmit="@CitiesSave">
                <DataAnnotationsValidator />
                <div>
                    <SfTextBox Enabled="true" Placeholder="City"
                               FloatLabelType="@FloatLabelType.Always"
                               @bind-Value="addCities.CityName"></SfTextBox>
                    <ValidationMessage For="@(() => addCities.CityName)" />
                    <SfNumericTextBox Enabled="true" Placeholder="Population" Width="50"
                                      FloatLabelType="@FloatLabelType.Always"
                                      @bind-Value="addCities.CityPopulation"></SfNumericTextBox>
                    <ValidationMessage For="@(() => addCities.CityPopulation)" />                
                </div>
                <br /><br />
                <div class="e-footer-content">
                    <div class="button-container">
                        <button type="submit" class="e-btn e-normal e-primary">Save</button>tech
                    </div>

                </div>
            </EditForm>
        </SfDialog>

        <SfDialog @ref="DialogEditCity" IsModal="true" Width="500px" ShowCloseIcon="true" Visible="false">
            <EditForm Model="@editCities" OnValidSubmit="@CitiesSaveEdit">
                <DataAnnotationsValidator />
                <div>
                    <SfTextBox Enabled="true" Placeholder="City"
                               FloatLabelType="@FloatLabelType.Always"
                               @bind-Value="editCities.CityName"></SfTextBox>
                    <ValidationMessage For="@(() => editCities.CityName)" />
                    <SfNumericTextBox Enabled="true" Placeholder="Population" Width="50"
                                      FloatLabelType="@FloatLabelType.Always"
                                      @bind-Value="editCities.CityPopulation"></SfNumericTextBox>
                    <ValidationMessage For="@(() => editCities.CityPopulation)" />
                </div>
                <br /><br />
                <div class="e-footer-content">
                    <div class="button-container">
                        <button type="submit" class="e-btn e-normal e-primary">Save</button>

                    </div>
                </div>
            </EditForm>
        </SfDialog>ialog>

Styling

By default Blazor installs and uses Bootstrap 4 for styling.  Like my knowledge of C#, I know virtually nothing about CSS and Bootstrap, so very little styling has been applied to the project so far.  And not a lot is about to be added - just the minimum required to make it look a little bit better!

The first item to be tackled is that both the countries and cities data-grids are the full width of the screen.  Bootstrap splits the width of the screen into 12 columns, and by wrapping the page titles and, separately, the data-grids in <div class="col-sm-8"> tags, the titles and data-grids will only extend the width of 8 columns, i.e. two thirds of the screen's width.  (The 'sm' part refers to the screen size - see Bootstrap Documentation for more information.)

I have also added a <br> tag after the page titles and after the drop-down list to insert a blank line after the title.

There is probably a lot more that could be done to improve the look and feel of this, but with minimal effort I don't think the project looks too bad.

Adding a total for Population

The Synfusion data-grid has an inbuilt 'Aggregate' function that allows totals, averages, minimum, maximum, count, etc. to be displayed in the grid footer (or group footer).  In our example copy and paste the following code after the closing </GridColumns> tag to display the total city population by country.

            <GridAggregates>
                <GridAggregate>
                    <GridAggregateColumns>
                        <GridAggregateColumn Field=@nameof(Cities.CityPopulation) Type="AggregateType.Sum" Format="N">
                            <FooterTemplate>
                                @{
                                    var aggregate = (context as AggregateTemplateContext);
                                    <div>
                                        <p>Total Cities Population: @aggregate.Sum</p>
                                    </div>
                                }
                            </FooterTemplate>
                        </GridAggregateColumn>
                    </GridAggregateColumns>
                </GridAggregate>
            </GridAggregates>

Lastly...

Add filter to drop-down list

By including 'AllowFiltering="true"' in the <SfDropDownList> tag a filter box will be added to the top of the dropdown list.

Tidy the Countries data-grid

The user doesn't need to know the CountryId and the CountryID column should be removed.  To do this, simply delete the <GridColumn Field="@nameof(Countries.CountryId)"... section. 

We were probably too conservative with the number of records to show on one page.  Increase the "PageSize" to 10.

The grid filtering currently uses a row at the top of the grid for the user to enter filtering criteria and it works by the user entering the start of a country name and pressing the return key.  An alternative can be used that displays a pop-up box where the user can enter whether the criteria is 'starts with', 'contains', 'ends with', etc.  To use this alternative filtering method add the following after <GridPageSettings> (or anywhere within the <SfGrid> tag).

<GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>

YouTube Video

Blazor + Syncfusion + Dapper: Part 11