Webアプリ 検索機能を追加しました

プログラミング

検索機能を追加します。
検索項目は、日付、血糖値、時間帯で、それぞれ From Toで範囲指定できるようにします。

複数条件が指定されたときは、AND条件(全ての指定条件を満たすデータ)で検索します。

次の2つに分けて記述します。

検索条件の入力機能
データの検索機能

検索条件の入力機能

フォームで入力した検索値とページモデルのプロパティのバインドを定義します。

バインドを定義することで、フォームで入力した値は、クエリ文字列を使わなくても、ページモデルのプロパティに値が反映されます。
また、データアノテーションでページモデルのプロパティの型を定義したり、プロパティに値をセットする前の妥当性チェックを定義します。
フォームで入力されたデータはプロパティに値がセットされる前に妥当性のチェックが行われ、正しいデータだけを受取ることができます。

ページモデルクラス(Index.cshtml.cs)に次のように定義しました。

[BindProperty(SupportsGet =true)]の(SupportsGet =true)は、GETメソッドの時に必要になります。
デフォルトでは、POSTメソッドのみ有効なので、GETメソッドのときはSupportsGet =true指定が必要です。

[BindProperty(SupportsGet =true)]
[DataType(DataType.Date)]
[Range(typeof(DateTime), “1956/11/19”, “2100/12/31”)]
public DateTime? DateFromFilter { get; set; }
 
[BindProperty(SupportsGet = true)]
[DataType(DataType.Date)]
[Range(typeof(DateTime), “1956/11/19”, “2100/12/31”)]
public DateTime? DateToFilter { get; set; }
 
[BindProperty(SupportsGet = true)]
[Range(0, 9999)]
public int? BGVFromFilter { get; set; }
 
[BindProperty(SupportsGet = true)]
[Range(0, 9999)]
public int? BGVToFilter { get; set; }
 
[BindProperty(SupportsGet = true)]
[Range(0, 9999)]
public int? TimeZoneFromFilter { get; set; }
 
[BindProperty(SupportsGet = true)]
[Range(0, 9999)]
public int? TimeZoneToFilter { get; set; }

フォーム画面は次のような画面です。

入力フォームのHTMLは次の通りです。

<td><input asp-for=”DateFromFilter” class=”form-control” /> </td>

asp-for=”DateFromFilter”の指定で、ページモデルのDateFromFiltプロパティがバインドされます。

Class=”form-control”の指定で、日付型の入力フォームでカレンダー入力ができるようになります。

<span asp-validation-for=”DateFromFilter” class=”text-danger”></span>
は、入力値の妥当性チェックの結果エラーがあった場合、エラーメッセージを表示します。

<form asp-page=”./Index” method=”get”>
    <table class=”table”>
        <thead>
            <tr>
                <th>日付(From)</th>
                <th>日付(To)</th>
                <th>血糖値(From)</th>
                <th>血糖値(To)</th>
                <th>時間帯(From)</th>
                <th>時間帯(To)</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td><input asp-for=”DateFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”DateToFilter” class=”form-control” /> </td>
                <td><input asp-for=”BGVFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”BGVToFilter” class=”form-control” /> </td>
                <td><input asp-for=”TimeZoneFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”TimeZoneToFilter” class=”form-control” /> </td>
                <td><input type=”submit” value=”Search” class=”btn-primary” /></td>
            </tr>
 
        </tbody>
    </table>
    <span asp-validation-for=”DateFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”DateToFilter” class=”text-danger”></span>
    <span asp-validation-for=”BGVFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”BGVToFilter” class=”text-danger”></span>
    <span asp-validation-for=”TimeZoneFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”TimeZoneToFilter” class=”text-danger”></span>

</form>

データの検索機能

データの検索はLINQ(Language Integrated Query)式で行います。
フォームの入力値にもとづいて Where句を作成します。

LINQ式はIQueryable<>型の変数に作成することで、データベースに対してクエリを実行できます。

IQueryable<>型の変数に作成したクエリ式は、ToListAsync()メソッドで実行します。

Where句で検索

次のWhere文を作成します。

where  (s => s.Date >= DateFrom && s.Date <= DateTo &&
s.BloodGlucoseValue >=BGVFrom && s.BloodGlucoseValue <= BGVTo &&
s.TimeZone >= TimeZoneFrom && s.TimeZone <= TimeZoneTo);

