Razor Pages using Dropdowns

Karen Payne - Jul 20 '23 - - Dev Community

Learn how to use DropDown/Select List which permits a user to select a single value. For demonstration there will be two DropDown list to select a start and end time which can be useful for enter time for a timesheet application. FluentValidation library is used for model validation e.g. end time cannot be less than start time and start time cannot be lower than 8 AM.

No scripting used, everything presented is strongly typed.

GitHub source code

Screen shot

Displaying two dropdown list for selecting hours

Define class for displaying data in a DropDown

For versatility the class can present whole hours, half hours and quarter hours.

To allow a choice of hours, hours with half hours or hours with half and quarter hours the following Enum is used.

public enum TimeIncrement
{
    Hourly,
    Quarterly,
    HalfHour
}
Enter fullscreen mode Exit fullscreen mode

For returning hours, the following model is used.

public class HourItem
{
    public int Id { get; set; }
    public string Text { get; set; }
    public TimeSpan? TimeSpan { get; set; }
    public override string ToString() => Text;
}
Enter fullscreen mode Exit fullscreen mode

Class for generating hours

There are three methods defined to return quarterly, half hour and hourly which are not used here, instead the Choice method is used which obtains a string value from appsettings.json and converted to TimeIncrement Enum.

using HoursApplication.Models;

namespace HoursApplication.Classes;

public class Hours
{
    /// <summary>
    /// Creates an list quarter hours
    /// </summary>
    public static List<HourItem> Quarterly => Range(TimeIncrement.Quarterly);
    /// <summary>
    /// Creates an list of hours
    /// </summary>
    public static List<HourItem> Hourly => Range();
    /// <summary>
    /// Creates an list of half-hours
    /// </summary>
    public static List<HourItem> HalfHour => Range(TimeIncrement.HalfHour);

    public static List<HourItem> Choice(TimeIncrement timeIncrement) => Range(timeIncrement);

    private static string TimeFormat { get; set; } = "hh:mm tt";

    public static List<HourItem> Range(TimeIncrement timeIncrement = TimeIncrement.Hourly)
    {

        IEnumerable<DateTime> hours = Enumerable.Range(0, 24).Select((index) => (DateTime.MinValue.AddHours(index)));

        var hoursList = new List<HourItem> { new() {Text ="Select", Id = -1, TimeSpan = null} };
        foreach (var dateTime in hours)
        {

            hoursList.Add(new HourItem() { Text = dateTime.ToString(TimeFormat), TimeSpan = dateTime.TimeOfDay, Id = (int)dateTime.TimeOfDay.TotalMinutes });

            if (timeIncrement == TimeIncrement.Quarterly)
            {
                hoursList.Add(new HourItem() { Text = dateTime.AddMinutes(15).ToString(TimeFormat), TimeSpan = dateTime.AddMinutes(15).TimeOfDay, Id = (int)dateTime.AddMinutes(15).TimeOfDay.TotalMinutes });
                hoursList.Add(new HourItem() { Text = dateTime.AddMinutes(30).ToString(TimeFormat), TimeSpan = dateTime.AddMinutes(30).TimeOfDay, Id = (int)dateTime.AddMinutes(30).TimeOfDay.TotalMinutes });
                hoursList.Add(new HourItem() { Text = dateTime.AddMinutes(45).ToString(TimeFormat), TimeSpan = dateTime.AddMinutes(45).TimeOfDay, Id = (int)dateTime.AddMinutes(45).TimeOfDay.TotalMinutes });
            }
            else if (timeIncrement == TimeIncrement.HalfHour)
            {
                hoursList.Add(new HourItem() { Text = dateTime.AddMinutes(30).ToString(TimeFormat), TimeSpan = dateTime.AddMinutes(30).TimeOfDay, Id = (int)dateTime.AddMinutes(30).TimeOfDay.TotalMinutes });
            }
        }

        return hoursList;

    }
}
Enter fullscreen mode Exit fullscreen mode

