1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Fluent UI Blazorを用いてExcelのようなフィルタリング機能・ソート機能を持つデータグリッドを作成する

Last updated at Posted at 2025-02-18

1. はじめに

Fluent UI Blazorを用いてExcelのようなフィルタリング機能・ソート機能を持つデータグリッドの実装を行う方法をご紹介します。

image.png

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>&nbsp; 該当データがありません。</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);
}

FluentDataGridPropertyColumnで簡単にデータグリッドの作成が可能です。
本記事では、ColumnOptionsでデータグリッドを拡張していきます。

※本記事のソート機能紹介用にTemperatureF(℉)の型定義をstringに変更しています。

4. 解説

4.1 フィルタリング

日付

image.png

image.png

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に変更
  • 指定された日時の範囲でフィルタリングする

文字列

image.png

image.png

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でフィルタリング条件を削除

コンボボックス

image.png

image.png

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 - 規定のソート機能

  • 昇順
    image.png
  • 降順
    image.png

規定のソート機能です。
Sortable="true"と実装することで表示対象の型の仕様に合わせた昇順・降順のソート機能が実現できます。

<PropertyColumn Title="Date" Sortable="true" Property="@(c => DateTimeToString(c.Date.ToLocalTime()))" Align="Align.Start" Class="multiline-text">

SortBy - カスタムソート

image.png

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. 参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?