■SQL文にある BETWEEN が LINQ にはありませんので、Where文を使用します。

LINQ式を次のように2回(以上)に分割してvalueIQ変数にセットすることができます。

IQueryable<Value> valueIQ = from s in _context.Value select s;
valueIQ = valueIQ.Where(s => s.Date >= DateFrom && s.Date <= DateTo &&
                          s.BloodGlucoseValue >= BGVFrom && s.BloodGlucoseValue <= BGVTo &&
                          s.TimeZone >= TimeZoneFrom && s.TimeZone <= TimeZoneTo);

■しかし、Whereの部分を分割することはできません。

×
IQueryable<Value> valueIQ = from s in _context.Value select s;
valueIQ = valueIQ.Where(s => s.Date >= DateFrom && s.Date <= DateTo &&;
valueIQ = valueIQ. s.BloodGlucoseValue >= BGVFrom && s.BloodGlucoseValue <= BGVTo &&;
valueIQ = valueIQ. s.TimeZone >= TimeZoneFrom && s.TimeZone <= TimeZoneTo);

■次のようにLINQ式を一旦変数に入れて使うことはできません。

×
String whereString = “Where(s => s.Date >= DateFrom && s.Date <= DateTo &&
                           s.BloodGlucoseValue >= BGVFrom && s.BloodGlucoseValue <= BGVTo &&
                           s.TimeZone >= TimeZoneFrom && s.TimeZone <= TimeZoneTo)”;
valueIQ = valueIQ.whereString;

■条件式に変数、プロパティを使用することはできます。

■IQueryable<Value> valueIQをメソッドに処理を渡すことができません。

IQueryableは、クエリを表す式ツリーを構築するために使用されます。
しかし、IQueryableは遅延実行されるため、メソッドが呼び出されて、メソッド内でvalueIQを変更しても、変更したクエリ式は実行されないようです。

検索条件を変数にセットするロジック

■日付範囲、血糖値範囲、時間帯範囲のいずれかの検索条件が指定されているか判定する条件式

検索項目のいずれかに入力があった場合に、From To の検索値をセットします。

if (DateFromFilter.HasValue ||
   DateToFilter.HasValue ||
  BGVFromFilter <> 0 ||
   BGVToFilter <> 0 ||
   TimeZoneFromFilter <> 0 ||
   TimeZoneToFilter <> 0 )
{
   //Where句の構築処理

■デフォルトの最小値と最大値

検索値の入力において、最小値と最大値を下記のように設定しました。

項目最小値最大値
日付1956/11/192100/12/31
血糖値09999
時間帯023

検索条件の一部だけ入力(例えば日付)した時に、その他の入力しなかった項目にセットされる値です。
例)
日付(From):2023/10/1 、 日付(To): 2023/10/31 と入力し、血糖値と時間帯に何も入力しなかった場合、変数には次のように値がセットされます。

DateFrom=”2023/10/1”  DateTo=”2023/10/31”
BGVFrom=0               GBVTo=9999
TimeZoneFrom=0         TimeZoneTo=23

Where句に使う検索条件変数に値をセットする処理の概要

最小値、最大値のセット事例
DateFromFilter、DateToFilterなど~Filterという名前は、バインドプロパティです。(フォーム入力値)
DateFrom、DateToなど~Filterがついていない変数は、Whereで使用する値です。
下表の左にある青と紫のバーは、
青のバーはFromとToのどちらにも値が入力されているケース、
紫のバーはFromもしくはToのどちらか一方に値が入力されているケース、
色なしはどちらにも入力されていないケース。

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

ページモデルクラス(Index.cshtml.cs)の全コードを以下に示します。

ページモデルクラス Index.cshtml.cs
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using BGC.Data;
using BGC.Models;
using System.ComponentModel.DataAnnotations;
using System.Configuration;
 
namespace BGC.Pages
{
    public class IndexModel : PageModel
    {
        private readonly BGC.Data.BGCContext _context;
 
        public IndexModel(BGC.Data.BGCContext context)
        {
            _context = context;
        }
        /**
         * 並べ替え用プロパティ
         */
        public string BloodGlucoseValueSort { get; set; }
        public string DateSort { get; set; }
 
