Razor Pages using Dropdowns
Tags: webdev, dotnetcore, csharp, codenewbie
Published: 2023-07-20 19:14:55

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](https://docs.fluentvalidation.net/en/latest/index.html) 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.

{% cta https://github.com/karenpayneoregon/dropdowns-razor-pages %} GitHub source code {% endcta %}

## Screen shot

![Displaying two dropdown list for selecting hours](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6p25b1j8tbwf491df850.png)

## 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.

```csharp
public enum TimeIncrement
{
    Hourly,
    Quarterly,
    HalfHour
}
```

For returning hours, the following model is used.

```csharp
public class HourItem
{
    public int Id { get; set; }
    public string Text { get; set; }
    public TimeSpan? TimeSpan { get; set; }
    public override string ToString() => Text;
}
```

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.

```csharp
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;

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

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

  "AppSettings": {
    "Title": "Select example",
    "TimeIncrement": "HalfHour"
  }
}
```
## Rigging up in the Index Page backend code.

Setup a property for the type of hours to use.

```csharp
public TimeIncrement SelectedTimeIncrement { get; set; }
```

Setup a property used to read appsettings.json

```csharp
private readonly Appsettings _appSettings;
```

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

```csharp
builder.Services.Configure<Appsettings>(builder.Configuration.GetSection(nameof(Appsettings)));
```

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

```csharp
SelectedTimeIncrement = Enum.TryParse<TimeIncrement>(_appSettings.TimeIncrement, true,
    out var selection) ?
    selection :
    TimeIncrement.Hourly;
```

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.

```csharp
public SelectList StartHoursList { get; set; }
public SelectList EndHoursList { get; set; }
```

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

```csharp
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;
}
```

### Validation

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

```csharp
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();

}
```


## Rigging up in the Index Page frontend code.

Create a select/dropdown for start time.

```html
<select name="StartTime"
        id="startHours"
        class="form-select"
        asp-for="@Model.Container.StartTime"
        asp-items="@Model.StartHoursList">
</select>
```
Create a select/dropdown for endtime.

```html
<select name="EndTime"
        id="endHours"
        class="form-select"
        asp-for="@Model.Container.EndTime"
        asp-items="@Model.EndHoursList">
</select>
```

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yraswlzpg5cu0zklr7lm.png)

Our model this time is an Enum using [Html.GetEnumSelectList](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.viewfeatures.htmlhelper.getenumselectlist?view=aspnetcore-7.0) to populate the DropDown.

```csharp
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
}
```
## Index page front end

```csharp
<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>
```

## 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. 

```csharp
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 });
}
```

## Inject into a view

The following code sample injections the service into a view.


![displays to select elements](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/icx8m7dku3c7f2g0dm3a.png)

## Service

```csharp
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();
    }
}

```

## Program.cs

Register ICountryService service

```csharp
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>();
```

## Index Page

In the view add

```csharp
@inject ICountryService CountriesModel
```

Construct the DropDown

```html
<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>
```

### 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.

```csharp
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);
    }
}
```




## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/dropdowns-razor-pages)

## Related

- [FluentValidation tips C#](https://dev.to/karenpayneoregon/fluentvalidation-tips-c-3olf)

- [ASP.NET Core/Razor pages working with Checkboxes](https://dev.to/karenpayneoregon/aspnet-corerazor-pages-working-with-checkboxes-3ck4)