WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ドロップダウンリストを使って絞込み (CORE)

by WebSurfer 2. August 2023 15:29

先の記事「DropDownList を使って絞込み」の .NET 7.0 ASP.NET Core MVC 版を作ってみました。

ドロップダウンリストを使って絞込み

上の画像がそれで、Microsoft の SQL Server サンプルデータベース Northwind の Orders テーブルから、ドロップダウンリストで選択された Customer (注文主) および Employee (注文を扱った従業員) を WHERE 句の選択条件にしてレコードを抽出し、抽出されたレコードを一覧表示するものです。

SQL Server の Orders テーブルは下の画像の構造となっており、複数の顧客の過去の注文データが 830 レコード含まれています。CustomerID, EmployeeID, ShipVia フィールドは、それぞれ Customers, Employees, Shippers テーブル (顧客、従業員、商品の輸送者の詳細) に FK 制約が張られています。

Northwind Orders テーブル

この Orders テーブルから、この記事の一番上の画像のように、ドロップダウンリストの選択結果に応じてレコードを抽出し、そのレコード一覧を表示する ASP.NET Core MVC アプリを作ります。作り方の概要を以下に書きます。

(1) プロジェクトの作成

Visual Studio 2022 のテンプレートの中から「ASP.NET Core Web アプリ (Model-View-Controller)」を選び ASP.NET Core MVC アプリのプロジェクトを作成します。

テンプレート

この記事の例ではターゲットフレームワークを .NET 7.0 にしています。その他のバージョンではこの記事とは多少違いがあるかもしれません。

(2) コンテキストクラスとエンティティクラスの作成

Entity Framework を利用するので、リバースエンジニアリングという手法を使って、SQL Server データベース Northwind からコンテキストクラスとエンティティクラスを生成します。

作成方法は Microsoft のドキュメント「スキャフォールディング (リバース エンジニアリング)」を見てください。各パラメータについては、こちらの記事 Scaffold-DbContext がまとまっていて分かりやすいと思います。

参考に、EF Core パッケージマネージャーコンソール (PMC) ツールを使った場合の Scaffold-DbContext コマンドの例を下に載せておきます。

Scaffold-DbContext -Connection "Data Source=(localdb)\MSSQLLocalDb;Initial Catalog=Northwind;Integrated Security=True" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Products, Categories, Suppliers, Customers, Orders, Employees, Shippers, "Order Details" -DataAnnotations

生成されたコンテキストクラスとエンティティクラスのダイアグラムを EF Core Power Tools を使って表示すると以下のようになります。

DbContext ダイアグラム

上の画像の内、この記事で関係するのは Order, Customer, Employee, Shipper です。

(3) Controller と View の作成

Program.cs で DI コンテナに NorthwindContext を追加します。コードは以下の通りです。

builder.Services.AddDbContext<NorthwindContext>(options =>
    options.UseSqlServer(builder.Configuration
           .GetConnectionString("NorthwindConnection")));

appsettings.json に接続文字列を追加します。名前は上のコードの "NorthwindConnection" とし、接続文字列本体はリバースエンジニアリングの Scaffold-DbContext コマンドで使ったものと同じにします。ただし、文字列中の \ は \\ にエスケープする必要があるので注意してください。

その後で、Visual Studio のスキャフォールディング機能を使って、Orders テーブルの CRUD (Create / Read / Upadate / Delete) を行う Controller と View 一式を生成します。

スキャフォールディング

アプリを実行してみて CRUD 機能が期待通り動くことを確認します。

(4) アクションメソッドの追加

ドロップダウンリストによる絞り込み機能を持つアクションメソッドを、上のステップ (3) で作った Controller に追加します。下のコードの Search メソッドと SearchResult メソッドが追加したアクションメソッドです。

基本的には、先の記事「jQuery ajax で部分ビューの呼出・表示 (CORE)」で書いたのと同様に、ajax によって部分ビューを呼び出して抽出結果のレコード一覧を表示します。

