以下は、前回のサンプルを改良した ASP.NET Core MVC 実装例です。
主な変更点は以下の通りです。
- CSRF 対策 を
@Html.AntiForgeryToken() と RequestVerificationToken ヘッダーで確実に実装 - 新規カテゴリ追加時の UI 改善(追加後に即選択状態にする)
- エラーハンドリング強化(サーバー側・クライアント側両方)
- SelectList の更新位置を「+ 項目を追加」の直前」に固定
1. ViewModel
public class IncomeViewModel
{
public int IncomeCategoryId { get; set; }
public IEnumerable<SelectListItem> IncomeCategories { get; set; }
}
2. Controller
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;
public class IncomeController : Controller
{
private readonly AppDbContext _context;
public IncomeController(AppDbContext context)
{
_context = context;
}
// GET: Create
public IActionResult Create()
{
var vm = new IncomeViewModel
{
IncomeCategories = _context.IncomeCategories
.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Name })
.ToList()
};
return View(vm);
}
// POST: Create
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Create(IncomeViewModel model)
{
if (model.IncomeCategoryId <= 0)
{
ModelState.AddModelError("", "カテゴリを選択してください");
}
if (!ModelState.IsValid)
{
model.IncomeCategories = _context.IncomeCategories
.Select(c => new SelectListItem { Value = c.Id.ToString(), Text = c.Name })
.ToList();
return View(model);
}
// 保存処理例
// var income = new Income { CategoryId = model.IncomeCategoryId, ... };
// _context.Incomes.Add(income);
// _context.SaveChanges();
return RedirectToAction("Index");
}
// Ajax: 新しいカテゴリ追加
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult AddCategory([FromBody] CategoryRequest request)
{
if (string.IsNullOrWhiteSpace(request?.Name))
return BadRequest(new { message = "カテゴリ名は必須です" });
var category = new IncomeCategory { Name = request.Name.Trim() };
_context.IncomeCategories.Add(category);
_context.SaveChanges();
return Json(new { id = category.Id, name = category.Name });
}
public class CategoryRequest
{
public string Name { get; set; }
}
}
3. Create.cshtml
@model IncomeViewModel
<form asp-action="Create" method="post">
@Html.AntiForgeryToken()
<div class="form-group">
<label asp-for="IncomeCategoryId"></label>
<select asp-for="IncomeCategoryId" class="form-control" id="IncomeCategoryId"
asp-items="Model.IncomeCategories">
<option value="">-- 選択してください --</option>
<option value="add">+ 項目を追加</option>
</select>
</div>
<button type="submit" class="btn btn-primary">保存</button>
</form>
<!-- モーダル -->
<div class="modal fade" id="addCategoryModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">新しいカテゴリを追加</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="text" id="newCategoryName" class="form-control" placeholder="カテゴリ名">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">キャンセル</button>
<button type="button" id="saveCategoryBtn" class="btn btn-primary">追加</button>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
const categorySelect = document.getElementById("IncomeCategoryId");
const modalElement = document.getElementById('addCategoryModal');
const modalInstance = new bootstrap.Modal(modalElement);
categorySelect.addEventListener("change", function () {
if (this.value === "add") {
this.value = "";
modalInstance.show();
}
});
document.getElementById("saveCategoryBtn").addEventListener("click", function () {
const name = document.getElementById("newCategoryName").value.trim();
if (!name) {
alert("カテゴリ名を入力してください");
return;
}
fetch('@Url.Action("AddCategory", "Income")', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': document.querySelector('input[name="__RequestVerificationToken"]').value
},
body: JSON.stringify({ name: name })
})
.then(res => {
if (!res.ok) return res.json().then(err => { throw new Error(err.message || "追加に失敗しました"); });
return res.json();
})
.then(data => {
// 「+ 項目を追加」の直前に挿入
const addOptionIndex = categorySelect.options.length - 1;
const option = new Option(data.name, data.id, true, true);
categorySelect.add(option, addOptionIndex);
modalInstance.hide();
document.getElementById("newCategoryName").value = "";
})
.catch(err => alert(err.message));
});
</script>
}
改良ポイント
CSRF 対策
- Razor 側で
@Html.AntiForgeryToken() を追加 - Fetch API で
RequestVerificationToken ヘッダーを送信
UI 改善
- 新規追加後に即選択状態にする
- 「+ 項目を追加」の直前に新しい
<option> を挿入
エラーハンドリング強化
- サーバー側で
BadRequest を返す場合に JSON メッセージを返却 - クライアント側で
.catch() でエラー表示
この形にすると、ページ遷移なしでカテゴリ追加ができ、
セキュリティとユーザビリティの両方を確保できます。
もし希望があれば、このコードを 完全に非同期で保存まで行うバージョン に拡張して、
カテゴリ追加後にそのまま新規レコードも保存できるようにできます。
それも作りますか?
Powered by Froala Editor