To get the type of hours, this is stored in appsettings.json under the section AppSettings:TimeIncrement.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",

  "AppSettings": {
    "Title": "Select example",
    "TimeIncrement": "HalfHour"
  }
}
Enter fullscreen mode Exit fullscreen mode

Rigging up in the Index Page backend code.

Setup a property for the type of hours to use.

public TimeIncrement SelectedTimeIncrement { get; set; }
Enter fullscreen mode Exit fullscreen mode

Setup a property used to read appsettings.json

private readonly Appsettings _appSettings;
Enter fullscreen mode Exit fullscreen mode

In Prgram.cs, setup for reading AppSettings from appsettings.json

builder.Services.Configure<Appsettings>(builder.Configuration.GetSection(nameof(Appsettings)));
Enter fullscreen mode Exit fullscreen mode

Back in Index Page, a constructor is needed to pass in IOptions and convert the value in appsettings.json to an enum.

SelectedTimeIncrement = Enum.TryParse<TimeIncrement>(_appSettings.TimeIncrement, true,
    out var selection) ?
    selection :
    TimeIncrement.Hourly;
Enter fullscreen mode Exit fullscreen mode

If the value in appsettings.json does not represent a member of TimeIncrement, Hourly is used.

Properties used which provide data sources for the DropDown list below.

public SelectList StartHoursList { get; set; }
public SelectList EndHoursList { get; set; }
Enter fullscreen mode Exit fullscreen mode

The method to load hours to the two properties above called from OnGet.

private void LoadSelects()
{
    StartHoursList = new SelectList(Hours.Choice(SelectedTimeIncrement), "TimeSpan", "Text");
    StartHoursList.FirstOrDefault()!.Disabled = true;
    EndHoursList = new SelectList(Hours.Choice(SelectedTimeIncrement), "TimeSpan", "Text");
    EndHoursList.FirstOrDefault()!.Disabled = true;
}
Enter fullscreen mode Exit fullscreen mode

Validation

  1. Setup a instance of our validator
  2. Pass in Start and EndTime from our model Container to
  3. If valid pass Container to the About Page using json
  4. If not valid, load the DropDowns up and set to the invalid value(s).
  5. Present page again with annotated error messages.
public IActionResult OnPost()
{

    TimesContainerValidator validator = new TimesContainerValidator();

    var result = validator.Validate(Container);

    if (result.IsValid)
    {
        var container = new TimesContainer()
        {
            StartTime = Container.StartTime, 
            EndTime = Container.EndTime
        };


        return RedirectToPage("About", new
        {
            container = JsonSerializer.Serialize(container, new JsonSerializerOptions
            {
                WriteIndented = true
            })
        });
    }


    result.AddToModelState(ModelState, nameof(Container));

    LoadSelects();

    var selectedStartHour = StartHoursList.FirstOrDefault(x => x.Text == Container.StartTime.FormatAmPm());
    if (selectedStartHour != null)
    {
        selectedStartHour.Selected = true;
    }


    var selectedEndHour = EndHoursList.FirstOrDefault(x => x.Text == Container.EndTime.FormatAmPm());
    if (selectedEndHour != null)
    {
        selectedEndHour.Selected = true;
    }

    return Page();

}
Enter fullscreen mode Exit fullscreen mode

Rigging up in the Index Page frontend code.

Create a select/dropdown for start time.

<select name="StartTime"
        id="startHours"
        class="form-select"
        asp-for="@Model.Container.StartTime"
        asp-items="@Model.StartHoursList">
</select>
Enter fullscreen mode Exit fullscreen mode

Create a select/dropdown for endtime.

<select name="EndTime"
        id="endHours"
        class="form-select"
        asp-for="@Model.Container.EndTime"
        asp-items="@Model.EndHoursList">
</select>
Enter fullscreen mode Exit fullscreen mode

Completed

That's it other than suppose you want to have an item that indicates a selection needs to be made as the code sample above defaults to 12 AM for both DropDowns.

DropDown with select option enabled

Our model this time is an Enum using Html.GetEnumSelectList to populate the DropDown.

