20200226のC#に関する記事は14件です。

検索+ソート機能付き一覧画面(MVC)・・・ASP.NET Core開発ノウハウ 4-1

今回のテーマ・課題

ASP.NET Core MVCアプリケーションで、EntityFrameworkを利用して作成したデータ一覧表示画面に検索機能と並べ替え機能を追加する。

1. 作業概要上のポイント

  1. ASP.NET CoreアプリケーションではRazorページアプリケーションでもMVCでも、ビューで使用する(=表示したり入力したりする)可変要素は全てページモデルのプロパティーとして割り当てる。
    一覧画面の場合、既定のモデルはListのように表示対象要素のリストである。
    この画面に検索条件やソート機能を追加する場合、以下のような事柄が画面UIから設定される。

    • どの項目を検索対象項目にするか?
    • 検索条件はどのようにするか?(=検索値は何か?一致条件はどうするか?)
    • どの項目をソート対象項目にするか?
    • ソートは昇順か?降順か?
  2. したがってページモデル側はそれらの項目をPropertyとして用意する必要がある。
    即ちEntityFrameworkのスキャフォールディングが自動生成してくれた大元のモデルのリストをメンバーの一つとして新たな「専用ページモデル」を作成する必要がある。一覧表示するリストはページモデルのプロパティーの一つとなる。

  3. ビュー側には検索条件+ソート条件設定用のUIを<form><input type=XXXX>として用意する。検索やソートはモデル側のデータを更新するわけではないので<form method="Get">となる。
    こうしておいて

    <form asp-action=[コントローラ名]>
        <div class="row">
            <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
            <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
        </div>
        ・・・
    

    のように書いて行けば、自動的に一覧画面ビューのGet時にコールされるメソッドで何も特別なことをしないでCrt_Nameを引数に指定するだけで自動的にバインドしてくれる。
    ここがASP.NET Coreの素晴らしいところ!


2. 元となるモデル(=Personal/ 個人データ)の作成とスキャフォールディング

\Models\Personal.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ANCEntry_EFMvcApp.Models
{
    /// <summary>
    /// 個人最小情報モデル
    /// </summary>
    public class Person
    {
        /// <summary>
        /// 個人ID。ScafoldingによりDB上では自動付番項目になる。
        /// </summary>
        public int PersonID { get; set; }

        /// <summary>
        /// 名前
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// 姓
        /// </summary>
        public string LastName { get; set;  }

        /// <summary>
        /// メールアドレス
        /// </summary>
        public string EMail { get; set; }

        /// <summary>
        /// 年齢・・・ゆくゆくは生年月日にして年齢を計算項目にする。
        /// </summary>
        public int Age { get; set; }

    }
}

【解説】

  1. まずはこのモデルをベースにしてスキャフォールディング使用して既定のコントローラとビューを作成し、続けてとマイグレーションを使用してデータベースにPersonテーブルを作成する。(スキャフォールディングの結果作成されたPersonControllerや\View\Person\Index.cshtmlを後続の処理で修正しながらこのノートの課題をクリアしていく。
    マイグレーションで作成されたPersonてーぶいる

  2. データベースに初期データを投入するための静的メソッドを持つSeedDataクラスを作ってProgram.csからコールさせる

\Models\SeedData.cs

using ANCEntry_EFMvcApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace ANCEntry_EFMvcApp.Models
{
    public class SeedData
    {

        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new ANCEntry_EFMvcAppContext(
                serviceProvider.GetRequiredService<DbContextOptions<ANCEntry_EFMvcAppContext>>()))
            {
                // Look for any movies.
                if (context.Person.Count() > 4)
                {
                    return; // DB has been seeded
                }

                context.Person.AddRange(
                    new Person
                    {
                        FirstName = "裕子",
                        LastName = "高橋",
                        EMail = "yukorin@gmail.com",
                        Age = 52
                    },

                    new Person
                    {
                        FirstName = "裕子",
                        LastName = "金子",
                        EMail = "yukorin@gmail.com",
                        Age = 45
                    },

                    new Person
                    {
                        FirstName = "ひさえ",
                        LastName = "加藤",
                        EMail = "hisane@gmail.com",
                        Age = 51
                    },

                    // こんな感じで投入データを書き加えていく。
                );
                context.SaveChanges();
            }
        }
    }
}

Program.csの中でSeedData.Initalizeをコールするには以下のような書き方が一般的

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ANCEntry_EFMvcApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    ANCEntry_EFMvcApp.Models.SeedData.Initialize(services);
                }
                catch (Exception exp)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(exp, "An error occurred seeding the DB.");
                }
            }
            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

こうしてアプリケーションを実行すると以下のようにPersonテーブルに初期データが挿入される。
4-2b.Personテーブル内容.jpg


3. Indexで使用する専用ページモデルの作成

Modelsフォルダ内に新しいモデルクラスを作成し、Personをリスト化してメンバーにする。

\Models\PersonalSearchModel.cs

using ANCEntry_EFMvcApp.Common;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;

namespace ANCEntry_EFMvcApp.Models
{
    public class PersonSearchModel
    {
        /// <summary>
        /// 項目名とソート方法(ASC/DSC)を繋ぐ区切り文字
        /// </summary>
        internal const char DLM = '$';

        // public List<Person> People { get; set; }
        /// <summary>
        /// ビュー側に一覧表示するモデル(=Person)のリスト
        /// </summary>
        public List<Person> People { get; set; }

        /// <summary>
        /// ビュー側で選択されたソート対象項目
        /// </summary>
        public string SortField { get; set; }

        /// <summary>
        /// 名前(FirstName/ LastNameの何れか)の検索値
        /// </summary>
        public string Crt_Name { get; set; }

        /// <summary>
        /// 年齢(Age)の検索値
        /// </summary>
        public string Crt_Age { get; set; }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクのタグヘルパーasp-for-SortFieldに使用する関数。
        /// コールされると元の引数に指定された昇順・降順を反転させて返す。
        /// 元の引数に昇順・降順の指定がない場合は既定で昇順をセットして返す。
        /// </summary>
        /// <param name="sortFieldName">元となる項目名。</param>
        /// <returns>
        /// リンククリック時に当該ページをコールバックする際のクエリパラメータ「SortField」にセットする
        /// 項目名+区切り文字+昇順・降順の指定を返す。
        /// </returns>
        public string GetSortFieldParamValue(string sortFieldName)
        {
            var result = sortFieldName;
            string direction = "ASC";
            if (!string.IsNullOrEmpty(this.SortField) &&
                (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "DESC";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "ASC";
                    }
                }
            }
            result += DLM + direction;
            return result;
        }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクの表示文字列の出力に使用する関数。
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットして返す。
        /// </summary>
        /// <param name="sortFieldName">ソート元となる項目名。</param>
        /// <returns>
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットした値。
        /// 一致しない場合は単に引数の項目名のみを返す。
        /// </returns>
        public string GetSortFieldDisplayName(string sortFieldName)
        {
            var result = sortFieldName;
            if (!string.IsNullOrEmpty(this.SortField) &&
                 (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string direction = "▲";
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▲";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▼";
                    }
                }
                result += direction;
            }
            return result;
        }
    }
}

【解説】

  1. 一覧表示する項目はList Peopleのみである。それ以外に以下のメンバーを有する。

    • 検索/並べ替え用のプロパティー(クエリパラメータとして引き渡されるため何れも文字列)
      • 名前(FirstName/ LastName共通)の検索値
      • 年齢の検索値
      • ソート条件(項目名と昇順・降順の指定を所定の区切り文字で繋ぐ)
    • 一覧表示グリッド項目欄にソート条件/ソート指定を行うためのメソッド
  2. ソート条件用のメソッドは画面上の項目名リンククリックで昇順・降順が反転するよにしており、項目表示メソッドは現在の並べ替え状態ではなく、次のクリックで並べ替えられる方向を示す。


4. ビューの修正

Index.cshtmlを修正して検索条件入力エリアを設け、また一覧表示テーブルのタイトル行の項目名をソート指定用のリンク(<a>)に修正する。
\Views\People\Index.cshtml

@model ANCEntry_EFMvcApp.Models.PersonSearchModel

@{
    ViewData["Title"] = "個人情報一覧";
}

<h1>個人情報一覧</h1>

@* 検索条件入力エリア *@
<form asp-controller="People" asp-action="Index" method="get">
    <div class="row">
        <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
        <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
    </div>
    <div class="row">
        <span class="col-md-5">年齢を入力</span>
        <input type="text" asp-for="Crt_Age" class="col-md-4 form-control" />
        <input type="submit" value="検索実行" class="col-2 btn btn-primary" />
    </div>
</form>

<table class="table">
    <thead>
        @* ヘッダー部の項目名にはソート条件式及び検索条件式をGet命令で引き渡す用意をさせている。 *@
        <tr>
            <th>
                @* リンクのアクションにはコントローラメソッド(=GET用)を指定して、その下に引き渡すクエリパラメータを列挙する。 
                    ここではModel側に用意したプロパティーやメソッドをフル活用する。 *@
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].PersonID))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].PersonID))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].FirstName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].FirstName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].LastName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].LastName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].EMail))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].EMail))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].Age))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].Age))
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.People)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.PersonID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EMail)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Age)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.PersonID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.PersonID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.PersonID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

【解説】

  1. 一覧表示画面において検索条件・ソート条件を入力して実行する箇所は<form>であっても<a>であっても必ず以下のタグヘルパーを付ける。

    • asp-controller=[コントローラ名(この場合People)]
    • asp-action=[メソッド名(=View名、この場合Index)]
    • <form>の場合はaction="get"を明示する。
    • これにより一覧検索画面は大抵の場合GET命令一つで足りる。
  2. 即ち自分自身にGET命令で戻すため、検索条件+ソート条件としてクエリパラメータを引き渡さないといけない。その値もタグに応じて以下のようにタグヘルパーで指定する。

    • <form>内の<input>のasp-forでページが受け付けるパラメータ名を指定。(大抵検索条件の値)
    • <a>であれば、asp-forでページがページが受け付けるパラメータ名を指定。(大抵ソート条件の値)
  3. 検索条件/ソート条件を受け付けるページでは、大抵の場合それらを全て複数パラメータとして引き受けるため、<a>タグで自身をコールバックする場合には往々にして一つのタグ内に複数のタグヘルパーを書く必要が生じる。

  4. asp-controllerasp-actionを使用できるのがMVCアプリケーションの強み!


5. 一覧ページアクションの修正

コントローラのIndexアクションを検索条件とソート条件をパラメータとして受け取るように修正し、実際の検索とソート処理を実装する。また検索条件によるデータ絞り込みにはトライアル的にSQLの直接実行を組み込む。

\Controllers\PeopleController

#define USE_RAWSQL

using ANCEntry_EFMvcApp.Common;
using ANCEntry_EFMvcApp.Data;
using ANCEntry_EFMvcApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;

// ★非常に重要 System.Data.SqlClientを使わないこと!
// ★特にSystem.Data.SqlClient.SqlParameterはEFのFromSqlRawでは使えない!

namespace ANCEntry_EFMvcApp.Controllers
{
    /// <summary>
    /// このクラス名によりアプリケーションからは/People/{メソッド名}にてPersonモデルに対する
    /// CRUD処理の呼び出しが可能。
    /// </summary>
    public class PeopleController : Controller
    {
        private const int PageSize = 5;

        private readonly ANCEntry_EFMvcAppContext _context;

        public PeopleController(ANCEntry_EFMvcAppContext context)
        {
            _context = context;
        }

        // ・・・・(途中省略)・・・・