まず、Search メソッドに対応する Search.cshtml (コードは下のステップ(5)参照) にあるボタンクリックで、jQuery ajax を使ってアクションメソッド SearchResult を呼び出します。

アクションメソッド SearchResult が呼び出される時、ドロップダウンリストの選択結果が引数 customerId, employeeId に渡されます。SearchResult は渡された引数の内容に応じてレコードを抽出し、結果を Model として部分ビュー SearchResult.cshtml に渡します。

部分ビューSearchResult.cshtml がレンダリングした html ソース (抽出結果のテーブル) が SearchResult メソッドの応答として返されますので、それをドロップダウンリストの下に表示するようにしています。その結果がこの記事の一番上の画像です。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;

namespace MvcNet7App.Controllers
{
    public class OrdersController : Controller
    {
        private readonly NorthwindContext _context;

        public OrdersController(NorthwindContext context)
        {
            _context = context;
        }

        // ・・・中略・・・

        // Customer と Employee の DropDownList で絞り込みが行えるページを追加
        public IActionResult Search()
        {
            ViewData["CustomerId"] = 
                new SelectList(_context.Customers, "CustomerId", "CompanyName");

            // Employees テーブルは FirstName と LastName でフィールドが別れて
            // いるので、以下のようにして FirstName と LastName を結合する
            var employeeList = _context.Employees
                               .Select(e => new 
                               {
                                   EmployeeId = e.EmployeeId.ToString(),
                                   EmployeeName = e.FirstName + " " + e.LastName
                               });

            ViewData["EmployeeId"] = 
                new SelectList(employeeList, "EmployeeId", "EmployeeName");
            return View();
        }

        // 部分ビュー用のアクションメソッド。DropDownList で ALL を選ぶと
        // 引数の customerId, employeeId には null が渡される
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> SearchResult(string customerId, 
                                   int? employeeId)
        {
            
            // Include が返すのは IIncludableQueryable<TEntity,TProperty>
            // 型なので、var order = ... とすると下の Where メソッドの所
            // で IQueryable<Order> を IIncludableQueryable<Order,Shipper>
            // に暗黙的に変換できませんというエラーになるので注意
            IQueryable<Order> orders = _context.Orders
                                       .Include(o => o.Customer)
                                       .Include(o => o.Employee)
                                       .Include(o => o.ShipViaNavigation);
            
            // null は ALL (全顧客) なのでスキップ
            if (customerId != null)
            {
                orders = orders.Where(o => o.CustomerId == customerId);
            }

            // null は ALL (全従業員) なのでスキップ
            if (employeeId != null)
            {
                orders = orders.Where(o => o.EmployeeId == employeeId);
            }

            return PartialView(await orders.ToListAsync());
        }

        // ・・・中略・・・
    }
}

(5) Search.cshtml

上のステップ (4) のアクションメソッド Search に対応する View です。jQuery ajax を使ってアクションメソッド SearchResult を呼び出し、応答として返された部分ビューの html ソースをドロップダウンリストの下の div 要素に表示するための JavaScript / jQuery のコードを含んでいます。

@{
    ViewData["Title"] = "Search";
}

<h1>Search by Customer and Employee</h1>

@*asp-action 属性を入れて Tag ヘルパーだと認識させないと
CSRF 用のトークンが発行されないので注意*@
<form asp-action="Search">
    Customer: 
    <select id="customerId" name="customerId" 
        asp-items="ViewBag.CustomerId">
        <option value="">ALL</option>
    </select>
    Employee: 
    <select id="employeeId" name="employeeId"  
        asp-items="ViewBag.EmployeeId">
        <option value="">ALL</option>
    </select>
    <input type="submit" value="Search" />
</form>