        /**
         * 検索条件の入力欄とバインドするプロパティ
         */
        [BindProperty(SupportsGet =true)]
        [DataType(DataType.Date)]
        [Range(typeof(DateTime), “1956/11/19”, “2100/12/31”)]
        public DateTime? DateFromFilter { get; set; }
 
        [BindProperty(SupportsGet = true)]
        [DataType(DataType.Date)]
        [Range(typeof(DateTime), “1956/11/19”, “2100/12/31”)]
        public DateTime? DateToFilter { get; set; }
 
        [BindProperty(SupportsGet = true)]
        [Range(0, 9999)]
        public int? BGVFromFilter { get; set; }
 
        [BindProperty(SupportsGet = true)]
        [Range(0, 9999)]
        public int? BGVToFilter { get; set; }
 
        [BindProperty(SupportsGet = true)]
        [Range(0, 9999)]
        public int? TimeZoneFromFilter { get; set; }
 
        [BindProperty(SupportsGet = true)]
        [Range(0, 9999)]
        public int? TimeZoneToFilter { get; set; }
 
        /**
         * WHERE句を作成するときに使用する最小値と最大値を保持する変数
         * デフォルト値をセット
         */
        DateTime DateFrom = new DateTime(1956, 11, 19);
        DateTime DateTo = new DateTime(2100, 12, 31);
        int BGVFrom = 0;
        int BGVTo = 9999;
        int TimeZoneFrom = 0;
        int TimeZoneTo = 23;
 