        /// <summary>
        /// 検索ソート機能付き一覧画面表示用メソッド。
        /// 2項目用の検索値とソート条件指定を受け取る。
        /// </summary>
        /// <param name="crt_name">名前(FirstName/LastName共通)の検索値。一致条件は一部一致とする。</param>
        /// <param name="crt_age">年齢の検索条件。一致条件は前後5歳の幅を持たせる。</param>
        /// <param name="sortfield">ソート条件。項目名に所定の区切り文字で昇順・逆順指定を加える。</param>
        /// <returns>引数の検索+ソート条件で抽出されたレコードのリスト。更にLINQ式により変換が可能</returns>
        [HttpGet]
        public async Task<IActionResult> Index(string crt_name, string crt_age,
            string sortfield)
        {
            IQueryable<Person> selected;

#if USE_RAWSQL
            // こっちが有効、生のSQLが引き渡される。
            string strSQL = "SELECT * FROM dbo.Person";
            string strWhere = string.Empty;

            var param_name = new SqlParameter("@param_name", string.Empty);
            var param_age = new SqlParameter("@param_age", 0);
            param_age.Value = 0;

            if (!string.IsNullOrEmpty(crt_name))
            {
                param_name.Value = "%" + crt_name + "%";
                strWhere = "( FirstName LIKE @param_name ) OR ( LastName LIKE @param_name )";
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                var int_age = 0;
                if (Int32.TryParse(crt_age, out int_age))
                {
                    param_age.Value = int_age;
                    strWhere = (!string.IsNullOrEmpty(strWhere) ? string.Format("({0}) AND", strWhere) : string.Empty)
                        + "(( Age >= @param_age - 5 ) AND ( Age <= @param_age + 5 ))";
                }
            }
            if (!string.IsNullOrEmpty(strWhere))
            {
                strSQL = strSQL += Environment.NewLine + "WHERE " + strWhere;
            }

            // ★ショックなことにSQLでOrder byを書くとPaginatingListによるPagingが効かなくなる!
            //bool desc = false;
            //bool.TryParse(sortdesc, out desc);
            //if (!string.IsNullOrEmpty(sortfield))
            //{
            //    strSQL += Environment.NewLine +
            //        "ORDER BY " + sortfield + (desc ? " DESC" : string.Empty);
            //}
            selected = _context.Person.FromSqlRaw(strSQL, param_name, param_age);

#else
            // SQLを使わずに全てLINQで書く場合。
            selected = from p in _context.Person select p;
            if (!string.IsNullOrEmpty(crt_name))
            {
                selected = selected.Where(model =>
                (
                    (model.LastName.ToLower().Contains(crt_name.ToLower())) ||
                    (model.FirstName.ToLower().Contains(crt_name.ToLower()))
                ));
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                int age = 0;
                if (Int32.TryParse(crt_age, out age))
                {
                    selected =selected.Where(model =>
                        ((model.Age >= age - 5) && (model.Age <= age + 5)));
                }
            }
#endif
            // ★ショックなことにSQLでOrder byを使うとページング機能が使えないため
            // 下のようにグダグダ書かないといけない。

            bool desc = false;
            string sortfiledBody = string.Empty;
            if (!string.IsNullOrEmpty(sortfield))
            {
                string[] tokens = sortfield.Split(PersonFind2Model.DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        desc = true;
                    }
                    for(int i = 0; i < tokens.Length - 1; i++)
                    {
                        sortfiledBody += tokens[i] + PersonFind2Model.DLM;
                    }
                    sortfiledBody = sortfiledBody.Substring(0, sortfiledBody.Length - 1);
                }
                else
                {
                    sortfiledBody = sortfield;
                }

                // ★OrderByやOrderByDescendingが受けるのはFunc<T>ではなくて
                // Linq.Expression.Expression<Func<T>>である。
                // これは構文解析したFunc<T>なので、Func<T>を先に定義してから
                // その後で逆アセンブリすることはかなり難しい模様なのでいったんここで終了。
                switch (sortfiledBody.ToLower())
                {
                    case "personid":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.PersonID);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.PersonID);
                        }
                        break;

                    case "firstname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.FirstName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.FirstName);
                        }
                        break;

                    case "lastname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.LastName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.LastName);
                        }
                        break;

                    case "email":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.EMail);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.EMail);
                        }
                        break;

                    case "age":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.Age);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.Age);
                        }
                        break;
                }
            }
            else
            {
                if (desc)
                {
                    selected = selected.OrderByDescending(m => m.PersonID);
                }
                else
                {
                    selected = selected.OrderBy(m => m.PersonID);
                }
            }

            var peopleList = await selected.ToListAsync();
            var myModel = new PersonSearchModel
            {
                People = peopleList,
                Crt_Name = crt_name,
                Crt_Age = crt_age,
                SortField = sortfield,
            };

            return View(myModel);
        }

    }

【解説】

  1. ここではスキャフォールディングによるお任せLINQ式を使うのではなく可能な限りSQLを直書きする方法を紹介する。

    1. SQLを直書きする際には当該コントローラの処理対象メインモデル(この場合Person)のFromSqlRaw拡張メソッドを使用する。
      • FromRawSqlはMicrosoft.EntityFrameworkCore.RelationalQueryableExtensionsクラスで拡張メソッドとして定義されている。
    2. FromSqlRawにパラメータを引き渡す場合にはSystem.Data.SqlClientではなく、Microsoft.Data.SqlClientを使用すること。

    3. FromSqlRawの戻り値はIQueryable<TEntity>である。従ってこの戻り値に対してLINQ式を追加して絞り込み条件やソート条件の追加を行うことが出来る。

    4. ソート条件をFromRawSqlと"Order By"句を使用して実行すると、次節で説明する一覧表示グリッドのページングが実行できない。(=例外が返される。) 従ってソート条件はソースコードにある通り少々冗長になるがLINQに任せるしかない。
  2. IndexメソッドはGET要求しか受け取らないように[HttpGet]属性で修飾する。

    1. これにより自動的にメソッド引数であるcrt_name、crt_age、sortfieldはクエリパラメータとして受け取る。

    2. ビュー(.cshtml)側で検索条件やソート条件を指定する入力・リンクタグはタグヘルパー「asp-for-XXXX」でバインドするパラメータを指定するだけで、内部実装無しでこのメソッドに自動的に渡ってくる。

    ● 注意するのはasp-for-XXXXのパラメータ名部分のスペルミスのみ。

  3. 予告編として、次節で一覧表示対象となるpepleListをページングする方法を紹介する。


6. 表示結果

  1. 画面が最初に表示された状態
    一覧表示画面が最初に表示された状態

  2. 検索を実行
    一覧表示画面で検索を実行した状態

  3. ソートを実行
    一覧表示画面でソートを実行した状態


■ 参考サイト

ASP.NET Core MVC アプリへの検索の追加
(https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/search?view=aspnetcore-3.0)

ASP.NET Core の Razor Pages と EF Core - 並べ替え、フィルター、ページング - 3/8
(https://docs.microsoft.com/ja-jp/aspnet/core/data/ef-rp/sort-filter-page?view=aspnetcore-3.0)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Core開発ノウハウ 4-2:検索+ソート機能付き一覧画面(MVC)

Section 4. EntityFrameworkの拡張によるDB操作

4-2. 一覧画面での検索と並べ替え機能(MVCアプリケーション)

1) 作業概要上のポイント

  1. ASP.NET CoreアプリケーションではRazorページアプリケーションでもMVCでも、ビューで使用する(=表示したり入力したりする)可変要素は全てページモデルのプロパティーとして割り当てる。
    一覧画面の場合、既定のモデルはListのように表示対象要素のリストである。
    この画面に検索条件やソート機能を追加する場合、以下のような事柄が画面UIから設定される。

    • どの項目を検索対象項目にするか?
    • 検索条件はどのようにするか?(=検索値は何か?一致条件はどうするか?)
    • どの項目をソート対象項目にするか?
    • ソートは昇順か?降順か?
  2. したがってページモデル側はそれらの項目をPropertyとして用意する必要がある。
    即ちEntityFrameworkのスキャフォールディングが自動生成してくれた大元のモデルのリストをメンバーの一つとして新たな「専用ページモデル」を作成する必要がある。一覧表示するリストはページモデルのプロパティーの一つとなる。

  3. ビュー側には検索条件+ソート条件設定用のUIを<form><input type=XXXX>として用意する。検索やソートはモデル側のデータを更新するわけではないので<form method="Get">となる。
    こうしておいて

    <form asp-action=[コントローラ名]>
        <div class="row">
            <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
            <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
        </div>
        ・・・
    

    のように書いて行けば、自動的に一覧画面ビューのGet時にコールされるメソッドで何も特別なことをしないでCrt_Nameを引数に指定するだけで自動的にバインドしてくれる。
    ここがASP.NET Coreの素晴らしいところ!


2a) 大元のモデル(=Personal/ 個人データ)

\Models\Personal.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ANCEntry_EFMvcApp.Models
{
    /// <summary>
    /// 個人最小情報モデル
    /// </summary>
    public class Person
    {
        /// <summary>
        /// 個人ID。ScafoldingによりDB上では自動付番項目になる。
        /// </summary>
        public int PersonID { get; set; }

        /// <summary>
        /// 名前
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// 姓
        /// </summary>
        public string LastName { get; set;  }

        /// <summary>
        /// メールアドレス
        /// </summary>
        public string EMail { get; set; }

        /// <summary>
        /// 年齢・・・ゆくゆくは生年月日にして年齢を計算項目にする。
        /// </summary>
        public int Age { get; set; }

    }
}


2b) 専用ページモデル(Personをリスト化してメンバーにする)

\Models\PersonalSearchModel.cs

using ANCEntry_EFMvcApp.Common;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;

namespace ANCEntry_EFMvcApp.Models
{
    public class PersonSearchModel
    {
        /// <summary>
        /// 項目名とソート方法(ASC/DSC)を繋ぐ区切り文字
        /// </summary>
        internal const char DLM = '$';

        // public List<Person> People { get; set; }
        /// <summary>
        /// ビュー側に一覧表示するモデル(=Person)のリスト
        /// </summary>
        public List<Person> People { get; set; }

        /// <summary>
        /// ビュー側で選択されたソート対象項目
        /// </summary>
        public string SortField { get; set; }

        /// <summary>
        /// 名前(FirstName/ LastNameの何れか)の検索値
        /// </summary>
        public string Crt_Name { get; set; }

        /// <summary>
        /// 年齢(Age)の検索値
        /// </summary>
        public string Crt_Age { get; set; }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクのタグヘルパーasp-for-SortFieldに使用する関数。
        /// コールされると元の引数に指定された昇順・降順を反転させて返す。
        /// 元の引数に昇順・降順の指定がない場合は既定で昇順をセットして返す。
        /// </summary>
        /// <param name="sortFieldName">元となる項目名。</param>
        /// <returns>
        /// リンククリック時に当該ページをコールバックする際のクエリパラメータ「SortField」にセットする
        /// 項目名+区切り文字+昇順・降順の指定を返す。
        /// </returns>
        public string GetSortFieldParamValue(string sortFieldName)
        {
            var result = sortFieldName;
            string direction = "ASC";
            if (!string.IsNullOrEmpty(this.SortField) &&
                (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "DESC";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "ASC";
                    }
                }
            }
            result += DLM + direction;
            return result;
        }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクの表示文字列の出力に使用する関数。
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットして返す。
        /// </summary>
        /// <param name="sortFieldName">ソート元となる項目名。</param>
        /// <returns>
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットした値。
        /// 一致しない場合は単に引数の項目名のみを返す。
        /// </returns>
        public string GetSortFieldDisplayName(string sortFieldName)
        {
            var result = sortFieldName;
            if (!string.IsNullOrEmpty(this.SortField) &&
                 (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string direction = "▲";
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▲";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▼";
                    }
                }
                result += direction;
            }
            return result;
        }
    }
}

■ ポイント

  1. 一覧表示する項目はList Peopleのみである。それ以外に以下のメンバーを有する。

    • 検索/並べ替え用のプロパティー(クエリパラメータとして引き渡されるため何れも文字列)
      • 名前(FirstName/ LastName共通)の検索値
      • 年齢の検索値
      • ソート条件(項目名と昇順・降順の指定を所定の区切り文字で繋ぐ)
    • 一覧表示グリッド項目欄にソート条件/ソート指定を行うためのメソッド
  2. ソート条件用のメソッドは画面上の項目名リンククリックで昇順・降順が反転するよにしており、項目表示メソッドは現在の並べ替え状態ではなく、次のクリックで並べ替えられる方向を示す。


3) ビューの修正

\Views\People\Index.cshtml

@model ANCEntry_EFMvcApp.Models.PersonSearchModel

@{
    ViewData["Title"] = "個人情報一覧";
}

<h1>個人情報一覧</h1>

@* 検索条件入力エリア *@
<form asp-controller="People" asp-action="Index" method="get">
    <div class="row">
        <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
        <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
    </div>
    <div class="row">
        <span class="col-md-5">年齢を入力</span>
        <input type="text" asp-for="Crt_Age" class="col-md-4 form-control" />
        <input type="submit" value="検索実行" class="col-2 btn btn-primary" />
    </div>
</form>

<table class="table">
    <thead>
        @* ヘッダー部の項目名にはソート条件式及び検索条件式をGet命令で引き渡す用意をさせている。 *@
        <tr>
            <th>
                @* リンクのアクションにはコントローラメソッド(=GET用)を指定して、その下に引き渡すクエリパラメータを列挙する。 
                    ここではModel側に用意したプロパティーやメソッドをフル活用する。 *@
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].PersonID))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].PersonID))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].FirstName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].FirstName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].LastName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].LastName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].EMail))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].EMail))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].Age))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].Age))
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.People)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.PersonID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EMail)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Age)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.PersonID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.PersonID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.PersonID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

■ ポイント

  1. 一覧表示画面において検索条件・ソート条件を入力して実行する箇所は<form>であっても<a>であっても必ず以下のタグヘルパーを付ける。

    • asp-controller=[コントローラ名(この場合People)]
    • asp-action=[メソッド名(=View名、この場合Index)]
    • <form>の場合はaction="get"を明示する。
    • これにより一覧検索画面は大抵の場合GET命令一つで足りる。
  2. 即ち自分自身にGET命令で戻すため、検索条件+ソート条件としてクエリパラメータを引き渡さないといけない。その値もタグに応じて以下のようにタグヘルパーで指定する。

    • <form>内の<input>のasp-forでページが受け付けるパラメータ名を指定。(大抵検索条件の値)
    • <a>であれば、asp-forでページがページが受け付けるパラメータ名を指定。(大抵ソート条件の値)
  3. 検索条件/ソート条件を受け付けるページでは、大抵の場合それらを全て複数パラメータとして引き受けるため、<a>タグで自身をコールバックする場合には往々にして一つのタグ内に複数のタグヘルパーを書く必要が生じる。

  4. asp-controllerasp-actionを使用できるのがMVCアプリケーションの強み!


4) 一覧ページアクションの修正

\Controllers\PeopleController