<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[

        var form = document.querySelector("form");

        // ボタンクリックで form が submit されるという
        // 動きになるので、form 要素の submit イベントに
        // リスナをアタッチして jQuert ajax で処理する
        $(form).on("submit", function (event) {
            // submit されては困るのでキャンセル
            event.preventDefault();

            // jQuery ajax を使って、部分ビューを応答とし
            // て返すアクションメソッド Orders/SeachResult
            // にフォームデータを POST 送信する
            $.ajax({
                type: "POST",
                url: "/Orders/SearchResult",
                
                // フォームデータを取得、送信 data に設定
                data: $(this).serialize(),

                dataType: "html",

                // 要求後、応答が返ってくるまで Loading... 
                // というメッセージを出す
                beforeSend: () => {
                    $("#result").empty();
                    $("#result").append(
                        '<span>Loading...</span>'
                    );
                }
            }).done(function (data) {
                $("#result").empty();
                $("#result").append(data);
            }).fail(function (jqXHR, status, error) {
                $('#result').text('Status: ' + status + 
                    ', Error: ' + error);
            });
        })

        //]]>
    </script>
}

(6) SearchResult.cshtml

アクションメソッド SearchResult からドロップダウンリストの選択結果に従って作成された Model を渡され、それを使って結果をブラウザに表示する html ソースを生成する部分ビューです。

@model IEnumerable<MvcNet7App.Models.Order>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.OrderDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.RequiredDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShippedDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Freight)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipAddress)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipCity)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipRegion)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipPostalCode)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipCountry)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Customer)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Employee)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ShipViaNavigation)
            </th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.OrderDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.RequiredDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShippedDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Freight)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipAddress)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipCity)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipRegion)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipPostalCode)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ShipCountry)
            </td>
            <td>
                @Html.DisplayFor(modelItem => 
                    item.Customer!.CompanyName)
            </td>
            <td>
                @{
                    string name = (item.Employee != null) ? 
                                      item.Employee.FirstName + " " + 
                                      item.Employee.LastName : "";
                }
                @Html.DisplayFor(modelItem => name)
            </td>
            <td>
                @Html.DisplayFor(modelItem => 
                    item.ShipViaNavigation!.CompanyName)
            </td>
        </tr>
}
    </tbody>
</table>

Tags: , , , ,

CORE

Blazor WASM でファイルアップロード

by WebSurfer 28. October 2022 14:12

Blazor Web Assembly (WASM) アプリで、html の form 要素及び input type="file" 要素と jQuery ajax を使ってファイルをアップロードする方法を書きます。

Blazor WASM でファイルアップロード

(.NET 5.0 以降であれば、InputFile クラスを利用できるそうですので、そちらを使う方がいいかもしれません。詳細は Microsoft のドキュメント「ASP.NET Core Blazor ファイルのアップロード」を見てください)

jQuery / JavaScript を使ってファイルのアップロードはできますので、基本ブラウザである Blazor WASM のクライアント側からでも同様にファイルのアップロードはできます。

その jQuery / JavaScript のコードを Blazor WASM のクライアント側に組み込んで呼び出すことで、Blazor WASM アプリとしてファイルのアップロード機能を持たせることが可能です。(以下に概略を書きますが、詳細は Microsoft のドキュメント「ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す」を見てください)

なお、input type="submit" ボタンを form 要素内に配置して、それをクリックするというのは NG です。ファイルはアップロードされますが、返ってきた応答でブラウザの画面が書き換えられてしまうので。

jQuery ajax を使ってのファイルのアップロード機能をどのように実装するかの具体例を以下に書いておきます。

ベースとする Blazor WASM ソリューションは、Visual Studio 2022 のテンプレートを使って、フレームワーク .NET 6.0 で、[ASP.NET Core でホストされた(h)]にチェックを入れて作成したものを使います。ソリューション内に Client, Server, Shared という 3 つのプロジェクトができるはずです。

(1) ファイルアップロード用の Razor ページ

Client プロジェクトの Pages フォルダに以下の Razor ページを追加します。

@page "/fileupload"
@inject IJSRuntime JSRuntime;

<h3>File Upload by jQuery Ajax</h3>

