Fluent UI Blazorのデータグリッドでデータの追加・削除・編集、まとめて操作を行う

# 1. はじめに

`Fluent UI Blazor`のデータグリッドでデータの追加・削除・編集、まとめて操作を行う方法をご紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/27d991ed-ac47-41fe-8bea-51e4ac86c097.png)

# 2. 使用コンポーネント

本記事では`Microsoft.FluentUI.AspNetCore.Components`を使用します。

https://learn.microsoft.com/en-us/fluent-ui/web-components/integrations/blazor

上記の記事の導入方法で指定するコンポーネント名を`Microsoft.FluentUI.AspNetCore.Components`に変更し実施いただくことで利用できます。

# 3. ソースコード

本ソースコードは、プロジェクトテンプレート`Blazor Web App`に含まれる`ServerProject\Components\Pages\Weather.razor`の実装を元にしています。

```html
@page "/weather"
@rendermode CustomRenderingMode.InteractiveServerWithoutPrerendering
 @using Microsoft.FluentUI.AspNetCore.Components

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<FluentStack Orientation="Orientation.Horizontal" Width="1600px">
    <FluentNumberField Min="-20" Max="50" @bind-Value="@(temperatureCNum)" Style="width:100px" />℃

    <FluentButton Appearance="Appearance.Accent" OnClick="Create">
        追加
    </FluentButton>

    <FluentButton Appearance="Appearance.Accent" OnClick="@(x => Update(forecasts!.Where(x => x.Selected).ToList()))" Disabled="@(forecasts!.Where(x => x.Selected).ToList().Count == 0)">
        更新
    </FluentButton>

    <FluentButton Appearance="Appearance.Accent" OnClick="@(x => Delete(forecasts!.Where(x => x.Selected).ToList()))" Disabled="@(forecasts!.Where(x => x.Selected).ToList().Count == 0)">
        削除
    </FluentButton>
</FluentStack>
<FluentStack Orientation="Orientation.Horizontal" Width="1200px">
    <div style="overflow-x: auto;">
        <FluentDataGrid Style="height: 700px;width:1200px;overflow:auto;" TGridItem="WeatherRecord" Items="@forecasts?.OrderBy(x => x.weather.Date).AsQueryable()" GenerateHeader="GenerateHeaderOption.Sticky">
            <ChildContent>
                <SelectColumn TGridItem="WeatherRecord"
							SelectMode="DataGridSelectMode.Multiple"
							SelectFromEntireRow="true"
							Property="@(e => e.Selected)"
							OnSelect="@(e => e.Item.Selected = e.Selected)"
							SelectAll="@(forecasts?.All(p => p.Selected))"
							SelectAllChanged="@(all => forecasts?.ToList().ForEach(p => p.Selected = (all == true)))" />
                <PropertyColumn Title="Date" Property="@(c => DateTimeToString(c.weather.Date.ToLocalTime()))" Align="Align.Start"/>
                <PropertyColumn Title="℃" Property="@(c => c.weather.TemperatureC)" Align="Align.Start" />
                <PropertyColumn Title="℉" Property="@(c => c.weather.TemperatureF)" Align="Align.Start"/>
                <PropertyColumn Title="Summary" Property="@(c => GetSummariesStr(c.weather.Summary))" Align="Align.Start" />

            </ChildContent>
            <EmptyContent>
                <FluentLabel>&nbsp; 該当データがありません。</FluentLabel>
            </EmptyContent>
            <LoadingContent>
                <FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
                    <FluentProgressRing Width="120px" />
                </FluentStack>
            </LoadingContent>
        </FluentDataGrid>
    </div>
</FluentStack>
```

```c#
@code {
    public class WeatherForecast
    {
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public int TemperatureF => (32 + (int)(TemperatureC / 0.5556));
        public int Summary => GetSummaries(TemperatureC);
    }

    public record WeatherRecord
    {
        public WeatherForecast? weather { get; set; }
        public bool Selected { get; set; } = false;
    }

    public enum summariesEnum : uint
    {
        Freezing = 1,
        Bracing,
        Chilly,
        Cool,
        Mild,
        Warm,
        Balmy,
        Hot,
        Sweltering,
        Scorching,
        Unknown = 100,
    }

    List<WeatherRecord>? forecasts = new List<WeatherRecord>();
    int temperatureCNum { get; set; } = 0;
    //表示用データを生成する
    protected override async Task OnInitializedAsync()
    {
        if (!forecasts.Any())
        {
            for (int i = 0; i < 10; i++)
            {
                WeatherForecast tempWeather = new WeatherForecast()
                    {
                        Date = DateTime.UtcNow.AddDays(i),
                        TemperatureC = Random.Shared.Next(-20, 55),
                    };
                WeatherRecord temp = new WeatherRecord() { weather = tempWeather, Selected = false };
                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";
        }
    }

    //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;
    }

    //追加処理
    async Task Create()
    {
        WeatherForecast tempWeather = new WeatherForecast()
            {
                Date = DateTime.UtcNow,
                TemperatureC = (int)temperatureCNum,
            };
        WeatherRecord temp = new WeatherRecord() { weather = tempWeather, Selected = false };
        forecasts.Add(temp);
    }

    //編集処理
    async Task Update(List<WeatherRecord> weathers)
    {
        foreach (var weather in weathers)
        {
            forecasts.Remove(weather);
            WeatherForecast tempWeather = new WeatherForecast()
                {
                    Date = weather.weather.Date,
                    TemperatureC = (int)temperatureCNum,
                };
            WeatherRecord temp = new WeatherRecord() { weather = tempWeather, Selected = false };
            forecasts.Add(temp);
        }
    }

    //削除処理
    async Task Delete(List<WeatherRecord> weathers)
    {
        foreach (var weather in weathers)
        {
            forecasts.Remove(weather);
        }
    }
}

```