#define USE_RAWSQL

using ANCEntry_EFMvcApp.Common;
using ANCEntry_EFMvcApp.Data;
using ANCEntry_EFMvcApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;

// ★非常に重要 System.Data.SqlClientを使わないこと!
// ★特にSystem.Data.SqlClient.SqlParameterはEFのFromSqlRawでは使えない!

namespace ANCEntry_EFMvcApp.Controllers
{
    /// <summary>
    /// このクラス名によりアプリケーションからは/People/{メソッド名}にてPersonモデルに対する
    /// CRUD処理の呼び出しが可能。
    /// </summary>
    public class PeopleController : Controller
    {
        private const int PageSize = 5;

        private readonly ANCEntry_EFMvcAppContext _context;

        public PeopleController(ANCEntry_EFMvcAppContext context)
        {
            _context = context;
        }

        // ・・・・(途中省略)・・・・

        /// <summary>
        /// 検索ソート機能付き一覧画面表示用メソッド。
        /// 2項目用の検索値とソート条件指定を受け取る。
        /// </summary>
        /// <param name="crt_name">名前(FirstName/LastName共通)の検索値。一致条件は一部一致とする。</param>
        /// <param name="crt_age">年齢の検索条件。一致条件は前後5歳の幅を持たせる。</param>
        /// <param name="sortfield">ソート条件。項目名に所定の区切り文字で昇順・逆順指定を加える。</param>
        /// <returns>引数の検索+ソート条件で抽出されたレコードのリスト。更にLINQ式により変換が可能</returns>
        [HttpGet]
        public async Task<IActionResult> Index(string crt_name, string crt_age,
            string sortfield)
        {
            IQueryable<Person> selected;

#if USE_RAWSQL
            // こっちが有効、生のSQLが引き渡される。
            string strSQL = "SELECT * FROM dbo.Person";
            string strWhere = string.Empty;

            var param_name = new SqlParameter("@param_name", string.Empty);
            var param_age = new SqlParameter("@param_age", 0);
            param_age.Value = 0;

            if (!string.IsNullOrEmpty(crt_name))
            {
                param_name.Value = "%" + crt_name + "%";
                strWhere = "( FirstName LIKE @param_name ) OR ( LastName LIKE @param_name )";
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                var int_age = 0;
                if (Int32.TryParse(crt_age, out int_age))
                {
                    param_age.Value = int_age;
                    strWhere = (!string.IsNullOrEmpty(strWhere) ? string.Format("({0}) AND", strWhere) : string.Empty)
                        + "(( Age >= @param_age - 5 ) AND ( Age <= @param_age + 5 ))";
                }
            }
            if (!string.IsNullOrEmpty(strWhere))
            {
                strSQL = strSQL += Environment.NewLine + "WHERE " + strWhere;
            }

            // ★ショックなことにSQLでOrder byを書くとPaginatingListによるPagingが効かなくなる!
            //bool desc = false;
            //bool.TryParse(sortdesc, out desc);
            //if (!string.IsNullOrEmpty(sortfield))
            //{
            //    strSQL += Environment.NewLine +
            //        "ORDER BY " + sortfield + (desc ? " DESC" : string.Empty);
            //}
            selected = _context.Person.FromSqlRaw(strSQL, param_name, param_age);

#else
            // SQLを使わずに全てLINQで書く場合。
            selected = from p in _context.Person select p;
            if (!string.IsNullOrEmpty(crt_name))
            {
                selected = selected.Where(model =>
                (
                    (model.LastName.ToLower().Contains(crt_name.ToLower())) ||
                    (model.FirstName.ToLower().Contains(crt_name.ToLower()))
                ));
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                int age = 0;
                if (Int32.TryParse(crt_age, out age))
                {
                    selected =selected.Where(model =>
                        ((model.Age >= age - 5) && (model.Age <= age + 5)));
                }
            }
#endif
            // ★ショックなことにSQLでOrder byを使えないため下のように
            // グダグダ書かないといけない。

            bool desc = false;
            string sortfiledBody = string.Empty;
            if (!string.IsNullOrEmpty(sortfield))
            {
                string[] tokens = sortfield.Split(PersonFind2Model.DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        desc = true;
                    }
                    for(int i = 0; i < tokens.Length - 1; i++)
                    {
                        sortfiledBody += tokens[i] + PersonFind2Model.DLM;
                    }
                    sortfiledBody = sortfiledBody.Substring(0, sortfiledBody.Length - 1);
                }
                else
                {
                    sortfiledBody = sortfield;
                }

                // ★OrderByやOrderByDescendingが受けるのはFunc<T>ではなくて
                // Linq.Expression.Expression<Func<T>>である。
                // これは構文解析したFunc<T>なので、Func<T>を先に定義してから
                // その後で逆アセンブリすることはかなり難しい模様なのでいったんここで終了。
                switch (sortfiledBody.ToLower())
                {
                    case "personid":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.PersonID);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.PersonID);
                        }
                        break;

                    case "firstname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.FirstName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.FirstName);
                        }
                        break;

                    case "lastname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.LastName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.LastName);
                        }
                        break;

                    case "email":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.EMail);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.EMail);
                        }
                        break;

                    case "age":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.Age);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.Age);
                        }
                        break;
                }
            }
            else
            {
                if (desc)
                {
                    selected = selected.OrderByDescending(m => m.PersonID);
                }
                else
                {
                    selected = selected.OrderBy(m => m.PersonID);
                }
            }

            var peopleList = await selected.ToListAsync();
            var myModel = new PersonSearchModel
            {
                People = peopleList,
                Crt_Name = crt_name,
                Crt_Age = crt_age,
                SortField = sortfield,
            };

            return View(myModel);
        }

    }

■ ポイント

  1. ここではスキャフォールディングによるお任せLINQ式を使うのではなく可能な限りSQLを直書きする方法を紹介する。

    1. SQLを直書きする際には当該コントローラの処理対象メインモデル(この場合Person)のFromSqlRaw拡張メソッドを使用する。
      • FromRawSqlはMicrosoft.EntityFrameworkCore.RelationalQueryableExtensionsクラスで拡張メソッドとして定義されている。
    2. FromSqlRawにパラメータを引き渡す場合にはSystem.Data.SqlClientではなく、Microsoft.Data.SqlClientを使用すること。

    3. FromSqlRawの戻り値はIQueryable<TEntity>である。従ってこの戻り値に対してLINQ式を追加して絞り込み条件やソート条件の追加を行うことが出来る。

    4. ソート条件をFromRawSqlと"Order By"句を使用して実行すると、次節で説明する一覧表示グリッドのページングが実行できない。(=例外が返される。) 従ってソート条件はソースコードにある通り少々冗長になるがLINQに任せるしかない。
  2. IndexメソッドはGET要求しか受け取らないように[HttpGet]属性で修飾する。

    1. これにより自動的にメソッド引数であるcrt_name、crt_age、sortfieldはクエリパラメータとして受け取る。

    2. ビュー(.cshtml)側で検索条件やソート条件を指定する入力・リンクタグはタグヘルパー「asp-for-XXXX」でバインドするパラメータを指定するだけで、内部実装無しでこのメソッドに自動的に渡ってくる。

    ● 注意するのはasp-for-XXXXのパラメータ名部分のスペルミスのみ。

  3. 予告編として、次節で一覧表示対象となるpepleListをページングする方法を紹介する。


5) 表示結果

  1. 画面が最初に表示された状態
    一覧表示画面が最初に表示された状態

  2. 検索を実行
    一覧表示画面で検索を実行した状態

  3. ソートを実行
    一覧表示画面でソートを実行した状態

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

検索+ソート機能付き一覧画面(MVC)・・・ASP.NET Core開発ノウハウ 4-2:

今回のテーマ・課題

ASP.NET Core MVCアプリケーションで、EntityFrameworkを利用して作成したデータ一覧表示画面に検索機能と並べ替え機能を追加する。

1. 作業概要上のポイント

  1. ASP.NET CoreアプリケーションではRazorページアプリケーションでもMVCでも、ビューで使用する(=表示したり入力したりする)可変要素は全てページモデルのプロパティーとして割り当てる。
    一覧画面の場合、既定のモデルはListのように表示対象要素のリストである。
    この画面に検索条件やソート機能を追加する場合、以下のような事柄が画面UIから設定される。

    • どの項目を検索対象項目にするか?
    • 検索条件はどのようにするか?(=検索値は何か?一致条件はどうするか?)
    • どの項目をソート対象項目にするか?
    • ソートは昇順か?降順か?
  2. したがってページモデル側はそれらの項目をPropertyとして用意する必要がある。
    即ちEntityFrameworkのスキャフォールディングが自動生成してくれた大元のモデルのリストをメンバーの一つとして新たな「専用ページモデル」を作成する必要がある。一覧表示するリストはページモデルのプロパティーの一つとなる。

  3. ビュー側には検索条件+ソート条件設定用のUIを<form><input type=XXXX>として用意する。検索やソートはモデル側のデータを更新するわけではないので<form method="Get">となる。
    こうしておいて

    <form asp-action=[コントローラ名]>
        <div class="row">
            <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
            <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
        </div>
        ・・・
    

    のように書いて行けば、自動的に一覧画面ビューのGet時にコールされるメソッドで何も特別なことをしないでCrt_Nameを引数に指定するだけで自動的にバインドしてくれる。
    ここがASP.NET Coreの素晴らしいところ!


2. 元となるモデル(=Personal/ 個人データ)の作成とスキャフォールディング

\Models\Personal.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ANCEntry_EFMvcApp.Models
{
    /// <summary>
    /// 個人最小情報モデル
    /// </summary>
    public class Person
    {
        /// <summary>
        /// 個人ID。ScafoldingによりDB上では自動付番項目になる。
        /// </summary>
        public int PersonID { get; set; }

        /// <summary>
        /// 名前
        /// </summary>
        public string FirstName { get; set; }

        /// <summary>
        /// 姓
        /// </summary>
        public string LastName { get; set;  }

        /// <summary>
        /// メールアドレス
        /// </summary>
        public string EMail { get; set; }

        /// <summary>
        /// 年齢・・・ゆくゆくは生年月日にして年齢を計算項目にする。
        /// </summary>
        public int Age { get; set; }

    }
}

【解説】

  1. まずはこのモデルをベースにしてスキャフォールディング使用して既定のコントローラとビューを作成し、続けてとマイグレーションを使用してデータベースにPersonテーブルを作成する。(スキャフォールディングの結果作成されたPersonControllerや\View\Person\Index.cshtmlを後続の処理で修正しながらこのノートの課題をクリアしていく。
    マイグレーションで作成されたPersonてーぶいる

  2. データベースに初期データを投入するための静的メソッドを持つSeedDataクラスを作ってProgram.csからコールさせる

\Models\SeedData.cs

using ANCEntry_EFMvcApp.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;

namespace ANCEntry_EFMvcApp.Models
{
    public class SeedData
    {

        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new ANCEntry_EFMvcAppContext(
                serviceProvider.GetRequiredService<DbContextOptions<ANCEntry_EFMvcAppContext>>()))
            {
                // Look for any movies.
                if (context.Person.Count() > 4)
                {
                    return; // DB has been seeded
                }

                context.Person.AddRange(
                    new Person
                    {
                        FirstName = "裕子",
                        LastName = "高橋",
                        EMail = "yukorin@gmail.com",
                        Age = 52
                    },

                    new Person
                    {
                        FirstName = "裕子",
                        LastName = "金子",
                        EMail = "yukorin@gmail.com",
                        Age = 45
                    },

                    new Person
                    {
                        FirstName = "ひさえ",
                        LastName = "加藤",
                        EMail = "hisane@gmail.com",
                        Age = 51
                    },

                    // こんな感じで投入データを書き加えていく。
                );
                context.SaveChanges();
            }
        }
    }
}

Program.csの中でSeedData.Initalizeをコールするには以下のような書き方が一般的

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ANCEntry_EFMvcApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    ANCEntry_EFMvcApp.Models.SeedData.Initialize(services);
                }
                catch (Exception exp)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(exp, "An error occurred seeding the DB.");
                }
            }
            host.Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

こうしてアプリケーションを実行すると以下のようにPersonテーブルに初期データが挿入される。
4-2b.Personテーブル内容.jpg


3. Indexで使用する専用ページモデルの作成

Modelsフォルダ内に新しいモデルクラスを作成し、Personをリスト化してメンバーにする。

\Models\PersonalSearchModel.cs

using ANCEntry_EFMvcApp.Common;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;

namespace ANCEntry_EFMvcApp.Models
{
    public class PersonSearchModel
    {
        /// <summary>
        /// 項目名とソート方法(ASC/DSC)を繋ぐ区切り文字
        /// </summary>
        internal const char DLM = '$';

        // public List<Person> People { get; set; }
        /// <summary>
        /// ビュー側に一覧表示するモデル(=Person)のリスト
        /// </summary>
        public List<Person> People { get; set; }

        /// <summary>
        /// ビュー側で選択されたソート対象項目
        /// </summary>
        public string SortField { get; set; }

        /// <summary>
        /// 名前(FirstName/ LastNameの何れか)の検索値
        /// </summary>
        public string Crt_Name { get; set; }

        /// <summary>
        /// 年齢(Age)の検索値
        /// </summary>
        public string Crt_Age { get; set; }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクのタグヘルパーasp-for-SortFieldに使用する関数。
        /// コールされると元の引数に指定された昇順・降順を反転させて返す。
        /// 元の引数に昇順・降順の指定がない場合は既定で昇順をセットして返す。
        /// </summary>
        /// <param name="sortFieldName">元となる項目名。</param>
        /// <returns>
        /// リンククリック時に当該ページをコールバックする際のクエリパラメータ「SortField」にセットする
        /// 項目名+区切り文字+昇順・降順の指定を返す。
        /// </returns>
        public string GetSortFieldParamValue(string sortFieldName)
        {
            var result = sortFieldName;
            string direction = "ASC";
            if (!string.IsNullOrEmpty(this.SortField) &&
                (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "DESC";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "ASC";
                    }
                }
            }
            result += DLM + direction;
            return result;
        }

        /// <summary>
        /// ビューの一覧表示グリッドの項目名リンクの表示文字列の出力に使用する関数。
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットして返す。
        /// </summary>
        /// <param name="sortFieldName">ソート元となる項目名。</param>
        /// <returns>
        /// 当該クラスのソート項目が引数と一致する場合、現在の昇順・降順の指定とは逆方向の
        /// アイコンを項目名の右隣にセットした値。
        /// 一致しない場合は単に引数の項目名のみを返す。
        /// </returns>
        public string GetSortFieldDisplayName(string sortFieldName)
        {
            var result = sortFieldName;
            if (!string.IsNullOrEmpty(this.SortField) &&
                 (this.SortField.StartsWith(sortFieldName, StringComparison.CurrentCultureIgnoreCase)))
            {
                string direction = "▲";
                string[] tokens = this.SortField.Split(DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("ASC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▲";
                    }
                    else if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        direction = "▼";
                    }
                }
                result += direction;
            }
            return result;
        }
    }
}

【解説】

  1. 一覧表示する項目はList Peopleのみである。それ以外に以下のメンバーを有する。

    • 検索/並べ替え用のプロパティー(クエリパラメータとして引き渡されるため何れも文字列)
      • 名前(FirstName/ LastName共通)の検索値
      • 年齢の検索値
      • ソート条件(項目名と昇順・降順の指定を所定の区切り文字で繋ぐ)
    • 一覧表示グリッド項目欄にソート条件/ソート指定を行うためのメソッド
  2. ソート条件用のメソッドは画面上の項目名リンククリックで昇順・降順が反転するよにしており、項目表示メソッドは現在の並べ替え状態ではなく、次のクリックで並べ替えられる方向を示す。


4. ビューの修正

Index.cshtmlを修正して検索条件入力エリアを設け、また一覧表示テーブルのタイトル行の項目名をソート指定用のリンク(<a>)に修正する。
\Views\People\Index.cshtml

@model ANCEntry_EFMvcApp.Models.PersonSearchModel

@{
    ViewData["Title"] = "個人情報一覧";
}

<h1>個人情報一覧</h1>

@* 検索条件入力エリア *@
<form asp-controller="People" asp-action="Index" method="get">
    <div class="row">
        <span class="col-md-5">氏名の検索値を入力(一部一致検索)</span>
        <input type="text" asp-for="Crt_Name" class="col-md-4 form-control" />
    </div>
    <div class="row">
        <span class="col-md-5">年齢を入力</span>
        <input type="text" asp-for="Crt_Age" class="col-md-4 form-control" />
        <input type="submit" value="検索実行" class="col-2 btn btn-primary" />
    </div>
</form>

<table class="table">
    <thead>
        @* ヘッダー部の項目名にはソート条件式及び検索条件式をGet命令で引き渡す用意をさせている。 *@
        <tr>
            <th>
                @* リンクのアクションにはコントローラメソッド(=GET用)を指定して、その下に引き渡すクエリパラメータを列挙する。 
                    ここではModel側に用意したプロパティーやメソッドをフル活用する。 *@
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].PersonID))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].PersonID))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].FirstName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].FirstName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].LastName))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].LastName))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].EMail))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].EMail))
                </a>
            </th>
            <th>
                <a asp-controller="People" action="Index"
                   asp-route-Crt_Name="@Model.Crt_Name"
                   asp-route-Crt_Age="@Model.Crt_Age"
                   asp-route-SortField="@Model.GetSortFieldParamValue(Html.DisplayNameFor(model => model.People[0].Age))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].Age))
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.People)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.PersonID)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EMail)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Age)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.PersonID">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.PersonID">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.PersonID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

【解説】

  1. 一覧表示画面において検索条件・ソート条件を入力して実行する箇所は<form>であっても<a>であっても必ず以下のタグヘルパーを付ける。

    • asp-controller=[コントローラ名(この場合People)]
    • asp-action=[メソッド名(=View名、この場合Index)]
    • <form>の場合はaction="get"を明示する。
    • これにより一覧検索画面は大抵の場合GET命令一つで足りる。
  2. 即ち自分自身にGET命令で戻すため、検索条件+ソート条件としてクエリパラメータを引き渡さないといけない。その値もタグに応じて以下のようにタグヘルパーで指定する。

    • <form>内の<input>のasp-forでページが受け付けるパラメータ名を指定。(大抵検索条件の値)
    • <a>であれば、asp-forでページがページが受け付けるパラメータ名を指定。(大抵ソート条件の値)
  3. 検索条件/ソート条件を受け付けるページでは、大抵の場合それらを全て複数パラメータとして引き受けるため、<a>タグで自身をコールバックする場合には往々にして一つのタグ内に複数のタグヘルパーを書く必要が生じる。

  4. asp-controllerasp-actionを使用できるのがMVCアプリケーションの強み!


5. 一覧ページアクションの修正

コントローラのIndexアクションを検索条件とソート条件をパラメータとして受け取るように修正し、実際の検索とソート処理を実装する。また検索条件によるデータ絞り込みにはトライアル的にSQLの直接実行を組み込む。

\Controllers\PeopleController

#define USE_RAWSQL

using ANCEntry_EFMvcApp.Common;
using ANCEntry_EFMvcApp.Data;
using ANCEntry_EFMvcApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Data.SqlClient;

// ★非常に重要 System.Data.SqlClientを使わないこと!
// ★特にSystem.Data.SqlClient.SqlParameterはEFのFromSqlRawでは使えない!

namespace ANCEntry_EFMvcApp.Controllers
{
    /// <summary>
    /// このクラス名によりアプリケーションからは/People/{メソッド名}にてPersonモデルに対する
    /// CRUD処理の呼び出しが可能。
    /// </summary>
    public class PeopleController : Controller
    {
        private const int PageSize = 5;

        private readonly ANCEntry_EFMvcAppContext _context;

        public PeopleController(ANCEntry_EFMvcAppContext context)
        {
            _context = context;
        }

        // ・・・・(途中省略)・・・・

        /// <summary>
        /// 検索ソート機能付き一覧画面表示用メソッド。
        /// 2項目用の検索値とソート条件指定を受け取る。
        /// </summary>
        /// <param name="crt_name">名前(FirstName/LastName共通)の検索値。一致条件は一部一致とする。</param>
        /// <param name="crt_age">年齢の検索条件。一致条件は前後5歳の幅を持たせる。</param>
        /// <param name="sortfield">ソート条件。項目名に所定の区切り文字で昇順・逆順指定を加える。</param>
        /// <returns>引数の検索+ソート条件で抽出されたレコードのリスト。更にLINQ式により変換が可能</returns>
        [HttpGet]
        public async Task<IActionResult> Index(string crt_name, string crt_age,
            string sortfield)
        {
            IQueryable<Person> selected;

#if USE_RAWSQL
            // こっちが有効、生のSQLが引き渡される。
            string strSQL = "SELECT * FROM dbo.Person";
            string strWhere = string.Empty;

            var param_name = new SqlParameter("@param_name", string.Empty);
            var param_age = new SqlParameter("@param_age", 0);
            param_age.Value = 0;

            if (!string.IsNullOrEmpty(crt_name))
            {
                param_name.Value = "%" + crt_name + "%";
                strWhere = "( FirstName LIKE @param_name ) OR ( LastName LIKE @param_name )";
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                var int_age = 0;
                if (Int32.TryParse(crt_age, out int_age))
                {
                    param_age.Value = int_age;
                    strWhere = (!string.IsNullOrEmpty(strWhere) ? string.Format("({0}) AND", strWhere) : string.Empty)
                        + "(( Age >= @param_age - 5 ) AND ( Age <= @param_age + 5 ))";
                }
            }
            if (!string.IsNullOrEmpty(strWhere))
            {
                strSQL = strSQL += Environment.NewLine + "WHERE " + strWhere;
            }

            // ★ショックなことにSQLでOrder byを書くとPaginatingListによるPagingが効かなくなる!
            //bool desc = false;
            //bool.TryParse(sortdesc, out desc);
            //if (!string.IsNullOrEmpty(sortfield))
            //{
            //    strSQL += Environment.NewLine +
            //        "ORDER BY " + sortfield + (desc ? " DESC" : string.Empty);
            //}
            selected = _context.Person.FromSqlRaw(strSQL, param_name, param_age);

#else
            // SQLを使わずに全てLINQで書く場合。
            selected = from p in _context.Person select p;
            if (!string.IsNullOrEmpty(crt_name))
            {
                selected = selected.Where(model =>
                (
                    (model.LastName.ToLower().Contains(crt_name.ToLower())) ||
                    (model.FirstName.ToLower().Contains(crt_name.ToLower()))
                ));
            }
            if (!string.IsNullOrEmpty(crt_age))
            {
                int age = 0;
                if (Int32.TryParse(crt_age, out age))
                {
                    selected =selected.Where(model =>
                        ((model.Age >= age - 5) && (model.Age <= age + 5)));
                }
            }
#endif
            // ★ショックなことにSQLでOrder byを使うとページング機能が使えないため
            // 下のようにグダグダ書かないといけない。

            bool desc = false;
            string sortfiledBody = string.Empty;
            if (!string.IsNullOrEmpty(sortfield))
            {
                string[] tokens = sortfield.Split(PersonFind2Model.DLM);
                if (tokens.Length > 1)
                {
                    var sortDirection = tokens[tokens.Length - 1];
                    if (sortDirection.Equals("DESC", StringComparison.CurrentCultureIgnoreCase))
                    {
                        desc = true;
                    }
                    for(int i = 0; i < tokens.Length - 1; i++)
                    {
                        sortfiledBody += tokens[i] + PersonFind2Model.DLM;
                    }
                    sortfiledBody = sortfiledBody.Substring(0, sortfiledBody.Length - 1);
                }
                else
                {
                    sortfiledBody = sortfield;
                }

                // ★OrderByやOrderByDescendingが受けるのはFunc<T>ではなくて
                // Linq.Expression.Expression<Func<T>>である。
                // これは構文解析したFunc<T>なので、Func<T>を先に定義してから
                // その後で逆アセンブリすることはかなり難しい模様なのでいったんここで終了。
                switch (sortfiledBody.ToLower())
                {
                    case "personid":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.PersonID);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.PersonID);
                        }
                        break;

                    case "firstname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.FirstName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.FirstName);
                        }
                        break;

                    case "lastname":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.LastName);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.LastName);
                        }
                        break;

                    case "email":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.EMail);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.EMail);
                        }
                        break;

                    case "age":
                        if (desc)
                        {
                            selected = selected.OrderByDescending(m => m.Age);
                        }
                        else
                        {
                            selected = selected.OrderBy(m => m.Age);
                        }
                        break;
                }
            }
            else
            {
                if (desc)
                {
                    selected = selected.OrderByDescending(m => m.PersonID);
                }
                else
                {
                    selected = selected.OrderBy(m => m.PersonID);
                }
            }

            var peopleList = await selected.ToListAsync();
            var myModel = new PersonSearchModel
            {
                People = peopleList,
                Crt_Name = crt_name,
                Crt_Age = crt_age,
                SortField = sortfield,
            };

            return View(myModel);
        }

    }

【解説】

  1. ここではスキャフォールディングによるお任せLINQ式を使うのではなく可能な限りSQLを直書きする方法を紹介する。

    1. SQLを直書きする際には当該コントローラの処理対象メインモデル(この場合Person)のFromSqlRaw拡張メソッドを使用する。
      • FromRawSqlはMicrosoft.EntityFrameworkCore.RelationalQueryableExtensionsクラスで拡張メソッドとして定義されている。
    2. FromSqlRawにパラメータを引き渡す場合にはSystem.Data.SqlClientではなく、Microsoft.Data.SqlClientを使用すること。

    3. FromSqlRawの戻り値はIQueryable<TEntity>である。従ってこの戻り値に対してLINQ式を追加して絞り込み条件やソート条件の追加を行うことが出来る。

    4. ソート条件をFromRawSqlと"Order By"句を使用して実行すると、次節で説明する一覧表示グリッドのページングが実行できない。(=例外が返される。) 従ってソート条件はソースコードにある通り少々冗長になるがLINQに任せるしかない。
  2. IndexメソッドはGET要求しか受け取らないように[HttpGet]属性で修飾する。

    1. これにより自動的にメソッド引数であるcrt_name、crt_age、sortfieldはクエリパラメータとして受け取る。

    2. ビュー(.cshtml)側で検索条件やソート条件を指定する入力・リンクタグはタグヘルパー「asp-for-XXXX」でバインドするパラメータを指定するだけで、内部実装無しでこのメソッドに自動的に渡ってくる。

    ● 注意するのはasp-for-XXXXのパラメータ名部分のスペルミスのみ。

  3. 予告編として、次節で一覧表示対象となるpepleListをページングする方法を紹介する。