public enum BookCategories
{
    [Description("Options")]
    Select = 0,
    [Display( Name = "Space Travel")]
    SpaceTravel = 1,
    [Display( Name = "Adventure")]
    Adventure = 2,
    [Display(Name = "Popular sports")]
    Sports = 3,
    [Display( Name = "Cars")]
    Automobile = 4,
    [Display( Name = "Programming with C#")]
    Programming = 5
}
Enter fullscreen mode Exit fullscreen mode

Index page front end

<form method="post"  class="row">
    <div class="col-4">

        <div>

            <label asp-for="Book.Title" class="mt-2"></label>
            <input asp-for="Book.Title" class="form-control"/>

            <label asp-for="Book.Category" class="mt-2"></label>

            <select asp-for="Book.Category"
                    class="form-select"
                    asp-items="Html.GetEnumSelectList<BookCategories>()">
            </select>
        </div>

        <div class="mt-2">
            <input type="submit" value="Submit" class="btn btn-primary" />
        </div>

    </div>

    <div class="mt-2">
        <p>@Html.Raw(Model.Message)</p>
    </div>
</form>
Enter fullscreen mode Exit fullscreen mode

OnPost for Index page

We simply check if the Select member is the selected item rather than checking if the model state is valid. When implementing with other inputs best to incorporate into is the model state valid as per the first code sample.

public IActionResult OnPost()
{
    if (Book.Category == BookCategories.Select)
    {
        Message = "Please select a <span class=\"text-danger fw-bold\">category</span>";
        return Page();
    }

    return RedirectToPage("Receiving", new { category = Book.Category });
}
Enter fullscreen mode Exit fullscreen mode

Inject into a view

The following code sample injections the service into a view.

displays to select elements

Service

public interface ICountryService
{
    public List<SelectListItem> Countries { get; set; }
    public string Name { get; set; }
    public string Iso { get; set; }
}
public class CountriesModel : ICountryService
{
    public List<SelectListItem> Countries { get; set; }
    public string Name { get; set; }
    public string Iso { get; set; }

    public CountriesModel()
    {
        using var context = new Context();

        Countries = context.Countries.Select(a =>
            new SelectListItem
            {
                Value = a.Iso,
                Text = a.Name
            }).ToList();
    }
}

Enter fullscreen mode Exit fullscreen mode

Program.cs

Register ICountryService service

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add services to the container.
        builder.Services.AddRazorPages();
        builder.Services.AddScoped<ICountryService, CountriesModel>();
Enter fullscreen mode Exit fullscreen mode

Index Page

In the view add

@inject ICountryService CountriesModel
Enter fullscreen mode Exit fullscreen mode

Construct the DropDown

<div class="row g-3 align-items-center mb-2 mt-1">
    <div class="col-3 text-end ms-1">
        @* ReSharper disable once Html.IdNotResolved *@
        <label for="countries">Countries:</label>
    </div>
    <div class="col me-2">
        @Html.DropDownListFor(model => 
            model.CountryCode1, 
            Model.CountryList,
            new {
                @class = "form-select",
                style = "width:18.5em;",
                id = "countries"
            })
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Index code behind

In the actual front end code there are two DropDowns, for the DropDown shown above the following property CountryCode1 is the selected item while CountryCode2 is for a conventional HTML tag version of a DropDown.

public class IndexModel : PageModel
{
    public readonly ICountryService CountryService;
    public List<SelectListItem> CountryList { get; set; }
    [BindProperty]
    public string CountryCode1 { get; set; }
    [BindProperty]
    public string CountryCode2 { get; set; }

    public IndexModel(ICountryService countryService)
    {
        CountryService = countryService;
        CountryList = countryService.Countries;
    }

    public void OnGet()
    {

    }

    public void OnPost()
    {
       Log.Information("Code 1 {P1}", CountryCode1);
       Log.Information("Code 2 {P1}", CountryCode2);
    }
}
Enter fullscreen mode Exit fullscreen mode

Source code

Clone the following GitHub repository

Related

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .