1. はじめに
Fluent UI Blazor
を用いてExcelのようなフィルタリング機能・ソート機能を持つデータグリッドの実装を行う方法をご紹介します。
2. Fluent UI Blazorとは
Fluent UI Blazor
は、MicrosoftのFluent UI Web Components
に基づいたBlazor向けのUIコンポーネントライブラリです。
高いアクセシビリティ、一貫性、レスポンシブデザイン、カスタマイズ性を提供し、効率的なWebアプリケーション開発をサポートします。
Fluent UI Blazor
の導入方法に関しては以下の記事をご覧ください。
3. ソースコード
本ソースコードは、プロジェクトテンプレートBlazor Web App
に含まれるServerProject\Components\Pages\Weather.razor
の実装を元にしています。
@page "/weather"
@rendermode CustomRenderingMode.InteractiveServerWithoutPrerendering
@using Microsoft.Fast.Components.FluentUI
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<FluentStack Orientation="Orientation.Horizontal" Width="1600px">
<div style="overflow-x: auto;">
<FluentDataGrid Style="height: 700px;width:1200px;overflow:auto;" Items="@FilteredItems?.OrderBy(x => x.Date).AsQueryable()" GenerateHeader="GenerateHeaderOption.Sticky">
<ChildContent>
<PropertyColumn Title="Date" Sortable="true" Property="@(c => DateTimeToString(c.Date.ToLocalTime()))" Align="Align.Start" Class="multiline-text">
<ColumnOptions>
<div class="search-box">
<FluentStack Orientation="Orientation.Vertical">
<FluentLabel Style="margin-top:5px;">
Date
</FluentLabel>
<FluentDatePicker @bind-Value="FilterDateFrom" />
<FluentLabel Style="margin-top:5px;">
~
</FluentLabel>
<FluentDatePicker @bind-Value="@FilterDateTo" />
</FluentStack>
</div>
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Title="℃" Property="@(c => c.TemperatureC)" Sortable="true" Align="Align.Start" Filtered="!string.IsNullOrWhiteSpace(temperatureCFilter)" Class="multiline-text">
<ColumnOptions>
<div class="search-box">
<FluentSearch type="search" Autofocus=true @bind-Value="temperatureCFilter" @oninput=@(x => HandleFilter(x, out temperatureCFilter)) @bind-Value:after=@(() => HandleClear(out temperatureCFilter)) Maxlength="255" />
</div>
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Title="℉" Property="@(c => c.TemperatureF)" SortBy="@temperatureFSort" Align="Align.Start" Filtered="!string.IsNullOrWhiteSpace(temperatureFFilter)" Class="multiline-text">
<ColumnOptions>
<div class="search-box">
<FluentSearch type="search" Autofocus=true @bind-Value="temperatureFFilter" @oninput=@(x => HandleFilter(x, out temperatureFFilter)) @bind-Value:after=@(() => HandleClear(out temperatureFFilter)) Maxlength="10" />
</div>
</ColumnOptions>
</PropertyColumn>
<PropertyColumn Title="Summary" Property="@(c => GetSummariesStr(c.Summary))" SortBy="@summarySort" Align="Align.Start" Class="multiline-text">
<ColumnOptions>
<div class="search-box" style="height:325px;">
<FluentSelect TOption="string"
Items="@SummarieItems"
Multiple="true"
OptionValue="@(p => p)"
OptionText="@(p => p)"
OptionSelected="@(p => true)"
@bind-SelectedOptions="@SelectedSummary"
Style="position:relative; overflow: visible;" />
</div>
</ColumnOptions>
</PropertyColumn>
</ChildContent>
<EmptyContent>
<FluentLabel> 該当データがありません。</FluentLabel>
</EmptyContent>
<LoadingContent>
<FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
<FluentProgressRing Width="120px" />
</FluentStack>
</LoadingContent>
</FluentDataGrid>
</div>
</FluentStack>
@code {
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string TemperatureF => (32 + (int)(TemperatureC / 0.5556)).ToString();
public int Summary => GetSummaries(TemperatureC);
}
public enum summariesEnum : uint
{
Freezing = 1,
Bracing,
Chilly,
Cool,
Mild,
Warm,
Balmy,
Hot,
Sweltering,
Scorching,
Unknown = 100,
}
private List<WeatherForecast>? forecasts = new List<WeatherForecast>();
//表示用データを生成する
protected override async Task OnInitializedAsync()
{
if (!forecasts.Any())
{
for (int i = 0; i < 10; i++)
{
WeatherForecast temp = new WeatherForecast()
{
Date = DateTime.UtcNow.AddDays(i),
TemperatureC = Random.Shared.Next(-20, 55),
};
forecasts.Add(temp);
}
}
}
//Summary判定用関数
public static int GetSummaries(int temperatureC)
{
if (-20 <= temperatureC && temperatureC <= -1)
{
//Freezing
return 1;
}
if (0 <= temperatureC && temperatureC <= 5)
{
//Bracing
return 2;
}
if (6 <= temperatureC && temperatureC <= 10)
{
//Chilly
return 3;
}
if (11 <= temperatureC && temperatureC <= 15)
{
//Cool
return 4;
}
if (16 <= temperatureC && temperatureC <= 20)
{
//Mild
return 5;
}
if (21 <= temperatureC && temperatureC <= 25)
{
//Warm
return 6;
}
if (26 <= temperatureC && temperatureC <= 30)
{
//Balmy
return 7;
}
if (31 <= temperatureC && temperatureC <= 35)
{
//Hot
return 8;
}
if (36 <= temperatureC && temperatureC <= 45)
{
//Sweltering
return 9;
}
if (46 <= temperatureC && temperatureC <= 55)
{
//Scorching
return 10;
}
//Unknown
return 100;
}
//Summary表示用関数
string GetSummariesStr(int summary)
{
switch (summary)
{
case 1:
return "Freezing";
case 2:
return "Bracing";
case 3:
return "Chilly";
case 4:
return "Cool";
case 5:
return "Mild";
case 6:
return "Warm";
case 7:
return "Balmy";
case 8:
return "Hot";
case 9:
return "Sweltering";
case 10:
return "Scorching";
default:
return "Unknown";
}
}
//フィルタリング用定義
//各フィルタリング処理を行いリストにする
List<WeatherForecast>? FilteredItems =>
forecasts?.Where(x => x.TemperatureC.ToString().Contains(temperatureCFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => x.TemperatureF.ToString().Contains(temperatureFFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => SummaryFilter(GetSummariesStr((int)x.Summary)))
.Where(x => DateFromFilter(FilterDateFrom) <= x.Date && x.Date < DateToFilter(FilterDateTo)).ToList();
DateTime? FilterDateTo;
DateTime? FilterDateFrom;
//日時のフィルタリング条件値のバリデーション処理
DateTime? DateFromFilter(DateTime? from)
{
if (from is null)
{
from = DateTime.MinValue.ToLocalTime();
}
return from;
}
DateTime? DateToFilter(DateTime? to)
{
if (to is null)
{
to = DateTime.MaxValue.ToLocalTime();
}
return to;
}
//Dateのフォーマット用関数
string DateTimeToString(DateTime? dateTime)
{
if (dateTime == null || dateTime == DateTime.MinValue || dateTime == DateTime.MinValue.ToLocalTime())
{
return string.Empty;
}
return dateTime?.ToString("yyyy-MM-dd") ?? string.Empty;
}
//文字列のフィルタリング処理
string temperatureCFilter = string.Empty;
string temperatureFFilter = string.Empty;
//フィルタリング条件文字列取得関数
void HandleFilter(ChangeEventArgs args, out string target)
{
if (args.Value is string value)
{
target = value;
}
else
{
target = string.Empty;
}
}
//フィルタリング条件文字列クリア関数
void HandleClear(out string target)
{
target = string.Empty;
}
//Summaryフィルタリング用コンボボックスの項目の定義
string[] SummarieItems = new string[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
IEnumerable<string>? SelectedSummary;
//Summaryフィルタリング用関数
bool SummaryFilter(string summary)
{
return SelectedSummary?.Where(x => x.Equals(summary, StringComparison.OrdinalIgnoreCase)).Any() ?? true;
}
//カスタムソート
//文字列を数値としてソートする
GridSort<WeatherForecast> temperatureFSort = GridSort<WeatherForecast>.ByAscending(x => Convert.ToInt32(x.TemperatureF));
//int(Enumの定義順)でソートする
GridSort<WeatherForecast> summarySort = GridSort<WeatherForecast>.ByAscending(x => x.Summary);
}
FluentDataGrid
とPropertyColumn
で簡単にデータグリッドの作成が可能です。
本記事では、ColumnOptions
でデータグリッドを拡張していきます。
※本記事のソート機能紹介用にTemperatureF(℉)
の型定義をstring
に変更しています。
4. 解説
4.1 フィルタリング
日付
Dataは指定した日付の範囲に当てはまる要素を表示するようフィルタリングします。
空欄で指定することで、全要素表示、2025/2/25~
の要素すべて、~2025/2/25
の要素すべてといった指定も可能です。
<PropertyColumn Title="Date" Sortable="true" Property="@(c => DateTimeToString(c.Date.ToLocalTime()))" Align="Align.Start" Class="multiline-text">
<ColumnOptions>
<div class="search-box">
<FluentStack Orientation="Orientation.Vertical">
<FluentLabel Style="margin-top:5px;">
Date
</FluentLabel>
<FluentDatePicker @bind-Value="FilterDateFrom" />
<FluentLabel Style="margin-top:5px;">
~
</FluentLabel>
<FluentDatePicker @bind-Value="@FilterDateTo" />
</FluentStack>
</div>
</ColumnOptions>
</PropertyColumn>
List<WeatherForecast>? FilteredItems =>
forecasts?.Where(x => x.TemperatureC.ToString().Contains(temperatureCFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => x.TemperatureF.ToString().Contains(temperatureFFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => SummaryFilter(GetSummariesStr((int)x.Summary)))
.Where(x => DateFromFilter(FilterDateFrom) <= x.Date && x.Date < DateToFilter(FilterDateTo)).ToList();
DateTime? FilterDateTo;
DateTime? FilterDateFrom;
DateTime? DateFromFilter(DateTime? from)
{
if (from is null)
{
from = DateTime.MinValue.ToLocalTime();
}
return from;
}
DateTime? DateToFilter(DateTime? to)
{
if (to is null)
{
to = DateTime.MaxValue.ToLocalTime();
}
return to;
}
- bind-ValueでFluentDatePickerで指定された時刻を取得
- 空欄指定されたかを判定し、空欄であれば
DateTime.MaxValue
またはDateTime.MinValue
に変更 - 指定された日時の範囲でフィルタリングする
文字列
TemperatureC(℃)
、TemperatureF(℉)
は指定された文字列の部分一致でフィルタリングを行っています。
FluentSearch
に入力された値に部分一致する要素のみを表示します。
<FluentSearch type="search" Autofocus=true @bind-Value="temperatureCFilter" @oninput=@(x => HandleFilter(x, out temperatureCFilter)) @bind-Value:after=@(() => HandleClear(out temperatureCFilter)) Maxlength="255" />
List<WeatherForecast>? FilteredItems =>
forecasts?.Where(x => x.TemperatureC.ToString().Contains(temperatureCFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => x.TemperatureF.ToString().Contains(temperatureFFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => SummaryFilter(GetSummariesStr((int)x.Summary)))
.Where(x => DateFromFilter(FilterDateFrom) <= x.Date && x.Date < DateToFilter(FilterDateTo)).ToList();
void HandleFilter(ChangeEventArgs args, out string target)
{
if (args.Value is string value)
{
target = value;
}
else
{
target = string.Empty;
}
}
void HandleClear(out string target)
{
target = string.Empty;
}
-
FluentSearch
での検索を検知しoninputイベントが発生した場合は、HandleFilter
でフィルタリング条件を取得し適用 -
FluentSearch
で指定してたフィルタリング条件の削除を検知した場合は、HandleClear
でフィルタリング条件を削除
コンボボックス
Summaryはコンボボックスで選択した項目に一致する要素のみを表示するようフィルタリングします。
<PropertyColumn Title="Summary" Property="@(c => GetSummariesStr(c.Summary))" SortBy="@summarySort" Align="Align.Start" Class="multiline-text">
<ColumnOptions>
<div class="search-box" style="height:325px;">
<FluentSelect TOption="string"
Items="@SummarieItems"
Multiple="true"
OptionValue="@(p => p)"
OptionText="@(p => p)"
OptionSelected="@(p => true)"
@bind-SelectedOptions="@SelectedSummary"
Style="position:relative; overflow: visible;" />
</div>
</ColumnOptions>
</PropertyColumn>
List<WeatherForecast>? FilteredItems =>
forecasts?.Where(x => x.TemperatureC.ToString().Contains(temperatureCFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => x.TemperatureF.ToString().Contains(temperatureFFilter, StringComparison.OrdinalIgnoreCase))
.Where(x => SummaryFilter(GetSummariesStr((int)x.Summary)))
.Where(x => DateFromFilter(FilterDateFrom) <= x.Date && x.Date < DateToFilter(FilterDateTo)).ToList();
string[] SummarieItems = new string[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
IEnumerable<string>? SelectedSummary;
bool SummaryFilter(string summary)
{
return SelectedSummary?.Where(x => x.Equals(summary, StringComparison.OrdinalIgnoreCase)).Any() ?? true;
}
-
bind-SelectedOptions
でコンボボックス選択中のSummary
のリストを取得 - リストに含まれている
Summary
の表示文字列に一致かで判定しフィルタリングを行う
4.2 ソート
Sortable - 規定のソート機能
規定のソート機能です。
Sortable="true"
と実装することで表示対象の型の仕様に合わせた昇順・降順のソート機能が実現できます。
<PropertyColumn Title="Date" Sortable="true" Property="@(c => DateTimeToString(c.Date.ToLocalTime()))" Align="Align.Start" Class="multiline-text">
SortBy - カスタムソート
SortBy
を用いてソート処理を記述することで、カスタマイズされたソート処理を実現できます。
-
TemperatureF(℉)
は文字列を数値としてソート -
Summary
は表示用に文字列に加工する前のintでソート
<PropertyColumn Title="℉" Property="@(c => c.TemperatureF)" SortBy="@temperatureFSort" Align="Align.Start" Filtered="!string.IsNullOrWhiteSpace(temperatureFFilter)" Class="multiline-text">
GridSort<WeatherForecast> temperatureFSort = GridSort<WeatherForecast>.ByAscending(x => Convert.ToInt32(x.TemperatureF));
5. おわりに
Fluent UI Blazorを用いることで、見やすく高機能なデータグリッドを簡単に作成することができました。
今後もFluentUIを用いてよりよいUIの実装を行っていきたいと思います。
6. 参考