6. 表示結果

  1. 画面が最初に表示された状態
    一覧表示画面が最初に表示された状態

  2. 検索を実行
    一覧表示画面で検索を実行した状態

  3. ソートを実行
    一覧表示画面でソートを実行した状態


■ 参考サイト

ASP.NET Core MVC アプリへの検索の追加
(https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/first-mvc-app/search?view=aspnetcore-3.0)

ASP.NET Core の Razor Pages と EF Core - 並べ替え、フィルター、ページング - 3/8
(https://docs.microsoft.com/ja-jp/aspnet/core/data/ef-rp/sort-filter-page?view=aspnetcore-3.0)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながらWPFに置き換えてみる(3)

エクスパンダコントロールはいまいちよくわからないのでVBでの実装と同じように自力でセッティングの画像をクリックしたら下に設定項目を表示することにする。
ちっこい▼のICONをプロジェクトに組み込んでBITMAP化し、Imageコントロールに表示する。
単純にTOBITMAP()でセットはできないようなのでBITMAP化した後でハンドルを取得して、ハンドル経由で取得する。

System.Drawing.Icon ico = Properties.Resources.ctl_close1;
System.Drawing.Bitmap bitmap = ico.ToBitmap();
IntPtr hbitmap = bitmap.GetHbitmap();
this.image1.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());

あと軽くはまったのがデザイナ上でオブジェクトをツリー形式で登録する場合の順番。
先に登録しているコントロールのイベントで、後述のコントロールのプロパティを変更しようとするとエラーになる。

スライダの値を表示させるラベルオブジェクトをデザイナ上でスライダの後に記述すると、スライダのChange Valueイベントに記述しているラベルのCONTENTへの代入で
「オブジェクト参照がオブジェクト インスタンスに設定されていません。」が出てしまう。
下のようにラベルをスライダの前に移動することでエラーは解消。

デザイナ.png

当たり前といえば当たりまえなのだが、デザイナに慣れてしまっているとコントロール記述の順番とかほとんど意識しないのでやってしまいがち。

デザイナ上で何となくスタックパネル使って設定部のON/OFFしようとおもってた。でOFFの場合は下のパネルのTOP切り替えればいいかなくらいに考えてたのだが、TOPとかそういうプロパティはないんですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPF(C#)でSwitchBot(BLEデバイス)を操作する

概要

UWP(C#)でSwitchBot(BLEデバイス)を操作する - Qiita
のWPF編
微妙に書き方が違ったり、できることが違ったりする。

環境

  • Windows10
  • Visual Studio 2017(C#)
  • SwitchBot
  • UwpDesktop(nuget)

WPFといいつつ、BLE周りのAPIはUWPにしかないので、UwpDesktopをnugetで入れる。

ソースコード

経緯はUWP編の方で書いたので、最終的なコードだけ

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.Advertisement;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using System.Threading;
using System.Runtime.InteropServices.WindowsRuntime;

namespace SwitchBotTry
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private BluetoothLEAdvertisementWatcher advWatcher;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void BtnScan_Click(object sender, RoutedEventArgs e)
        {
            await StartScan();
        }

        private async Task StartScan()
        {
            this.advWatcher = new BluetoothLEAdvertisementWatcher();
            this.advWatcher.SignalStrengthFilter.SamplingInterval = TimeSpan.FromMilliseconds(1000);
            this.advWatcher.ScanningMode = BluetoothLEScanningMode.Active;
            this.advWatcher.Received += this.Watcher_Received;

            // スキャン開始
            this.advWatcher.Start();
        }

        private Guid switchUUID = new Guid("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
        private Guid commandUUID = new Guid("cba20002-224d-11e6-9fb8-0002a5d5c51b");
        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var targetMac = "****"; // 事前に調べたMAC
            if (mac == targetMac)
            {
                //アドバタイスパケットの中身を羅列する
                foreach (var adv in args.Advertisement.DataSections)
                {
                    addLog(" " + adv.DataType.ToString("x") + ":" + BitConverter.ToString(adv.Data.ToArray()));
                }
            }
            //目当てのUUIDで絞り込み
            if (bleServiceUUID == switchUUID)
            {
                advWatcher.Stop(); //デバイスを見つけたので止める
                try
                {
                    //接続
                    BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    //サービス・キャラクタリスティックを列挙
                    addLog("get service");
                    foreach (var service in device.GattServices)
                    {
                        addLog(service.Uuid.ToString());
                        addLog("   get characteristics");
                        foreach (var ch in service.GetAllCharacteristics())
                        {
                            addLog("     "  + ch.Uuid.ToString() + "  " + ch.CharacteristicProperties.ToString());
                            if (ch.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read)){
                                var val = await ch.ReadValueAsync();
                                var data = val.Value.ToArray();
                                string text = System.Text.Encoding.UTF8.GetString(data);
                                addLog("     value " + BitConverter.ToString(data) + " >> " + text);

                            }
                        }
                    }
                    //サービスUUIDを使って目的のサービスを取得
                    //asyncバージョンは使えない
                    var gattService = device.GetGattService(switchUUID);
                    //キャラクタリスティックUUIDを使って目的のキャラクタリスティックを取得
                    //asyncバージョンは使えない
                    var characteristics = gattService.GetCharacteristics(commandUUID);
                    //戻ってくるのが配列なので空じゃないか確認
                    if (characteristics.Count > 0)
                    {
                        var command = characteristics.First();
                        if (command.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write))
                        {
                            byte[] comPress = { 0x57, 0x01, 0x00};
                            var res = await command.WriteValueAsync(comPress.AsBuffer(), GattWriteOption.WriteWithResponse);
                            addLog(res.ToString());
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception...{ex.Message})");
                }
            }
        }

        private void addLog(String log)
        {
            Dispatcher.Invoke(new Action<string>(loglog =>
            {
                this.txtLog.Text += loglog + "\r\n";
            }), log);

            Console.WriteLine(log);
        }

        private void BtnStop_Click(object sender, RoutedEventArgs e)
        {
            this.advWatcher.Stop();
        }
    }
}

UWPとの違い(制限事項)

  • サービス/キャラクタリスティックの取得メソッドが違う

中身的にはUWPのAPIを呼んでるだけのはずなのだが、asyncの方はWPFからは呼べない。
それに伴って、エラーチェック周りの処理も変わる。

  • 事前にペアリングしたSwitchBotしか操作できない

ペアリングしていないSwitchBotも見つかるが、GattServicesでサービスを取得できない。
ソースコード中でペアリングすることも考えたが、煩雑になるのでやめて、UWPに移行した。
UWPでは事前のペアリングに関わらずコマンドを送信することができる。
SwitchBotのiOSアプリではペアリング有無に関わらず、電波の届く範囲のSwitchBotが検出/操作できるのでUWPではそれに近い実装ができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UWP(C#)でSwitchBot(BLEデバイス)を操作する

概要

SwitchBotで何かできない?と上司に言われ「C#から操作できればHoloLensで遊べるんじゃね?(ピコーン」ってなったはいいけど、世の中に情報がなくて辛かったのでまとめ。
最初はWPFでやってたのですが、限界を感じてUWPでやりました。
WPF編はこちら → WPF(C#)でSwitchBot(BLEデバイス)を操作する - Qiita

参考

環境

  • Windows10
  • Visual Studio 2017(C#)
  • SwitchBot

まずはラズパイで

公式のコードがラズパイ向けなので、まずは動かしてみる。

が、動かない。
iOSアプリでデバイスのMACは取れたのでBLEスキャンしてるあたりを適当にデバッグ。
結果的に

service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'

これを

service_uuid = 'cba20d00-224d-11e6-9fb8-0002a5d5c51b'

に直したら動いた。

UWPでスキャン

BLEで調べるとアドバタイズだのサービスだのキャラクタリスティックだのUUIDだの出てくる。
よくわからない。
とりあえずスキャンしてみる。addLogはデバッグ用のTextBoxに追記する関数。

private async Task StartScan()
        {
            //ウォッチャー作成
            this.advWatcher = new BluetoothLEAdvertisementWatcher();
            this.advWatcher.SignalStrengthFilter.SamplingInterval = TimeSpan.FromMilliseconds(1000);
            this.advWatcher.Received += this.Watcher_Received;
            // スキャン開始
            this.advWatcher.Start();
        }

        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            addLog("MAC " + mac);
            addLog("uuid " + bleServiceUUID);
        }

嵐のように出てくる。
あとUUIDが取れない。なんでやねん。

なるほど。なるほど?
単なるスキャンでは基本的な情報しか取れず、詳細情報をデバイスにリクエストする必要があるらしい。(初期スキャンにUUIDが載ってくる場合もあるらしい)
ので追記。
MS公式曰く、消費電力が増えるらしいが、情報取れないんじゃ何もできないんだから仕方ない。

this.advWatcher.ScanningMode = BluetoothLEScanningMode.Active;

あとドバドバ出てくるのが心臓に悪いのでフィルタ。

private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*****"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x2");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //目当てのデバイスだけ
            if (mac == targetMac)
            {
                addLog("name " + name);
                addLog("uuid " + bleServiceUUID);
                //アドバタイズデータを羅列
                foreach (var adv in args.Advertisement.DataSections)
                {
                    addLog(" " + adv.DataType.ToString("x") + ":" + BitConverter.ToString(adv.Data.ToArray()));
                }
            }
        }
実行結果
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid cba20d00-224d-11e6-9fb8-0002a5d5c51b
 07:1B-C5-D5-A5-02-00-B8-9F-E6-11-4D-22-00-0D-A2-CB
 16:00-0D-48-10-64

複数回受信して、内容が変わっているのが分かる。
Advertising(解説) - BLE Docs
の通り、UUIDがType=07に入っている。
でもってデコード前のバイト列をPythonのコードと比べてみると

service_uuid = '1bc5d5a50200b89fe6114d22000da2cb'

なので、Pythonのコードはデコード前のUUIDを期待してたっぽい。

サービスとキャラクタリスティック

サービスを取って、キャラクタリスティックを取って、というのがお作法っぽいのでとりあえず全部取って羅列してみる。

private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*****"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //目当てのデバイスだけ
            if (mac == targetMac)
            {
                addLog("name " + name);
                addLog("uuid " + bleServiceUUID);
                //アドバタイズデータを羅列
                foreach (var adv in args.Advertisement.DataSections)
                {
                    addLog(" " + adv.DataType.ToString("x") + ":" + BitConverter.ToString(adv.Data.ToArray()));
                }
            }
            //目当てのデバイスのUUIDが取れたら
            if (mac == targetMac && bleServiceUUID != Guid.Empty)
            {
                advWatcher.Stop();
                try
                {
                    //接続
                    BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    addLog("get service");
                    var services = await device.GetGattServicesAsync();
                    foreach (var serv in services.Services)
                    {
                        addLog(serv.Uuid.ToString());
                        addLog("  get characteristics");
                        var charcteristic = await serv.GetCharacteristicsAsync();
                        foreach (var ch in charcteristic.Characteristics)
                        {
                            addLog("     " + ch.Uuid.ToString() + "  " + ch.CharacteristicProperties.ToString());
                            //読めそうなら読み取ってデコードしてみる
                            if (ch.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Read))
                            {
                                var val = await ch.ReadValueAsync();
                                var data = val.Value.ToArray();
                                string text = System.Text.Encoding.UTF8.GetString(data);
                                addLog("     value " + BitConverter.ToString(data) + " >> " + text);
                            }
                        }
                    }         
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception...{ex.Message})");
                }
            }
        }
実行結果
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid 00000000-0000-0000-0000-000000000000
 01:06
 ff:59-00-FD-CF-4B-A5-38-C4
name 
uuid cba20d00-224d-11e6-9fb8-0002a5d5c51b
 07:1B-C5-D5-A5-02-00-B8-9F-E6-11-4D-22-00-0D-A2-CB
 16:00-0D-48-10-64
get service
00001800-0000-1000-8000-00805f9b34fb //汎用情報
  get characteristics
     00002a00-0000-1000-8000-00805f9b34fb  Read, Write
     value 57-6F-48-61-6E-64 >> WoHand //デバイス名
     00002a01-0000-1000-8000-00805f9b34fb  Read
     value 00-00 >>      
     00002a04-0000-1000-8000-00805f9b34fb  Read
     value 06-00-18-00-00-00-90-01 >> 
00001801-0000-1000-8000-00805f9b34fb
  get characteristics
0000fee7-0000-1000-8000-00805f9b34fb // Custom UUID of Tencent Holdings Limited
  get characteristics
     0000fec8-0000-1000-8000-00805f9b34fb  Indicate // Custom UUID of Apple, Inc.
     0000fec7-0000-1000-8000-00805f9b34fb  Write    // Custom UUID of Apple, Inc.
     0000fec9-0000-1000-8000-00805f9b34fb  Read     // Custom UUID of Apple, Inc.
     value FD-CF-4B-A5-38-C4 >>   K 8 