# 4. 解説

## 4.1 複数操作

```html
                <SelectColumn TGridItem="WeatherRecord"
							SelectMode="DataGridSelectMode.Multiple"
							SelectFromEntireRow="true"
							Property="@(e => e.Selected)"
							OnSelect="@(e => e.Item.Selected = e.Selected)"
							SelectAll="@(forecasts?.All(p => p.Selected))"
```
`SelectColumn`でチェックボックスの表示・選択状態の取得を行っています。
全選択用チェックボックスはヘッダーに生成されます。

|プロパティ|説明|
|-|-|
|TGridItem|SelectColumnが扱う型を指定する|
|SelectMode|チェックボックスが単一選択か複数選択かを指定する|
|SelectFromEntireRow|チェックボックスの全選択に対応するかを指定する|
|Property|選択状態を指定された値にバインドする|
|OnSelect|チェックされたときの動作を指定する|
|SelectAll|全選択が行われているかをチェックする|

## 4.2 追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/1786bd6f-4c4f-42ab-89f6-a62fe3cd54d2.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/972a0562-62e6-4306-87e4-642b83ce1968.png)


```html
    <FluentButton Appearance="Appearance.Accent" OnClick="Create">
        追加
    </FluentButton>
    
    <FluentNumberField Min="-20" Max="50" @bind-Value="@(temperatureCNum)" Style="width:100px"/>℃
```

```c#
    async Task Create()
    {
        WeatherForecast tempWeather = new WeatherForecast()
            {
                Date = DateTime.UtcNow,
                TemperatureC = (int)temperatureCNum,
            };
        WeatherRecord temp = new WeatherRecord() { weather = tempWeather, Selected = false };
        forecasts.Add(temp);
    }
```

`FluentNumberField`で指定した`TemperatureC(℃)`を実行時の日時で登録します。
`TemperatureF(℉)`・`Summary`は`TemperatureC(℃)`を元に算出します。

## 4.3 更新
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/44ff6cac-dba8-403b-a42f-1009e723f6f7.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/af64f93c-0a81-419f-9fc0-4c4e7c24e47e.png)

```html
    <FluentButton Appearance="Appearance.Accent" OnClick="@(x => Update(forecasts!.Where(x => x.Selected).ToList()))" Disabled="@(forecasts!.Where(x => x.Selected).ToList().Count == 0)">
        更新
    </FluentButton>

    <FluentNumberField Min="-20" Max="50" @bind-Value="@(temperatureCNum)" Style="width:100px"/>℃
```

```c#
    async Task Update(List<WeatherRecord> weathers)
    {
        foreach (var weather in weathers)
        {
            forecasts.Remove(weather);
            WeatherForecast tempWeather = new WeatherForecast()
                {
                    Date = weather.weather.Date,
                    TemperatureC = (int)temperatureCNum,
                };
            WeatherRecord temp = new WeatherRecord() { weather = tempWeather, Selected = false };
            forecasts.Add(temp);
        }
    }
```

チェックボックスで選択した項目を`FluentNumberField`で指定した`TemperatureC(℃)`を元に更新します。
チェックボックスでなにも選択していない場合は、`Disabled`によって更新ボタンが無効になります。

## 4.4 削除

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/4e234257-ce75-49ea-83c6-834af3a160db.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2840516/2997cb07-ff11-4b42-abfd-3d330f649c5e.png)

```html
    <FluentButton Appearance="Appearance.Accent" OnClick="@(x => Delete(forecasts!.Where(x => x.Selected).ToList()))" Disabled="@(forecasts!.Where(x => x.Selected).ToList().Count == 0)">
```

```c#
    async Task Delete(List<WeatherRecord> weathers)
    {
        foreach (var weather in weathers)
        {
            forecasts.Remove(weather);
        }
    }
```

チェックボックスで選択した項目を削除します。
チェックボックスでなにも選択していない場合は、`Disabled`によって削除ボタンが無効になります。

# 5. おわりに

`Fluent UI Blazor`を使用することで、様々なデータの操作を行うことができるデータグリッドを簡単に実装することができました。
`Fluent UI Blazor`に関する以下の記事も見ていただけると嬉しいです！

https://qiita.com/spc_rtakei/items/62526d2ffe44d3b5165a

# 6. 参考

https://calm-sea-053fa6a03-archivesv3.westeurope.3.azurestaticapps.net/

https://demos.blazorbootstrap.com/