【Razor Pages】@functionsを使ってCSSクラスセレクタの付け替えを動的に行う

# この記事でやりたいこと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2257421/9d339c5e-ed3c-21db-337f-05ecb50accb5.png)
画面に表示された項目のうち、クリックされたものを赤文字にしたい場面を想定します。
方針として、「selected」という名前のクラスセレクタを動的に付与することで対応します。

## 背景
現在Razor Pagesの学習用に簡単なTodoアプリを作っています。
その中で、カテゴリーによるタスクの絞り込み機能を実装しようと思い、であれば選択中カテゴリーのレイアウトを変えようと思ったのがきっかけです。（Qiitaのストックした記事のカテゴリーのようなイメージ。というかほぼそれ）

なので、同じイメージのものを実現したい方は、ぜひ参考にしていただければと思います！

# 実装方法
## ①項目の表示
Sports型のリストを用意し、Razor構文である<span>@</span>foreachを使ってリンクを生成しています。
```html:Index.cshtml
<div id="sports-list">
    @foreach (var item in Model.SportsList)
    {
        <a>@item.Name</a>
    }
</div>
```
```c#:Index.cshtml.cs
public class IndexModel : PageModel
{
    public IList<Sports> SportsList { get; set; } = default!;

    public void OnGet()
    {
        SportsList = new List<Sports>
        {
            new Sports(1, "サッカー"),
            new Sports(2, "野球"),
            new Sports(3, "テニス")
        };
    }
}

public class Sports
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;

    public Sports(int id, string name)
    {
        Id = id;
        Name = name;
    }
}
```
```css:site.css
#sports-list > a {
  display: flex;
  color: black;
  text-decoration: none;
}
```
## ②クリックされた項目のIDを取得・保持
クリックされた項目のIDをページハンドラーで受け取り、セッションに格納します。
```diff_html:Index.cshtml
<div id="sports-list">
    @foreach (var item in Model.SportsList)
    {
-       <a>@item.Name</a>
+       <a asp-page-handler="SelectSports" asp-route-id="@item.Id">@item.Name</a>
    }
</div>
```
```diff_c_sharp:Index.cshtml.cs
public class IndexModel : PageModel
{
+   public IActionResult OnGetSelectSports(int id)
+   {
+       HttpContext.Session.SetInt32("sportsId", id);
+       return RedirectToPage("./Index");
+   }
}
```
```diff_c_sharp:Program.cs
  // 省略
+ builder.Services.AddDistributedMemoryCache();
+ builder.Services.AddSession();

  // 省略
+ app.UseSession();
```

## ③保持したIDから項目を判定し、クラスセレクタを付与
ページモデルのプロパティを用意して、セッション経由でIDを受け取り、ビューの<span>@</span>functions内で定義したメソッドから参照します。

自身のIDと合致した項目のみ、クラスセレクタを付与する仕組みです！
```diff_html:Index.cshtml
+ @functions {
+     private string GetSelectedSportsStyle(int id)
+     {
+         return Model.SelectedSports == id ? "selected" : string.Empty;
+     }
+ }

<div id="sports-list">
    @foreach (var item in Model.SportsList)
    {
-       <a asp-page-handler="SelectSports" asp-route-id="@item.Id">@item.Name</a>
+       <a class="@GetSelectedSportsStyle(item.Id)" asp-page-handler="SelectSports" asp-route-id="@item.Id">@item.Name</a>
    }
</div>
```
```diff_c_sharp:Index.cshtml.cs
public class IndexModel : PageModel
{
+   public int SelectedSports { get; set; } = default!;

    public void OnGet()
    {
        // 省略
+       SelectedSports = HttpContext.Session.GetInt32("sportsId") ?? 0;
    }

    public IActionResult OnGetSelectSports(int id)
    {
        HttpContext.Session.SetInt32("sportsId", id);
        return RedirectToPage("./Index");
    }
}
```
```diff_css:site.css
+ #sports-list > a.selected {
+   color: red;
+ }
```

このように<span>@</span>functionsを使うと、**サーバーサイドで処理を行った後に、HTMLコードを構築するタイミングで任意の処理を行ってくれます。**
よって、今回のクラスセレクタの付け替えのように、画面側で動的に処理を行いたい時にぴったりでした！

## [補足]完成したソースコード
最終的なビューとページモデルは以下の通りです。
```html:Index.cshtml
@page
@model IndexModel
@functions {
    private string GetSelectedSportsStyle(int id)
    {
        return Model.SelectedSports == id ? "selected" : string.Empty;
    }
}

<div id="sports-list">
    @foreach (var item in Model.SportsList)
    {
        <a class="@GetSelectedSportsStyle(item.Id)" asp-page-handler="SelectSports" asp-route-id="@item.Id">@item.Name</a>
    }
</div>

```
```c#:Index.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Pages
{
    public class IndexModel : PageModel
    {
        public IList<Sports> SportsList { get; set; } = default!;

        public int SelectedSports { get; set; } = default!;

        public void OnGet()
        {
            SportsList = new List<Sports>
            {
                new Sports(1, "サッカー"),
                new Sports(2, "野球"),
                new Sports(3, "テニス")
            };

            SelectedSports = HttpContext.Session.GetInt32("sportsId") ?? 0;
        }

        public IActionResult OnGetSelectSports(int id)
        {
            HttpContext.Session.SetInt32("sportsId", id);
            return RedirectToPage("./Index");
        }
    }

    public class Sports
    {
        public int Id { get; set; }
        public string Name { get; set; } = string.Empty;

        public Sports(int id, string name)
        {
            Id = id;
            Name = name;
        }
    }
}
```

## [補足]なぜJavaScriptでやらないのか
元々は、onclickイベントを使って、JavaScriptのメソッドでクラスセレクタの付け替えを考えていました。
が、「カテゴリーを絞り込んでタスクを一覧表示」を実現するために、表示中のページにリダイレクトする必要があり（それが微妙？）、しかしこれを行うとそのタイミングでクラスセレクタがリセットされてしまう…

といった課題があったため、JavaScriptを使わず、本記事に記載した実装方法で対応しました。

# 動作環境
* Windows 11 Home（23H2）
* .NET 8.0（SDK 8.0.204）
* Visual Studio 2022

# 参考
* <span>@</span>functions

https://learn.microsoft.com/ja-jp/aspnet/core/mvc/views/razor?view=aspnetcore-8.0#functions

* ページハンドラー

https://learn.microsoft.com/ja-jp/aspnet/core/mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-8.0#asp-page-handler

* セッション

https://learn.microsoft.com/ja-jp/aspnet/core/fundamentals/app-state?view=aspnetcore-8.0