cba20d00-224d-11e6-9fb8-0002a5d5c51b // ***目的のUUID***
  get characteristics
     cba20003-224d-11e6-9fb8-0002a5d5c51b  Notify
     cba20002-224d-11e6-9fb8-0002a5d5c51b  WriteWithoutResponse, Write

サービスは一種の名前空間みたいなもので、その中に個別のコマンド=キャラクタリスティックが入っていると理解。

コマンドを送る

private Guid switchUUID = new Guid("cba20d00-224d-11e6-9fb8-0002a5d5c51b");
        private Guid commandUUID = new Guid("cba20002-224d-11e6-9fb8-0002a5d5c51b");
        private async void Watcher_Received(BluetoothLEAdvertisementWatcher sender, BluetoothLEAdvertisementReceivedEventArgs args)
        {
            var targetMac = "*******"; // アプリで調べたMAC
            var mac = args.BluetoothAddress.ToString("x");
            var bleServiceUUID = args.Advertisement.ServiceUuids.FirstOrDefault();
            var name = args.Advertisement.LocalName;
            //アドレスじゃなくてUUIDで選択
            if (bleServiceUUID == switchUUID)
            {
                advWatcher.Stop();
                try
                {
                    //接続
                    BluetoothLEDevice device = await BluetoothLEDevice.FromBluetoothAddressAsync(args.BluetoothAddress);
                    //サービスUUIDを使って目的のサービスを取得
                    var gattService = await device.GetGattServicesForUuidAsync(switchUUID);
                    if (gattService.Status == GattCommunicationStatus.Success)
                    {
                        //コマンド送信用UUIDを使ってキャラクタリスティックを取得
                        var characteristics = await gattService.Services.FirstOrDefault().GetCharacteristicsForUuidAsync(commandUUID);
                        if (characteristics.Status == GattCommunicationStatus.Success)
                        {
                            var command = characteristics.Characteristics.FirstOrDefault();
                            if (command.CharacteristicProperties.HasFlag(GattCharacteristicProperties.Write))
                            {
                                byte[] comPress = { 0x57, 0x01, 0x00 };
                                var res = await command.WriteValueAsync(comPress.AsBuffer(), GattWriteOption.WriteWithResponse);
                                addLog(res.ToString());
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Exception...{ex.Message})");
                }
            }
        }

キュイッキュイッ
うごいたー!
複数のSwitchBotがあるとどれかがランダムで動きますw
実際には裏にマスタを持って指定するのがよいでしょう。

途中、コマンドを間違えたらSwitchBotがただの箱になって焦ったが、蓋を開けてリセットボタンを押すことで復旧。(電池のON/OFFでもいいらしい)

できてないこと

  • 長押し時間の切り替え
  • 「押す」「スイッチ」モードの切り替え
  • パスワード設定

アプリからは長押し時間を変更できるが、このコマンドが公開されていない?
「押す」と「スイッチ」の切り替え、パスワード設定も同様。
設定できそうなのは'cba20002-224d-11e6-9fb8-0002a5d5c51b'だけなので、ここに何らかのバイト列を送ればいいと思われる。

    if act == "Turn On":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570101')
    elif act == "Turn Off":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570102')
    elif act == "Press":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570100')
    elif act == "Down":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570103')
    elif act == "Up":
        con.sendline('char-write-cmd ' + cmd_handle + ' 570104')

公式のソースでは、5種類のコマンドがあるが、今の所呼べるのは「Press」のみ。。。

今後

C#から呼べるのは確定したので、HoloLensでバーチャルボタンを押したら現実のボタンが押される、みたいなことをしたい。
ホームオートメーション的なIoTデバイスが増えているものの、GoogleHome連携やAlexa連携しかインタフェースが無いものが大半なので、BLEで直接コントロールできるというのは嬉しいですね!
できれば他のコマンドも公開して欲しいところですが。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#]例外処理を理解しよう

1. 例外処理とは?

・ざっくり言うと想定内のエラーが発生した際にやる処理の事

2. 例外処理のメリット

・実行時に発生する問題に対応
・プログラムを安定して動作させる
・エラーメッセージを読み取れる

3. 例外処理(Exception)の機能

・try   あらかじめコードを指定して、プログラム実行時に処理の問題を検出
・catch   問題を検出した時、どのように対応するか記述しておく
・throw   プログラム実行中に、例外が発生した事を知らせる

4. 例外が発生する例

・ゼロで割り算
・数値変換で、数字でない文字を指定
・配列の範囲外にアクセス
・ファイルが存在しない

5. 実例

・簡単な例外処理をしてみよう

Lesson1.cs
// 簡単な例外処理をしてみよう
using System;

class Lesson1
{
    public static void Main()
    {
        // 例外が投げられる可能性のあるコード
        try
        {
            Console.WriteLine("Hello World");

            int number = 0;
            // 0で割り算できないためエラーが起きる
            // ここで処理が中断され、catchブロックに飛ぶ
            int answer = 100 / number;
            Console.WriteLine(answer);
        }
        // 例外が起きた場合の処理
        // Exception e の中に例外の詳細情報が格納されている
        catch(Exception e)
        {
            Console.WriteLine(e.Message);
        }
        // 例外発生の有無にかかわらず実行したいコード
        finally
        {
            Console.WriteLine("Hello C#");
        }
    }
}

・throwで意図的に例外を投げよう

Lesson2.cs
// throwで意図的に例外を投げよう
using System;

class Lesson2
{
    public static void Main()
    {
        Console.WriteLine("Hello World");

        try
        {
            int number = 2;
            int answer = 100 / number;
            Console.WriteLine(answer);
            // 意図的に例外を投げる
            throw new Exception();
        }
        catch (DivideByZeroException e)
        {
            Console.WriteLine("0では割り算できません");
            Console.Error.WriteLine(e);
        }
        // throw で呼ばれる
        catch (Exception e)
        {
            Console.WriteLine("例外が発生しました。");
            Console.Error.WriteLine(e);
        }
        finally
        {
            Console.WriteLine("Hello C#");
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『Unity C# 個人用メモ』 オブジェクトをクリックした座標へ移動させる(ナビゲーション不使用

クリックした位置の座標を取得し移動させる

RayRaycastHitを使用してオブジェクトをクリックした座標へ移動させる
Rayをちゃんと理解してないから推測込み('ω')難しいのよね

前提として

プレイ画面(スクリーン座標)は2Dなのでそれをゲーム上の3D空間(ワールド座標)へ変換する必要がある

素材

public class NewBehaviourScript : MonoBehaviour
{
    //レイを使用するための宣言
    private Ray ray;

    //レイがヒットした情報を格納するために使う
    private RaycastHit hit;

    //移動先の座標格納用
    private Vector3 pos;

    //移動用
    private float step;

本文

    void Update()
    {
        //クリックされた時
        if (Input.GetMouseButtonDown(0))
        {
            //レイのスタート位置
            ray = Camera.main.ScreenPointToRay(Input.mousePosition);

            //レイキャスト(スタート位置,方向 情報,距離)
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                //座標posへhitを変換
                pos = hit.point;
                pos.y = 1f;
            }
            //移動用
            step = 10f * Time.deltaTime;
        }
        //オブジェクトの移動
        transform.position = Vector3.MoveTowards(transform.position, pos, step);
    }
}

ray

そもそもray(レイ)ってなんぞやっていう疑問
特定の位置からセンサーを飛ばすって意味と推測してるけど実際どうなんだろう?

ray = Camera.main.ScreenPointToRay(Input.mousePosition);

rayを飛ばすスタート地点を宣言

Camera.main.ScreenPointToRay()
プレイ画面上のどこから飛ばすかを設定する
今回はマウスがクリックされた時のマウスの位置を取得しなければならないので
スタート地点はInput.mousePositionとなる

Raycast

if (Physics.Raycast(ray, out hit, Mathf.Infinity))
///Physics.Raycast(スタート地点, レイと飛ばす方向 レイが当たったオブジェクトの情報を格納, レイを飛ばす距離)

Physics.Raycast()はRaycastHitを使う際の決まり文句

rayはレイを飛ばすスタート地点のこと

outはどの方向にレイを飛ばすかということ
・補足:今回はoutなのでカメラの目線からout(奥側)となる(決まり文句でいいのかなと思う)

hitはレイキャストで飛ばしたレイが接触したオブジェクトの情報の格納先のこと
・補足:今回は変数hitに座標とか名前とか色々な情報が格納される

Mathf.Infinityは飛ばすレイの最大距離のこと{最大射程とか言った方が分かりやすい?}
・補足1:レイの飛ばす距離はfloat型であり、100f等とすれば範囲を制限できる
     (途方もなく遠い場所にあるオブジェクトに反応しないようにするため)
・補足2:逆に別に範囲を制限しないのであればMathf.Infinityと書けば距離は無限大になる

その後

pos = hit.point;
pos.y = 1f;

Vector3 poshit.point(hitの場所つまり座標)を変換して格納する
なおy軸は動いてほしくないのでオブジェクトの初期の設定値を入れる

step = 10f * Time.deltaTime;

これは移動速度の値10fにTime.deltaTimeを与えることでマシンスペックに関係なく処理速度を一定にする

Vector3.MoveTowards()

transform.position = Vector3.MoveTowards(transform.position, pos, step);
///transform.position = Vector3.MoveTowards(対象, 移動先, 速度);

Vector3.MoveTowards()で対象を移動先まで移動させることが出来る
今回は本体クリックされた座標へ設定された速度で移動させる。となる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

プログラムの条件式をシンプルにする2つだけの手法

条件式というのは何かと複雑になってくるものです。
1つ1つはシンプルなはず。なのになぜ?
そこを複数の視点から解明して実用的に使っていたら3個に集約されました。

プログラムの条件式をシンプルにする2つだけの手法

条件式は除外ケースから先に処理(書いて)いく

例えば、stringを正の整数にする関数でint型で返すとします。
仕様としては数字のみで、小数点「.」があれば切り捨てる。「-」その他の文字があれば仕様外ということで常に -1 を返す、とします。また0は許容(OK)とします。
また、nullは-1で、""(空文字)は0とします。

tryparse関数がありますが、あえてparse関数を利用するものとして、tryも利用しないこととします。

順序としては
1. nullならreturn -1
2. 0と1~9と「.」以外の文字があればreutrn -1
3. 「.」があれば、stringの最初の「.」以降の右側を除去する。
4. 先頭に0があれば除去する
5. ""ならreturn 0

上記1~5を行っておくことで、ここに処理が遷移してきた段階では数字のみで来ます。

例外エラーが生じない状態で、parse関数を使えますし、万が一にも例外エラーが出るケースがあれば、それに対応できます。

多くのケースではtry~catchでよいのですが、例外の内容によって細かく処理が分かれるようなケースでは
このように除外するケースを先に書いておく、というのがとても有用でした。

関数内の諸条件ではローカル関数もしくは匿名関数として処理する

VisualStudio2013の頃から、個人的には匿名関数が好きになってしまったのでご紹介します。
感覚としては関数の中に、その関数専用の特化チョイ関数を書く簡便さで使っています。

今では(C#7からは)ローカル関数としてシンプルに記述できます。(コメントで教えて頂きました)

匿名関数では
    Func<bool, string>is_correct_value = ( in_text) =>
    {
    };
だったのが、ローカル関数では
    bool is_correct_value(string in_text)
    {
    }
と、普通にかけます。
private int get_positive_integer(string in_int_text)
{
    Func<bool, string>is_correct_value = ( in_text) =>
    {
        if(in_text == null)
            return false;
        if(Regex.IsMatch("[^0-9.]", in_text)
            return false;
        return true;
    };
    if(is_corrct_value(in_int_text)==false)
        return -1;

    string tmp_text = in_int_text;
    if(tmp_text.Contains(".")==true)
        tmp_int_text = tmp_int_text.Substring(0, tmp_int_text.IndexOf('.'));
    if(tmp_int_text == "")
        return 0;

    int ret_value = int.Parse(tmp_int);
    return ret_value;
}

のような感じです。

ローカル関数なら IEnumerable内でも yield return を使える

yieldのおかげで複雑な処理を複数段に分けて、簡潔な処理記述にできるのですが、
匿名関数のときは IEnumerable の中で匿名関数が使えずに「仕方ないな」と思っていました。
ですが、ローカル関数ではいけるのでもっと早くにあればよかった、という感じです。
(匿名関数が使えなくても専属のprivate関数を書いた程度の違いではありますが、数ヶ月後の可読性というか
思い出しやすさが違います(汗)

ちなにみyieldは個人的には処理手順の段階化(レイヤーを分けられる)・単純化に有用だなと心底思っています。この機能のおかげでオフコン世代のインタープリタ記述のものをプリンタの独自命令にまで変遷させていく、というプログラムを書くことができました。(次の人に説明しやすい構成(のはず・・・)で書けました)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#の演習問題:しまづ君にプログラミングを教えよう!【Unityゲームスタジオ スタジオしまづ】

(ベータ版)

これはあなた自身の物語です

ときは戦国、、、
ゲーム開発をするためにプログラミングを学び始めたしまづ君(永遠の3才)。
ところが彼はまだまだプログラミングのことがわかっていません。
のちに世界を救うこととなる「スタジオしまづ」設立のために彼にプログラミングを教えましょう!

演習問題を行う前の準備:準備中

演習問題1:変数の宣言方法がわかっている?

しまづくん:コードを書いていたらこんなエラーが出てしまったよ。。。
どうすればいいかな?
スクリーンショット 2020-02-26 9.19.55.png

しまづ君のコード

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    myStadio = "スタジオしまづ";
    void Start()
    {
        Debug.Log(myStadio);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    string myStadio = "スタジオしまづ";
    void Start()
    {
        Debug.Log(myStadio);
    }
}

演習問題2:変数の種類(データ型)がわかっているか

しまづくん:僕のプロフィールをUnityさんに教えようとしたら、こんなエラーが出てしまったよ。。。
どうすればいいかな?
スクリーンショット 2020-02-26 9.40.19.png

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 名前:スタジオしまづ
    // 身長:163.0cm
    // 体重:44.5kg
    // 誕生日:5月7日
    bool myName = "スタジオしまづ";
    float height = 45.5;
    int weight = 45.5;
    string monthOfbirth = 5;
    Vector3 dayOfbirth = 7;

    void Start()
    {
        Debug.Log("名前:" + myName);
        Debug.Log("身長:" + height);
        Debug.Log("体重:" + weight);
        Debug.Log("誕生日:" + monthOfbirth+"月"+ dayOfbirth + "日");
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 名前:スタジオしまづ
    // 身長:163.0cm
    // 体重:44.5kg
    // 誕生日:5月7日
    string myName = "スタジオしまづ";
    float height = 45.5f;
    float weight = 45.5f;
    int monthOfbirth = 5;
    int dayOfbirth = 7;

    void Start()
    {
        Debug.Log("名前:" + myName);
        Debug.Log("身長:" + height);
        Debug.Log("体重:" + weight);
        Debug.Log("誕生日:" + monthOfbirth+"月"+ dayOfbirth + "日");
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#&Unityの演習問題:しまづ君にプログラミングを教えよう!【スタジオしまづアカデミー】

(ベータ版:いいねと思ったらtwitterに投稿してもらえると嬉しいです^^)
(おすすめの問題があれば、編集リクエストで追加していただけると助かります)

これはあなた自身の物語です

ときは戦国、、、
ゲーム開発をするためにプログラミングを学び始めたしまづ君(永遠の3才)。
ところが彼はまだまだプログラミングのことがわかっていません。
のちに世界を救うこととなる「スタジオしまづ」設立のために彼にプログラミングを教えましょう!

演習問題を行う前の準備:準備中

スタジオしまづのYouTube
スタジオしまづの学習サイト

演習問題1:変数の宣言方法がわかっている?

しまづくん:コードを書いていたらこんなエラーが出てしまったよ。。。
どうすればいいかな?
スクリーンショット 2020-02-26 9.19.55.png

しまづ君のコード

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    myStadio = "スタジオしまづ";
    void Start()
    {
        Debug.Log(myStadio);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    string myStadio = "スタジオしまづ";
    void Start()
    {
        Debug.Log(myStadio);
    }
}

しまづ君:ありがとう!変数の宣言方法がわかったよ^^

演習問題2:変数の種類(データ型)がわかっているか

しまづくん:僕のプロフィールをUnityさんに教えようとしたら、こんなエラーが出てしまったよ。。。
どうすればいいかな?
スクリーンショット 2020-02-26 9.40.19.png

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 名前:スタジオしまづ
    // 身長:163.0cm
    // 体重:44.5kg
    // 誕生日:5月7日
    bool myName = "スタジオしまづ";
    float height = 45.5;
    int weight = 45.5;
    string monthOfbirth = 5;
    Vector3 dayOfbirth = 7;

    void Start()
    {
        Debug.Log("名前:" + myName);
        Debug.Log("身長:" + height);
        Debug.Log("体重:" + weight);
        Debug.Log("誕生日:" + monthOfbirth+"月"+ dayOfbirth + "日");
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 名前:スタジオしまづ
    // 身長:163.0cm
    // 体重:44.5kg
    // 誕生日:5月7日
    string myName = "スタジオしまづ";
    float height = 45.5f;
    float weight = 45.5f;
    int monthOfbirth = 5;
    int dayOfbirth = 7;

    void Start()
    {
        Debug.Log("名前:" + myName);
        Debug.Log("身長:" + height);
        Debug.Log("体重:" + weight);
        Debug.Log("誕生日:" + monthOfbirth+"月"+ dayOfbirth + "日");
    }
}

しまづ君:ありがとう!これで誕生日プレゼントがみんなからたくさんもらえるぞ^^

演習問題3:変数の四則演算子がわかっているか

しまづくん:123かける456割る2を計算してコンソールに表示したいんだけどどうかけばいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 123かける456割る2の計算結果をコンソールに表示したい
    void Start()
    {
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    // 123かける456割る2の計算結果をコンソールに表示したい
    void Start()
    {
        Debug.Log(123*456/2); // 28044
    }
}

しまづ君:ありがとう!算数も勝手にやってくれるんだね^^

演習問題4:変数の四則演算子がわかっているか

しまづくん:1+2は3でしょ?そこに5をかけると15じゃん!でも11が出るんだけど、、、
どうすれば15になるかなぁ?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 1;
    int y = 2;
    int z = 5;
    // 1+2をおこなってから5をかけて、コンソールに15を表示したい
    void Start()
    {
        Debug.Log(x+y*z);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 1;
    int y = 2;
    int z = 5;
    // 1+2をおこなってから5をかけて、コンソールに15を表示したい
    void Start()
    {
        Debug.Log((x+y)*z);
    }
}

しまづ君:教えてくれてありがとう!掛け算が先に計算されちゃうんだ^^

演習問題5:変数の四則演算子がわかっているか

しまづくん:食事会の費用19800円を7人で割り勘することになったんだけど、割り切れなかった分はパパが払ってくれるんだ。
1人あたり何円集めて、いくらパパから貰えばいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 19800;
    int n = 7;
    // x,y,zを3で割ったあまりをそれぞれ表示したい
    void Start()
    {
        int y = 0; // ここに計算をかく
        int z = 0; // ここに計算をかく
        Debug.Log("1人あたりからもらうお金:"+ y);
        Debug.Log("パパからもらうお金:"+z);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 19800;
    int n = 7;
    // x,y,zを3で割ったあまりをそれぞれ表示したい
    void Start()
    {
        int y = x / n; // ここに計算をかく
        int z = x % n; // ここに計算をかく
        Debug.Log("1人あたりからもらうお金:"+ y);
        Debug.Log("パパからもらうお金:"+z);
    }
}

しまづ君:教えてくれてありがとう!パパのところに行ってこよ^^

演習問題6:変数の演算子がわかっているか

しまづくん:1つの変数だけで、数字を1づつ増やして表示したいんだけどどうすればいいかな?
できれば++とか+=とかを使いたい(> <)

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 0;
    void Start()
    {
        // 以下をxのみを使って行いたい(++, += を使ってやってみて)
        Debug.Log(x);
        int x1 = x + 1;
        Debug.Log(x1);
        int x2 = x1 + 1;
        Debug.Log(x2);
        int x3 = x2 + 1;
        Debug.Log(x3);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int x = 0;
    void Start()
    {
        // 以下をxのみを使って行いたい(++, += を使ってやってみて)
        Debug.Log(x);
        x = x + 1;
        Debug.Log(x);
        x += 1;
        Debug.Log(x);
        x ++;
        Debug.Log(x);
    }
}

しまづ君:教えてくれてありがとう!なんかプログラマになれた気分^^

演習問題7:比較演算子(イコール)がわかっているか

しまづくん:10と2が同じ数字か調べてみたら、2が返ってきたんだよ!!!!
どこが違うと思う?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 10;
    int m = 2;
    void Start()
    {
        Debug.Log(n = m);
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 10;
    int m = 2;
    void Start()
    {
        Debug.Log(n == m);
    }
}

しまづ君:教えてくれてありがとう!プログラムの=は数学の=と違うんだね^^

演習問題8:比較演算子(否定)がわかっているか

しまづくん:10と2が違う数字か調べてみたら、エラーが出ちゃった!!!!
どこが違うと思う?

スクリーンショット 2020-02-27 11.14.32.png

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 10;
    int m = 2;
    void Start()
    {
        Debug.Log(n =! m);
    }
}


---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 10;
    int m = 2;
    void Start()
    {
        Debug.Log(n != m);
    }
}

しまづ君:教えてくれてありがとう! 「!」の位置に決まりがあるんだね^^

演習問題9:ifがわかっているか

しまづくん:HPが0になったら「戦闘不能」と表示したかったんだけど、ずっと出ちゃってる。。。
どうすればいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int hp = 100;
    void Start()
    {
        if (hp >= 0)
        {
            Debug.Log("戦闘不能");
        }
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int hp = 100;
    void Start()
    {
        if (hp <= 0)
        {
            Debug.Log("戦闘不能");
        }
    }
}

しまづ君:教えてくれてありがとう! この辺りよくわ違えちゃうかも><

演習問題10:if elseがわかっているか

しまづくん:HPが0になったら「戦闘不能」、それ以外のときは「しまちゅー元気でちゅー」と表示したかったんだけど、ずっと元気なんだw
どうすればいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int hp = 100;
    void Start()
    {
        if (hp <= 0)
        {
            Debug.Log("戦闘不能");
        }
        Debug.Log("しまちゅー元気でちゅー");
    }
}


---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int hp = 100;
    void Start()
    {
        if (hp <= 0)
        {
            Debug.Log("戦闘不能");
        }
        else
        {
            Debug.Log("しまちゅー元気でちゅー");
        }
    }
}

しまづ君:教えてくれてありがとう! elseを使えばいいんだね

演習問題11:if-elseifがわかっているか

しまづくん:x>0なら「右に動く」、x<0なら「左に動く」、それ以外なら「そのまま」っと表示したいんだけど、「左に動く」と「そのまま」が出ちゃうんだ。。。
どうすればいい?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    //int x = 1; // x > 0のときのテスト用
    //int x = 0; // xが0のときのテスト用
    int x = -1;  // x < 0のときのテスト用
    void Start()
    {
        if (x > 0)
        {
            Debug.Log("右に動く");
        }
        else
        {
            Debug.Log("左に動く");
        }
        Debug.Log("そのまま");
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    //int x = 1; // x > 0のときのテスト用
    //int x = 0; // xが0のときのテスト用
    int x = -1;  // x < 0のときのテスト用
    void Start()
    {
        if (x > 0)
        {
            Debug.Log("右に動く");
        }
        else if(x < 0)
        {
            Debug.Log("左に動く");
        }
        else
        {
            Debug.Log("そのまま");
        }
    }
}

しまづ君:教えてくれてありがとう! else ifを使えば色々分けられるんだ^^

演習問題12:switchがわかっているか

しまづくん:以下のようなプログラムを作ろうとしてるんだけど、、、
・directionが1ならspeedは1.1
・directionが0ならspeedは0
・directionが-1ならspeedは-1.1
ネットで見つけた記事からここまでは書けたんだけど、あとどうすればいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int direction = 1;   // direction > 0のときのテスト用
    //int direction = 0;   // directionが0のときのテスト用
    //int direction = -1;   // direction < 0のときのテスト用
    float speed;

    void Start()
    {
        switch (direction)
        {
            case 100:
                speed = 22f;
                break;
            case -10:
                speed = -22f;
                break;
        }
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int direction = 1;   // direction > 0のときのテスト用
    //int direction = 0;   // directionが0のときのテスト用
    //int direction = -1;   // direction < 0のときのテスト用
    float speed;

    void Start()
    {
        switch (direction)
        {
            case 1:
                speed = 1.1f;
                break;
            case 0:
                speed = 0f;
                break;
            case -1:
                speed = 1.1f;
                break;
        }
    }
}

しまづ君:教えてくれてありがとう! if文でも出来そうだなぁ

演習問題13:whileがわかっているか

しまづくん:10から1000までの数字を表示したいんだけど、なんとか14までは表示出来たんだ!偉いでしょ^^
ネットで調べたらwhileが使えるって書いてあったんだけど、どうすればいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int count = 10;

    void Start()
    {
        Debug.Log(count);
        count = count + 1;
        Debug.Log(count);
        count = count + 1;
        Debug.Log(count);
        count = count + 1;
        Debug.Log(count);
        count = count + 1;
        Debug.Log(count);
        count = count + 1;
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int count = 10;
    void Start()
    {
        while (count <= 1000)
        {
            Debug.Log(count);
            count = count + 1;
        }
    }
}

しまづくん:ありがとう!whileの使い方がわかったよ^^

演習問題14:whileがわかっているか

しまづくん:1000から2000までの数字を表示し続けて、273の倍数があったらそこで処理止めたいってときはどうすればいいかな?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 1000;

    void Start()
    {
        while (n <= 2000)
        {
            Debug.Log(n);
            n++;
        }
    }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int n = 1000;

    void Start()
    {
        while (n <= 2000)
        {
            Debug.Log(n);
            if (n % 273 == 0)
            {
                break;
            }
            n++;
        }
    }
}

しまづくん:ありがとう!whileとbreakの使い方がわかったよ^^

演習問題15:forがわかっているか

しまづくん:前の問題「10から1000までの数字を表示」はfor文でもできるって聞いたんだけどどうやったら出来るの?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    int count = 10;

    void Start()
    {
        while (count <= 1000) {
            Debug.Log(count);
            count = count + 1;
        }
   }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    void Start()
    {
        for (int i=10; i<=1000; i++)
        {
            Debug.Log(i);
        }
   }
}

しまづくん:すごーい!for文の使い方がわかったよ^^

演習問題16:forとbreakがわかっているか

しまづくん:前の問題「1000から2000までの数字を表示し続けて、273の倍数があったらそこで処理止めたい」もfor文ならどうなるの?

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    void Start()
    {
        for (int i=10; i<=1000; i++)
        {
            Debug.Log(i);
        }
   }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    void Start()
    {
        for (int i = 1000; i <= 2000; i++)
        {
            Debug.Log(i);
            if ( i%273 == 0)
            {
                break;
            }
        }
    }
}

しまづくん:すごーい!breakの使い方がわかったよ^^

演習問題17:forとcontinueがわかっているか

しまづくん:0から99までの数字を表示したい。だけど、3の倍数は表示したくない場合、for文ならどうかけるの?
continueを使いたいなぁ

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    void Start()
    {
   }
}

---解答ルパン---

Question.cs
using UnityEngine;

public class Question : MonoBehaviour
{
    void Start()
    {
        for (int i = 0; i < 100; i++)
        {
            if ( i%3 == 0)
            {
                continue;
            }
            Debug.Log(i);
        }
    }
}

しまづくん:すごーい!continueの使い方がわかったよ^^

スタジオしまづのYouTube
スタジオしまづの学習サイト

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Calendar API

Google Calendar API をちょろっとだけ触ってみたくなった

いろんな分野に目移りしてちょろっと触ってみたくなる悪い癖なんですが
クラウドソーシングの副業を探していたら、Gooogle Calendarに予定を突っ込む/更新する案件があり
なぜかそれきっかけで無性にAPIを呼んでみたくなったので、試してみました。
※ちなみに案件は受けていませんw

Google Calendar API 入門

公式に入門がありました。
Get Started with the Calendar API

どうやらAPIを使用するには、認証情報を作成する必要があるようです。
以下3つの方法があるとのことです。
image.png

OAuthクライアントIDを用いる方法では、すぐに認証情報が作成できるのですが、「oauth 同意画面」の設定を行わないとプライベートデータへのアクセスが100回までと制限されるそうで
image.png
また、「oauth 同意画面」の設定が手間そうだったので、「サービス アカウント」を用いる方法を試してみたいと思います。

サービスアカウントの作成

以下に入力を行い作成。
image.png

出来上がったサービスアカウントのリンクをクリック
image.png

キーを作成をクリック
image.png

JSONを選択
image.png

ここでダウンロードしたJsonの中身の情報を用いてGoogleAPIを呼び出します。

クライアントライブラリ使用して予定を取得

Nugetからカレンダー用のDLLをダウンロードしてきます。
image.png

次に作成したサービスアカウントに操作させたいカレンダーを公開します。
対象のカレンダーの設定から特定のユーザーとの共有にて追加します。
image.png
メールアドレスのところに、サービスアカウントのメールアドレスを入れます。
※ダウンロードしたJSONのclient_emailにもあります。

サンプルコードの実装
以下.Netのサンプルが公式にありましたので、そちらを参考にしてみます。
.Net Sample

とりあえずこぴって、環境に依存するところは変更。
また、サービスアカウントで認証する必要があるので以下を参考にして、credentialを作成するところは書き換えた。
Google Calendar API ServiceAccountCredential

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading;

namespace GoogleAPITest
{
    public class Program
    {
        // If modifying these scopes, delete your previously saved credentials
        // at ~/.credentials/calendar-dotnet-quickstart.json
        private static string[] Scopes = { CalendarService.Scope.CalendarReadonly };
        private static string ApplicationName = "Google Calendar API .NET Quickstart";

        public static void Main(string[] args)
        {
            var jObject = JObject.Parse(File.ReadAllText(
                @"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json"));
            var serviceAccountEmail = jObject["client_email"].ToString();
            var certificate = jObject["private_key"].ToString();

            var credential = new ServiceAccountCredential(
            new ServiceAccountCredential.Initializer(serviceAccountEmail)
            {
                Scopes = new[] { CalendarService.Scope.Calendar }
            }.FromPrivateKey(certificate));

            // Create Google Calendar API service.
            var service = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });

            // おそらく不必要
            // Define parameters of request.
            //var request = service.Events.List("primary");
            //request.TimeMin = DateTime.Now;
            //request.ShowDeleted = false;
            //request.SingleEvents = true;
            //request.MaxResults = 10;
            //request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // ここで第2引数にサービスアカウントに公開したカレンダーIDを指定する
            var request = new EventsResource.ListRequest(service, "公開したカレンダーのカレンダーID");

            // List events.
            var events = request.Execute();
            Console.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    string when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }

                    Console.WriteLine("{0} start:({1}) end:({2})", eventItem.Summary, when, eventItem.End.DateTime.ToString());
                }
            }
            else
            {
                Console.WriteLine("No upcoming events found.");
            }

            Console.Read();
        }
    }
}

