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.
Screen shot
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
}
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;
}
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;
}
}
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"
}
}
Rigging up in the Index Page backend code.
Setup a property for the type of hours to use.
public TimeIncrement SelectedTimeIncrement { get; set; }
Setup a property used to read appsettings.json
private readonly Appsettings _appSettings;
In Prgram.cs, setup for reading AppSettings from appsettings.json
builder.Services.Configure<Appsettings>(builder.Configuration.GetSection(nameof(Appsettings)));
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;
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; }
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;
}
Validation
- Setup a instance of our validator
- Pass in Start and EndTime from our model Container to
- If valid pass Container to the About Page using json
- If not valid, load the DropDowns up and set to the invalid value(s).
- 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();
}
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>
Create a select/dropdown for endtime.
<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.
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
}
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>
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 });
}
Inject into a view
The following code sample injections the service into a view.
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();
}
}
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>();
Index Page
In the view add
@inject ICountryService CountriesModel
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>
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);
}
}
Source code
Clone the following GitHub repository