        /**
         * WHERE句のクエリを実行するか、しないか
         */
        bool SearchInput = false;
 
 
        /**
         * データの一覧を表示するためのリスト
         */
        public IList<Value> Values { get;set; } = default!;
 
 
        /**
         * GETメソッド
         */
        public async Task OnGetAsync(string sortOrder)
        {
            /**
             * 昇順⇔降順の切替
             */
            BloodGlucoseValueSort = sortOrder == “BloodGlucoseValue” ? “BloodGlucoseValue_desc” : “BloodGlucoseValue”;
            DateSort = sortOrder == “Date” ? “Date_desc” : “Date”;
 
            /**
             * クエリ式の作成
             */
            IQueryable<Value> valueIQ = from s in _context.Value select s;
 
            /**
             * WHERE句を作成する
             */
             await CreateWhere(valueIQ);
 
            if (SearchInput)
            {
                /**
                 * 検索条件が入力されている
                 */
 
                valueIQ = valueIQ.Where(s => s.Date >= DateFrom && s.Date <= DateTo &&
                                       s.BloodGlucoseValue >= BGVFrom && s.BloodGlucoseValue <= BGVTo &&
                                       s.TimeZone >= TimeZoneFrom && s.TimeZone <= TimeZoneTo);
            }
 
            /**
             * Order句を作成する
             */
            if (string.IsNullOrEmpty(sortOrder))
            {
                sortOrder = “Date”;
 
            }
 
            bool descending = false;
            if (sortOrder.EndsWith(“_desc”))
            {
                sortOrder = sortOrder.Substring(0, sortOrder.Length – 5);
                descending = true;
 
            }
            if (descending)
            {
                if (sortOrder.Equals(“Date”))
                {
                    valueIQ = valueIQ.OrderByDescending(s => EF.Property<object>(s, sortOrder)).ThenBy(s => s.Time);
                   
                } else if ( sortOrder.Equals(“BloodGlucoseValue”))
                {
                    valueIQ = valueIQ.OrderByDescending(s => EF.Property<object>(s, sortOrder)).ThenBy(s => s.Date).ThenBy(s => s.Time);
                }
            }
            else
            {
                if (sortOrder.Equals(“Date”))
                {
                    valueIQ = valueIQ.OrderBy(s => EF.Property<object>(s, sortOrder)).ThenBy(s => s.Time);
 
                }
                else if (sortOrder.Equals(“BloodGlucoseValue”))
                {
                    valueIQ = valueIQ.OrderBy(s => s.BloodGlucoseValue).ThenBy(s => s.Date).ThenBy(s => s.Time);
                   
                }
            }
            /**
             * クエリの実行
             */
            Values = await valueIQ.AsNoTracking().ToListAsync();
        }
 
 
        /**
         * WHERE句を作成します
         */
        public async Task<IActionResult> CreateWhere(IQueryable<Value> valueIQ)
        {
            /**
             * 検索条件が1つ以上指定されている場合
             */
            if (DateFromFilter.HasValue ||
                DateToFilter.HasValue ||
                BGVFromFilter != null ||
                BGVToFilter != null ||
                TimeZoneFromFilter != null ||
                TimeZoneToFilter != null)
            {
                SearchInput = true;
 
                /**
                 * 日付の範囲条件が指定されているとき
                 */
                if (DateFromFilter.HasValue && DateToFilter.HasValue)
                /**
                 * From と To どちらにも検索値がある場合
                 */
                {
                    if (DateFromFilter == DateToFilter)
                    {
                        DateFrom = (DateTime)DateFromFilter;
                        DateTo = (DateTime)DateToFilter;
                    }
                    else if (DateFromFilter < DateToFilter)
                    {
                        DateFrom = (DateTime)DateFromFilter;
                        DateTo = (DateTime)DateToFilter;
                    }
                    else if (DateFromFilter > DateToFilter)
                    {
                        DateFrom = (DateTime)DateToFilter;
                        DateTo = (DateTime)DateFromFilter;
                    }
 
                }
                else if (DateFromFilter.HasValue || DateToFilter.HasValue)
                /**
                 * From または To のどちらかに検索値がある場合
                 */
                {
                    if (DateFromFilter.HasValue)
                    {
                        DateFrom = (DateTime)DateFromFilter;
                        DateTo = (DateTime)DateFromFilter;
                    }
                    else if (DateToFilter.HasValue)
                    {
                        DateFrom = (DateTime)DateToFilter;
                        DateTo = (DateTime)DateToFilter;
                    }
                }
 
                /**
                 * 血糖値の範囲条件が指定されているとき
                 */
                if (BGVFromFilter.HasValue && BGVToFilter.HasValue)
                /**
                 * From と To どちらにも検索値がある場合
                 */
                {
                    if (BGVFromFilter == BGVToFilter)
                    {
                        BGVFrom = (int)BGVFromFilter;
                        BGVTo = (int)BGVToFilter;
                    }
                    else if (BGVFromFilter < BGVToFilter)
                    {
                        BGVFrom = (int)BGVFromFilter;
                        BGVTo = (int)BGVToFilter;
                    }
                    else if (BGVFromFilter > BGVToFilter)
                    {
                        BGVFrom = (int)BGVToFilter;
                        BGVTo = (int)BGVFromFilter;
                    }
 
                }
                else if (BGVFromFilter.HasValue || BGVToFilter.HasValue)
                /**
                 * From または To のどちらかに検索値がある場合
                 */
                {
                    if (BGVFromFilter.HasValue)
                    {
                        BGVFrom = (int)BGVFromFilter;
                        BGVTo = (int)BGVFromFilter;
                    }
                    else if (BGVToFilter.HasValue)
                    {
                        BGVFrom = (int)BGVToFilter;
                        BGVTo = (int)BGVToFilter;
                    }
                }
 
                /**
                 * 時間帯の範囲条件が指定されているとき
                 */
                if (TimeZoneFromFilter.HasValue && TimeZoneToFilter.HasValue)
                /**
                 * From と To どちらにも検索値がある場合
                 */
                {
                    if (TimeZoneFromFilter == TimeZoneToFilter)
                    {
                        TimeZoneFrom = (int)TimeZoneFromFilter;
                        TimeZoneTo = (int)TimeZoneToFilter;
                    }
                    else if (TimeZoneFromFilter < TimeZoneToFilter)
                    {
                        TimeZoneFrom = (int)TimeZoneFromFilter;
                        TimeZoneTo = (int)TimeZoneToFilter;
                    }
                    else if (TimeZoneFromFilter > TimeZoneToFilter)
                    {
                        TimeZoneFrom = (int)TimeZoneToFilter;
                        TimeZoneTo = (int)TimeZoneFromFilter;
                    }
 
                }
                else if (TimeZoneFromFilter.HasValue || TimeZoneToFilter.HasValue)
                /**
                 * From または To のどちらかに検索値がある場合
                 */
                {
                    if (TimeZoneFromFilter.HasValue)
                    {
                        TimeZoneFrom = (int)TimeZoneFromFilter;
                        TimeZoneTo = (int)TimeZoneFromFilter;
                    }
                    else if (TimeZoneToFilter.HasValue)
                    {
                        TimeZoneFrom = (int)TimeZoneToFilter;
                        TimeZoneTo = (int)TimeZoneToFilter;
                    }
                }
            }
 
            return Page();
        }
    }
}