公開したカレンダーの直近
image.png

実行結果
image.png

正常に取得できているようです。(昔のイベントは無視で・・・)

今回はここまでにします。
次回はカレンダー更新をやってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Calendar API #1

Google Calendar API をちょろっとだけ触ってみたくなった

いろんな分野に目移りしてちょろっと触ってみたくなる悪い癖なんですが
クラウドソーシングの副業を探していたら、Gooogle Calendarに予定を突っ込む/更新する案件があり
なぜかそれきっかけで無性にAPIを呼んでみたくなったので、試してみました。
※ちなみに案件は受けていませんw

環境

IDE:VisualStudio2019
アプリケーション:コンソールアプリ
フレームワーク:.NET Core 3.1

Google Calendar API 入門

公式に入門がありました。
Get Started with the Calendar API

どうやらAPIを使用するには、認証情報を作成する必要があるようです。
以下3つの方法があるとのことです。
image.png

OAuthクライアントIDを用いる方法では、すぐに認証情報が作成できるのですが、「oauth 同意画面」の設定を行わないとプライベートデータへのアクセスが100回までと制限されるそうで
image.png
また、「oauth 同意画面」の設定が手間そうだったので、「サービス アカウント」を用いる方法を試してみたいと思います。

サービスアカウントの作成

以下に入力を行い作成。
image.png