<form id="form1" method="post" enctype="multipart/form-data">
    <input type="file" name="postedfile" />
</form>

<button type="button" class="btn btn-primary" @onclick="UploadFile">
    アップロード
</button>

<div id="result"></div>

@code {
    private async Task UploadFile()
    {
        await JSRuntime.InvokeVoidAsync("uploadFile");
    }
}

Shared フォルダの NavMenu.razor に上の fileupload ページへのリンクを追加し、メニューからリンクをクリックしてアクセスできるようにします。

(2) ファイルアップロード用の js ファイル

ファイルをアップロードする jQuery / JavaScript コードのコードを含んだ js ファイルを作成します。コードは以下の通りです。js ファイルの名前は任意ですがここでは exampleJsInterop.js としておきます。

window.uploadFile = () => {
    // FormData オブジェクトの利用
    var fd = new FormData(document.getElementById("form1"));

    $.ajax({
        url: '/Upload',
        method: 'post',
        data: fd,
        processData: false, // jQuery にデータを処理させない
        contentType: false  // contentType を設定させない
    }).done(function (response) {
        $("#result").empty;
        $("#result").text(response);
    }).fail(function (jqXHR, textStatus, errorThrown) {
        $("#result").empty;
        $("#result").text('textStatus: ' + textStatus +
            ', errorThrown: ' + errorThrown);
    });
}

(3) js ファイルの配置

上の exampleJsInterop.js と jQuery.js を、Client プロジェクトの wwwroot フォルダ下に js フォルダを作って、その中に配置します。

js ファイルの配置

(4) index.html に script タグを追加

Client プロジェクトの wwwroot フォルダ下の index.html に、exampleJsInterop.js と jQuery.js がダウンロードされるよう、script タグを追加します。

Index.html に script タグを追加

(5) Server プロジェクトに Controller 追加

Server プロジェクトの Controllers フォルダにアップロードされたファイルを受け取って必要な処置をする Controller を追加します。

具体例は以下のコードを見てください。Web API の Controller としています。名前をステップ (2) の jQuery / JavaScript のコードにある url: '/Upload' と合わせる必要があることに注意してください。

using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmCoreHosted.Server.Controllers
{
    [Route("[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        // .NET 6.0 で作ったプロジェクトは「Null 許容」オプションが「有効化」に
        // 設定してあるので、引数も NULL 許容にしておかないと、クライアントでファ
        // イルを選択しないで送信した場合、HTTP 400 Bad Request エラーになって
        //  "The postedFile field is required."というエラーメッセージが返ってくる
        [HttpPost]
        public string Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アップロードされたファイルを処理(省略)

                result = $"{filename} ({postedFile.ContentType}) - " +
                    $"{postedFile.Length} bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return result;
        }
    }
}

アプリを実行してファイルアップロードに成功するとこの記事の一番上にある画像のようになります。

Tags: , , ,

Upload Download

jQuery.ajax を使っての事前検証

by WebSurfer 6. February 2022 15:56

ASP.NET Web Forms アプリで jQuery Ajax を使ってポストバックの際にサーバー側で行われる処理が可能かどうかを事前に調べ、可能と分かったら JavaScript で Button クリックしてポストバックする、可能ではないと分かった場合はポストバックせずメッセージを表示してユーザーに知らせるという話です。

ポストバックして処理完了

元の話は Teratail のスレッド「asp:button 押下前にサーバサイドが応答しているか確認したいためPingのようなことを行いたい」のものです。実際のそのような検証が必要なケースはあまりなさそうですが、せっかく考えた話ですし jQuery Ajax の timeout の使い方が今後の参考になるかもしれないと思ったので備忘録として書いておくことにしました。

Button クリックでポストバックしてサーバー側でユーザーが送信したデータを受けて、データベースサーバーにクエリを投げる等の処理をすることはよくあると思います。

その際、Button クリックでいきなりポストバックするのではなく、その前に jQuery Ajax を使って事前にサーバー側での処理が可能かどうかを調べるというストーリーです。