Razor Pages(Index.cshtml)の全コードを以下に示します。

Razor Pages  Index.cshtml
 
@page
@model BGC.Pages.IndexModel
 
@{
    ViewData[“Title”] = “血糖値管理”;
}
 
<h1>血糖値管理</h1>
 
<p>
    <a asp-page=”Create”>Create New</a><span>&nbsp&nbsp&nbsp</span><a href=”/”>Back to full List</a>
</p>
 
 
<form asp-page=”./Index” method=”get”>
    <table class=”table”>
        <thead>
            <tr>
                <th>日付(From)</th>
                <th>日付(To)</th>
                <th>血糖値(From)</th>
                <th>血糖値(To)</th>
                <th>時間帯(From)</th>
                <th>時間帯(To)</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td><input asp-for=”DateFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”DateToFilter” class=”form-control” /> </td>
                <td><input asp-for=”BGVFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”BGVToFilter” class=”form-control” /> </td>
                <td><input asp-for=”TimeZoneFromFilter” class=”form-control” /> </td>
                <td><input asp-for=”TimeZoneToFilter” class=”form-control” /> </td>
                <td><input type=”submit” value=”Search” class=”btn-primary” /></td>
            </tr>
 
        </tbody>
    </table>
    <span asp-validation-for=”DateFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”DateToFilter” class=”text-danger”></span>
    <span asp-validation-for=”BGVFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”BGVToFilter” class=”text-danger”></span>
    <span asp-validation-for=”TimeZoneFromFilter” class=”text-danger”></span>
    <span asp-validation-for=”TimeZoneToFilter” class=”text-danger”></span>
</form>
 
<table class=”table”>
    <thead>
        <tr>
            <th>
                <a asp-page=”/Index” asp-route-sortOrder=”@Model.DateSort”>
                    @Html.DisplayNameFor(model => model.Values[0].Date)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Values[0].Time)
            </th>
            <th>
                <a asp-page=”/Index” asp-route-sortOrder=”@Model.BloodGlucoseValueSort”>
                    @Html.DisplayNameFor(model => model.Values[0].BloodGlucoseValue)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Values[0].TimeZone)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Values[0].Comment)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Values[0].WorkFlg)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Values[0].FastingFlg)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model.Values) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Date)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Time)
            </td>
            <td>
                    @Html.DisplayFor(modelItem => item.BloodGlucoseValue)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.TimeZone)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Comment)
            </td>
           
                @Html.DisplayFor(modelItem => item.WorkFlg)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FastingFlg)
            </td>
            <td>
                <a asp-page=”./Edit” asp-route-id=”@item.id”>Edit</a> |
                <a asp-page=”./Details” asp-route-id=”@item.id”>Details</a> |
                <a asp-page=”./Delete” asp-route-id=”@item.id”>Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

追記
次回は、ページング機能を追加します。
さて、このブログを開始したのが昨年の12月1日でしたので、もうすこしで1年が経ちます。
孫のいるジイジのプログラミングの挑戦ということで始めてきました。
遅々とした歩みでありますが、着実に進んでいる実感があります。
こうして挑戦できるのも、現役のころに徹底して鍛えられたお陰だと思っています。
現役のころ(20年以上前)は、納期に追われ、連日終電帰りという生活を何度も送ってきました。
もちろん、今の時代、そうした働き方はコンプライアンス上許されません。
しかし、今、こうしてプログラミングに挑戦できるのも、そのころの必死の働きがあったからこそ、と思います。
そうした働きを通じて、「プログラミングには正解はない」「ベターなものを追求し、最善のものを提供していく」「自分が当事者となって、知恵を出して問題・課題を解決していく」ということを学びました。
プログラミングの習得に必要な気質、すなわち、自ら求めて技術を学ぶ、問題・課題の解決を他人任せにしないで自分で解決するという気持ちを持つ、ということを養えたように思います。
今は、納期に追われることはありません。品質を求められることもありません。対外折衝などもありません。
ひたすら、物づくりの楽しさを味わっています。

タイトルとURLをコピーしました