出来上がったサービスアカウントのリンクをクリック
image.png

キーを作成をクリック
image.png

JSONを選択
image.png

ここでダウンロードしたJsonの中身の情報を用いてGoogleAPIを呼び出します。

クライアントライブラリ使用して予定を取得

Nugetからカレンダー用のDLLをダウンロードしてきます。
image.png

次に作成したサービスアカウントに操作させたいカレンダーを公開します。
対象のカレンダーの設定から特定のユーザーとの共有にて追加します。
image.png
メールアドレスのところに、サービスアカウントのメールアドレスを入れます。
※ダウンロードしたJSONのclient_emailにもあります。

サンプルコードの実装
以下.Netのサンプルが公式にありましたので、そちらを参考にしてみます。
.Net Sample

とりあえずこぴって、環境に依存するところは変更。
また、サービスアカウントで認証する必要があるので以下を参考にして、credentialを作成するところは書き換えた。
Google Calendar API ServiceAccountCredential

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading;

namespace GoogleAPITest
{
    public class Program
    {
        // If modifying these scopes, delete your previously saved credentials
        // at ~/.credentials/calendar-dotnet-quickstart.json
        private static string[] Scopes = { CalendarService.Scope.CalendarReadonly };
        private static string ApplicationName = "Google Calendar API .NET Quickstart";

        public static void Main(string[] args)
        {
            var jObject = JObject.Parse(File.ReadAllText(
                @"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json"));
            var serviceAccountEmail = jObject["client_email"].ToString();
            var certificate = jObject["private_key"].ToString();

            var credential = new ServiceAccountCredential(
            new ServiceAccountCredential.Initializer(serviceAccountEmail)
            {
                Scopes = new[] { CalendarService.Scope.Calendar }
            }.FromPrivateKey(certificate));

            // Create Google Calendar API service.
            var service = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });

            // おそらく不必要
            // Define parameters of request.
            //var request = service.Events.List("primary");
            //request.TimeMin = DateTime.Now;
            //request.ShowDeleted = false;
            //request.SingleEvents = true;
            //request.MaxResults = 10;
            //request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // ここで第2引数にサービスアカウントに公開したカレンダーIDを指定する
            var request = new EventsResource.ListRequest(service, "公開したカレンダーのカレンダーID");

            // List events.
            var events = request.Execute();
            Console.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    string when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }

                    Console.WriteLine("{0} start:({1}) end:({2})", eventItem.Summary, when, eventItem.End.DateTime.ToString());
                }
            }
            else
            {
                Console.WriteLine("No upcoming events found.");
            }

            Console.Read();
        }
    }
}

公開したカレンダーの直近
image.png

実行結果
image.png

正常に取得できているようです。(昔のイベントは無視で・・・)

今回はここまでにします。
次回はカレンダー更新をやってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Calendar APIを使用してみる #1

Google Calendar API をちょろっとだけ触ってみたくなった

いろんな分野に目移りしてちょろっと触ってみたくなる悪い癖なんですが
クラウドソーシングの副業を探していたら、Gooogle Calendarに予定を突っ込む/更新する案件があり
なぜかそれきっかけで無性にAPIを呼んでみたくなったので、試してみました。
※ちなみに案件は受けていませんw

シリーズ

Google Calendar APIを使用してみる #2
Google Calendar APIを使用してみる #3

環境

IDE:VisualStudio2019
アプリケーション:コンソールアプリ
フレームワーク:.NET Core 3.1

Google Calendar API 入門

公式に入門がありました。
Get Started with the Calendar API

どうやらAPIを使用するには、認証情報を作成する必要があるようです。
以下3つの方法があるとのことです。
image.png

OAuthクライアントIDを用いる方法では、すぐに認証情報が作成できるのですが、「oauth 同意画面」の設定を行わないとプライベートデータへのアクセスが100回までと制限されるそうで
image.png
また、「oauth 同意画面」の設定が手間そうだったので、「サービス アカウント」を用いる方法を試してみたいと思います。

サービスアカウントの作成

以下に入力を行い作成。
image.png

出来上がったサービスアカウントのリンクをクリック
image.png

キーを作成をクリック
image.png

JSONを選択
image.png

ここでダウンロードしたJsonの中身の情報を用いてGoogleAPIを呼び出します。

クライアントライブラリ使用して予定を取得

Nugetからカレンダー用のDLLをダウンロードしてきます。
image.png

次に作成したサービスアカウントに操作させたいカレンダーを公開します。
対象のカレンダーの設定から特定のユーザーとの共有にて追加します。
image.png
メールアドレスのところに、サービスアカウントのメールアドレスを入れます。
※ダウンロードしたJSONのclient_emailにもあります。

サンプルコードの実装
以下.Netのサンプルが公式にありましたので、そちらを参考にしてみます。
.Net Sample

とりあえずこぴって、環境に依存するところは変更。
また、サービスアカウントで認証する必要があるので以下を参考にして、credentialを作成するところは書き換えた。
Google Calendar API ServiceAccountCredential

using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Threading;

namespace GoogleAPITest
{
    public class Program
    {
        // If modifying these scopes, delete your previously saved credentials
        // at ~/.credentials/calendar-dotnet-quickstart.json
        private static string[] Scopes = { CalendarService.Scope.CalendarReadonly };
        private static string ApplicationName = "Google Calendar API .NET Quickstart";

        public static void Main(string[] args)
        {
            var jObject = JObject.Parse(File.ReadAllText(
                @"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json"));
            var serviceAccountEmail = jObject["client_email"].ToString();
            var certificate = jObject["private_key"].ToString();

            var credential = new ServiceAccountCredential(
            new ServiceAccountCredential.Initializer(serviceAccountEmail)
            {
                Scopes = new[] { CalendarService.Scope.Calendar }
            }.FromPrivateKey(certificate));

            // Create Google Calendar API service.
            var service = new CalendarService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = ApplicationName,
            });

            // おそらく不必要
            // Define parameters of request.
            //var request = service.Events.List("primary");
            //request.TimeMin = DateTime.Now;
            //request.ShowDeleted = false;
            //request.SingleEvents = true;
            //request.MaxResults = 10;
            //request.OrderBy = EventsResource.ListRequest.OrderByEnum.StartTime;

            // ここで第2引数にサービスアカウントに公開したカレンダーIDを指定する
            var request = new EventsResource.ListRequest(service, "公開したカレンダーのカレンダーID");

            // List events.
            var events = request.Execute();
            Console.WriteLine("Upcoming events:");
            if (events.Items != null && events.Items.Count > 0)
            {
                foreach (var eventItem in events.Items)
                {
                    string when = eventItem.Start.DateTime.ToString();
                    if (String.IsNullOrEmpty(when))
                    {
                        when = eventItem.Start.Date;
                    }

                    Console.WriteLine("{0} start:({1}) end:({2})", eventItem.Summary, when, eventItem.End.DateTime.ToString());
                }
            }
            else
            {
                Console.WriteLine("No upcoming events found.");
            }

            Console.Read();
        }
    }
}

公開したカレンダーの直近
image.png

実行結果
image.png

正常に取得できているようです。(昔のイベントは無視で・・・)

今回はここまでにします。
次回はカレンダー更新をやってみようと思います。
次回

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む