(いきなりポストバックしても結果をきちんとユーザーに知らせることはできるし、やりすぎるとサーバーに無駄な負荷を増やすし軽くすると検証の意味が薄れるということで、そのような検証の必要性は低いとは思いますがそこは置いときます)

jQuery Ajax で呼び出してサーバー側で事前検証を行う HTTP ジェネリックハンドラを作ります。以下のコードがそのサンプルです。

HTTP ジェネリックハンドラ Handler1.ashx

using System.Web;

namespace WebForms1
{
    public class Handler1 : IHttpHandler
    {

        public void ProcessRequest(HttpContext context)
        {
            // Button クリックでポストバックされた時にサーバー
            // で行われる処理が可能か事前に検証する。処理に最
            // 長 3 秒かかるとして、ここでは単純に 3 待つ
            System.Threading.Thread.Sleep(3000);
            
            // 検証に成功したら "検証成功" という文字列を返す
            var validationResult = "検証成功";

            context.Response.ContentType = "text/plain";
            context.Response.Write(validationResult);
        }

        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}

上の Handler1.ashx を jQuery Ajax を使って呼び出します。Handler1.ashx は検証に成功すると "検証成功" という文字列を返すので、それを受けたら JavaScript で Button をクリックしてポストバックします。その結果がこの記事の一番上にある画像です。

Handler1.ashx を呼び出す JavaScript を含む Web Form ページのサンプルコードは以下の通りです。

WebForm1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs"
    Inherits="WebForms1.WebForm1" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
    <script src="Scripts/jquery-3.4.1.js"></script>
    <script type="text/javascript">
        $(function () {
            $("#btnSec").on("click", function () {
                $.ajax({
                    type: "get",
                    url: "Handler1.ashx",
                    timeout: 5000
                }).done(function (data) {
                    // 呼び出し先 Handler1.ashx で検証が成功すると "検証成功" 
                    // という文字列が返ってきて引数 data に代入される
                    if (data == "検証成功") {
                        var hiddenbutton = document.getElementById("Button1");
                        // Button をクリック
                        hiddenbutton.click();
                    } else {
                        $("#Label1").text("処理できません - 検証結果 NG");
                    }
                }).fail(function (jqXHR, textStatus, errorThrown) {
                    $("#Label1").text("処理できません - " + textStatus);
                });
            });
        });
    </script>
    <style type="text/css">
        .style1
        {
            display: none;
        }
    </style>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <h2>WebForm1 - jQuery.Ajax を使っての事前検証</h2>
            
            <input id="btnSec" type="button" value="反映" />

            <%--ポストバックを行う Button コントロール。上の style1 で
                隠しボタンに設定。クリックは JavaScript で行う。クリ
                ックされるとポストバックが発生し、サーバー側のイベント
                ハンドラ ClickBtnSection で必要な処理が行われる--%>
            <asp:Button ID="Button1" runat="server" 
                OnClick="ClickBtnSection" CssClass="style1" />

            <asp:Label ID="Label1" runat="server">
                Label1 初期値
            </asp:Label>
        </div>
    </form>
</body>
</html>

WebForm1.aspx.cs

using System;

namespace WebForms1
{
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void ClickBtnSection(object sender, EventArgs e)
        {
            // サーバー側での必要な処理

            Label1.Text = "Button1 がクリックされました";
        }
    }
}

Handler1.ashx を呼び出して、そこでの処理中にサーバーエラーが発生した場合は以下のようになります。

サーバーエラー

Handler1.ashx を呼び出した後、jQuery Ajax の timeout オプションに設定した時間(上のコード例では 5 秒)待っても応答が返ってこない場合は以下のようになります。

timeout

Handler1.ashx から帰ってきた文字列が "検証成功" 以外の場合は(実際にそういうケースはないかもしれませんが)以下のようになります。

検証結果 NG

Tags: , , ,

Validation

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  April 2024  >>
MoTuWeThFrSaSu
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345

View posts in large calendar