20200228のC#に関する記事は9件です。

C#-SQLite データベース データアクセスクラスを作成しました。

この記事では、Visual Studio 2019 を用いてアプリ開発において、データベースに SQLite を使用する場合に必要になるであろう、アクセス用ラッパークラスを作成しました。

環境

Windows 10 Pro
Visual Studio 2019 Comunity

対象読者

  • C# で SQLite を使おうとしている方
  • C# を勉強している方

基本的な設計

他のDBにも同じメソッドで接続できるように、ジェネリック型で定義したインタフェースを使用しました。

コード

インタフェース

IDatabaseConnectors.cs
using System;
using System.Collections.Generic;

namespace FormApp1.DatabaseConnectors
{
    interface IDatabaseConnectors<Reader> : IDisposable
    {
        // SQL発行関数(SELECT)
        Reader ExecuteQuery(string query);
        Reader ExecuteQuery(string query, Dictionary<string, object> keyValuePairs);
        // SQL発行関数(INSERT, UPDATE, DELETE)
        void ExecuteNonQuery(string query);
        void ExecuteNonQuery(string query, Dictionary<string, object> keyValuePairs);
        // 初期化
        void Initialize();
        // コネクションストリングの構築(DB毎に異なるので)
        void BuildSqlConnectionString();
        void Disconnect();
        // トランザクション管理
        void BeginTransaction();
        void CommitTransaction();
        void RollBack();
    }
}

IDatabaseConnectorsのポイント

  • 他のRDBMSでも使えることに配慮しています。
  • BuildSQlConnectionString() でRDBMS毎に異なるコネクションストリングをクラスで実装します。

クラス実装

SqliteDatabaseConnector.cs
using System;
using System.Data.SQLite;
using System.Collections.Generic;

namespace FormApp1.DatabaseConnectors
{
    public class SqliteDatabaseConnector : IDatabaseConnectors<SQLiteDataReader>, IDisposable
    {
        // Nuget で SQLite 関連は読み込み済み
        private SQLiteConnection _SQLiteConnection;
        private SQLiteTransaction _SQLiteTransaction;
        private string _dbConnectionString = "";
        // SQLiteDB ファイルのパス
        private readonly string _path;
        // SQLiteDB ファイルの名称
        private readonly string _fileName;

        // コンストラクタ
        public SqliteDatabaseConnector(string path, string fileName)
        {
            _path = path;
            _fileName = fileName;
            Initialize();
        }
        // 初期化:DB接続を確立させる
        public void Initialize()
        {
            BuildSqlConnectionString();
            _SQLiteConnection = new SQLiteConnection(_dbConnectionString);
            _SQLiteConnection.Open();
        }
        // コネクションストリングを作成
        public void BuildSqlConnectionString()
        {
            // DB ファイルネームが指定されなかった場合は、"default" を定義値にします。
            string fName = (_fileName.Length == 0) ? "default" : _fileName;
            if(_path.Length == 0)
                _dbConnectionString = $"Data Source ={fName}.db";
            else
                _dbConnectionString = $"Data Source ={_path}/{fName}.db";

        }
        public void Dispose()
        {
            this.Disconnect();
            this._SQLiteConnection.Dispose();
            this._SQLiteTransaction.Dispose();
        }
        public void Disconnect()
        {
            _SQLiteConnection.Close();
        }


        public void BeginTransaction()
        {
            this._SQLiteTransaction = this._SQLiteConnection.BeginTransaction();
        }
        public void CommitTransaction()
        {
            if (this._SQLiteTransaction.Connection != null)
            {
                this._SQLiteTransaction.Commit();
                this.Dispose();
            }
        }
        public void RollBack()
        {
            if (this._SQLiteTransaction.Connection != null)
            {
                this._SQLiteTransaction.Rollback();
                this.Dispose();
            }
        }

        // SQL発行
        public void ExecuteNonQuery(string query)
        {
            this.ExecuteNonQuery(query, new Dictionary<string, object>());
        }
        public void ExecuteNonQuery(string query, Dictionary<string, object> keyValuePairs)
        {
            using(var cmd = new SQLiteCommand())
            {
                cmd.Connection = _SQLiteConnection;
                cmd.Transaction = _SQLiteTransaction;
                // パラメータをバインド
                foreach (KeyValuePair<string, object> item in keyValuePairs)
                {
                    if ( query.IndexOf(item.Key) > 0 ) 
                        cmd.Parameters.Add(new SQLiteParameter(item.Key, item.Value));
                }
                cmd.CommandText = query;
                cmd.ExecuteNonQuery();
            }
        }
        public SQLiteDataReader ExecuteQuery(string query)
        {
            return this.ExecuteQuery(query, new Dictionary<string, object>());
        }
        public SQLiteDataReader ExecuteQuery(string query, Dictionary<string, object> keyValuePairs)
        {
            SQLiteDataReader reader;
            using (var cmd = new SQLiteCommand())
            {
                cmd.Connection = this._SQLiteConnection;
                cmd.Transaction = this._SQLiteTransaction;
                // パラメータをバインド
                foreach (KeyValuePair<string, object> item in keyValuePairs)
                {
                    if (query.IndexOf(item.Key) > 0)
                        cmd.Parameters.Add(new SQLiteParameter(item.Key, item.Value));
                }
                cmd.CommandText = query;
                reader = cmd.ExecuteReader();
            }
            return reader;
        }

    }
}

SqliteDatabaseConnectorのポイント

  • クエリのバインド文字列を Dictionary 形式で渡して、汎用性を持たせようとしています。

今後

  • Entity-Model を作成し、SqliteDatabaseConnectorを経由してDB登録します。
  • 別途、Column情報などを定義するクラスを書けば、汎用性が増すと思うので改良します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

一覧画面へのページング機能追加(MVC)・・・ASP.NET Core開発ノウハウ 4-2

今回のテーマ・課題

EntityFrameworkを利用したASP.NET Core MVCアプリケーションにおいてデータの一覧表示画面にページング機能を実装する。

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

  1. 一覧表示画面に表示する任意のモデルにページング機能を実装するリストクラスを作成して配置する。
    このリストクラスにはページングを実行可能にするために以下のようなプロパティーを持たせる。

    • 1ページ当たりの表示レコード数。
    • 全体のページ数
    • 現在のページインデックス
    • 現在のページインデックスの前方にまだ表示可能なページが存在するか?
    • 現在のページインデックスの後方にまだ表示可能なページが存在するか?
    • 母集団となる再絞り込み可能なモデルリストに対して与えられたページインデックスのレコードを取り出すためのメソッド。

    この拡張リストを使用してカレントページに表示するレコードを取り出す静的メソッドを用意する。

    このメソッドに引き渡される母集団となるデータリストはページング処理により再クエリーされるため、IQuerableインターフェイスを実装する必要がある。

  2. View側にはページングされたモデルのリストを表示するための以下のようなコントロールが必要になる。

  • トータルページ数と現在のページインデックスを表示するラベル。
  • 前方・後方にページをめくるボタン あるいは・・・
  • 「1, 2, 3, 4, ・・・」のように先頭からのページインデックスを(それらのページへの)リンクタグ付きで示すリスト。

ポイントとなるのはこれらの<ページング用のビューコントロールが元の一覧表示画面の検索機能や並べ替え機能に最小限の影響しか与えないようにすることである。

【説明の進め方】

このドキュメントの前提として、既にアプリケーションには一覧表示画面(Index.cshtml)が用意されており、そこには検索機能とソート機能が実装されているものとする。
そのサンプルとして「検索+ソート機能付き一覧画面(MVC)・・・ASP.NET Core開発ノウハウ 4-2」にて実装したPersonモデルの検索・ソート条件付き一覧表示画面にページング機能を実装する手順を示していく。(https://qiita.com/TR-MF/items/e5ba963be872ecdfe928)


2. ページング実装拡張リストクラス

プロジェクト内の\Commonまたは\Utility等の任意のフォルダに拡張リスト用クラス「PaginatingList<T>」クラスを作成する。

\Common\PaginatedList.cs

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

namespace ANCEntry_EFMvcApp.Common
{

    /// <summary>
    /// 一覧表示用UIに任意のモデルデータを表示する際にPaging機能を与えるList
    /// </summary>
    /// <typeparam name="T">UIに一覧表示するデータのモデルクラス</typeparam>
    public class PaginatedList<T>: List<T>
    {
        /// <summary>
        /// 現在のページのインデックス
        /// </summary>
        public int PageIndex { get; private set; }

        /// <summary>
        /// トータルページ数
        /// </summary>
        public int TotalPages { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="items">当該ページに表示するレコードリスト</param>
        /// <param name="count">元々リストに含まれていた総レコード数</param>
        /// <param name="pageIndex">現在表示するページのインデックス</param>
        /// <param name="pageSize">1ページに表示するサイズ</param>
        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            this.PageIndex = pageIndex;
            this.TotalPages = (int)Math.Ceiling(count / (double)pageSize);
            this.AddRange(items);
        }

        /// <summary>
        /// 現在表示されているページの前にまだ表示するページが存在するか?
        /// </summary>
        public bool HasPreviousPage
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        /// <summary>
        /// 現在表示されているページの後方にまだ表示するページが存在するか?
        /// </summary>
        public bool HasNextPage
        {
            get
            {
                return (PageIndex < this.TotalPages);
            }
        }

        /// <summary>
        /// PagenatedListのコアメソッド。
        /// ソースリストをページサイズによりページ分けして、指定された
        /// インデックスのページを表示するリストを返す。
        /// </summary>
        /// <param name="source">指定されたページ用のリストを取り出す元となるリスト</param>
        /// <param name="pageIndex">取得するページ番号</param>
        /// <param name="pageSize">1ページに表示するレコード数</param>
        /// <returns>ページに表示するデータリスト</returns>
        public static async Task<PaginatedList<T>> CreateAsync (
            IQueryable<T> source, int pageIndex, int pageSize
            )
        {
            var count = await source.CountAsync().ConfigureAwait(false);
            var items = await source.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync()
                .ConfigureAwait(false);
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

【解説】

  1. PaginatingListのメインは静的な非同期メソッドCreateAsyncである。これが唯一コントローラの一覧画面用アクションメソッド(Index)からコールされるメソッドである。
    • CreateAsyncメソッドは内部でIQuerableな母集団レコードに対して、まずSkipメソッドでカレントページの前方にある表示しないレコードをスキップし、次にTakeメソッドで当該ページに表示するコードを抽出する。

    • async Task メソッドにする場合のセオリーとして、awaitする非同期メソッドを ConfigureAwait(false) としてデッドロックを回避する。
      ConfigureAwait(false)はVisual Studioのコードヒントによりコード追加をレコメンドされる。
  2. コンストラクタはCreateAsyncからのみコールされる。従って静的メソッドCreateAsyncは指定されたページに表示するモデルデータのリストと、現在のページインデックス(=PageIndex)、総ページ数(=TotalPages)、現在のページの前後に表示するページがあるか?(=HasPreviousPage、HasNextPage)といった情報を統合して有するモデルを返すことになる。

3. モデルへのページング拡張リストの組み込み

前提より一覧表示画面(Index.cshtml)には検索機能とソート機能が実装されているので、モデルはこれらの設定をUIとやり取りするためのプロパティーやメソッドを有している。
このモデルのメンバープロパティーである一覧出力対象モデルリストをPagenatedListにセットする。

\Models\PersonSearchModel.cs

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

namespace ANCEntry_EFMvcApp.Models
{
    public class PersonSearchModel
    {
        // public List<Person> People { get; set; }
        /// <summary>
        /// ビュー側に一覧表示するモデル(=Person)のリスト。
        /// これをページング対応クラス(PaginatedList)にする。
        /// </summary>
        public PaginatedList<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; }

        // ・・・ 以下省略 ・・・
    }
}

【解説】

  1. ここでの改修はごく僅か。元々Listであった出力対象リストの型をPaginatedListに変更するだけ。
  • モデル自体にはページング機能を備えるのではなく、モデルメンバーの一覧表示対象リストにページング機能を実装すると考える。

  • 一覧表示画面は1画面あたり何レコード表示可能で、トータル何ページになり、現在何ページ目が表示されているか?といった事項は全てPagenatedList側に保持される。

4. コントローラでのページング拡張リストの使用

前提より一覧表示画面(Index.cshtml)には検索機能とソート機能が実装されているので、コントローラのIndexアクションメソッドは既に引数として検索値やソート条件を引数として受け付けている。
ここにページングを行うための引数としてページインデックス、即ち「何ページ目を表示するか?」を追加する。

\Controllers\PersonController.cs

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

        // ・・・ 途中省略 ・・・
        // この部分にIQuerable<Person>selectedに、引数crt_nameとcrt_ageで指定された検索条件でデータを検索し、
        // sortfiledに指定されたソート条件で並べ替えを行った結果をセットする処理が記載されている。
        // ・・・

        var people_pagelist = await PaginatedList<Person>.CreateAsync(
            selected.AsNoTracking(), pageIndex ?? 1, PageSize
            ).ConfigureAwait(false);

        var myModel = new PersonSearchModel
        {
            People = people_pagelist,
            Crt_Name = crt_name,
            Crt_Age = crt_age,
            SortField = sortfield,
        };

        return View(myModel);
    }

【解説】

  1. 追加する引数indexは未指定の場合もあるためint?(=Null許可型)とする。 ここに値がセットされないのは検索条件やソート条件が変更されたときで、その場合には必ず1ページ目を表示する。(そのためこの値がnullであれば1が指定される。)

  2. 検索条件+ソート条件の指定で抽出と並べ替えが完了しているIQuerableを引数にしてPaginatedListのコンストラクタを呼び出す。
    • selected.AsNoTracking()はこれに続くページング処理が、データの更新を行わないため、別のスレッドにこれ以降の当該データリストのトラッキングを行わなくても良いことを伝え、パフォーマンスを改善する。

    • ConfigureAwait(false)はコードアシスタントによりレコメンドされた処理で、リソースのデッドロックを回避するためのもの。
  3. 最後にページ用モデル生成時に、上のコンストラクタ呼び出しで生成されたページング機能付きリストをメンバーとして引き渡す。後はViewとPaginatedListクラスそのものが全部やってくれる。

4. ビューでのページング拡張リストの使用

元となるビュー(Index.cshtml)では、検索条件入力エリア及び、一覧画面表示用テーブルのヘッダーにソート条件設定用のリンクを設定していた。これに新たにページングを実装するためのコントロールを加える。
と言っても元々実装されていた検索条件・ソート条件設定用のコントロールには一切手を加えない。

\Views\People\Index.cshtmls

@model ANCEntry_EFMvcApp.Models.PersonSearchModel

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

<h1>Find</h1>
<p>
    <a asp-action="Index">一覧へ</a>
</p>

@* 検索条件入力エリア *@
<form asp-controller="People" asp-action="Index" method="get">
    ・・・ (検索条件入力用のコントロールが書かれている) ・・・
</form>

<table class="table">
    <thead>
        <tr>
            <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].PersonID))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].PersonID))
                </a>
            </th>
            ・・・ 以下、上の例に倣ってFirstName, LastName, EMail, Ageの各項目にソート条件設定用の
            ・・・ タグヘルパー付きの<a>を内包する<th![4-3-1.Index先頭ページ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583684/d8e5afd4-7df1-7d01-2fc1-6e726791727d.jpeg)
>を書いていく(記載省略)

        </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>
・・・ ここまでは何も手を加えない。

@* モデルより現在表示されているページの前後移動が可能であるかをコードセクションの変数に格納 *@
@{
    var prevDisabled = !Model.People.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.People.HasNextPage ? "disabled" : "";
}

<div>
    @* 先頭ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="1"
       class="btn btn-primary @prevDisabled">
        Top
    </a>
    @* 前ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.PageIndex - 1)"
       class="btn btn-primary @prevDisabled">
        Previous
    </a>
    @* 後ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.PageIndex + 1)"
       class="btn btn-primary @nextDisabled">
        Next
    </a>
    @* 最後のページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.TotalPages)"
       class="btn btn-primary @nextDisabled">
        Last
    </a>
    @* 「カレントページインデックス / トータルページ数」を表示する *@
    <span class="border">@Model.People.PageIndex  /  @Model.People.TotalPages</span>
</div>

【解説】

  1. Viewの後半の前後のページ移動用リンク<a>とカレントページ表示用の|、及び直前の前後ページ移動可否を取得するコードブロックが全て。

  2. 前後ページ移動リンクにのみasp-route-pageIndexタグヘルパーにて、コントローラーのIndexアクションメソッドに引き渡すページインデックスがパラメータ名=pageIndexとして指定される。

やることはたったこれだけ!


5. 実行結果

1. 先頭のページ

検索・ソート条件なしの場合。先頭ページが表示される。Top/Previousリンクは使用不可。
4-3-1.Index先頭ページ.jpg

2. 中間のページ

次へボタンを2回クリックして3ページ目を表示したところ。前後へのページ遷移可能。
4-3-2.Index中間ページ.jpg

3. ソートを実行した直後

この状態でテーブルヘッダー行の「LastName」をクリックして姓でソートを実行した直後。並べ替えが実行され、先頭ページに移動する。

4-3-3.ソート実行直後.jpg

6. 今後の課題

以下のような機能を実装すること
1. 前後のページに移動するボタンではなく、表示可能なページ番号を列挙して、それぞれのページへのリンクを実装できるようなPaginatedListを作成する。

2. ページ数が10を超えたら表示しきれないページは「・・・」で表す。

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

一覧画面へのページング機能追加(MVC)・・・ASP.NET Core開発ノウハウ 4-3

今回のテーマ・課題

EntityFrameworkを利用したASP.NET Core MVCアプリケーションにおいてデータの一覧表示画面にページング機能を実装する。

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

  1. 一覧表示画面に表示する任意のモデルにページング機能を実装するリストクラスを作成して配置する。
    このリストクラスにはページングを実行可能にするために以下のようなプロパティーを持たせる。

    • 1ページ当たりの表示レコード数。
    • 全体のページ数
    • 現在のページインデックス
    • 現在のページインデックスの前方にまだ表示可能なページが存在するか?
    • 現在のページインデックスの後方にまだ表示可能なページが存在するか?
    • 母集団となる再絞り込み可能なモデルリストに対して与えられたページインデックスのレコードを取り出すためのメソッド。

    この拡張リストを使用してカレントページに表示するレコードを取り出す静的メソッドを用意する。

    このメソッドに引き渡される母集団となるデータリストはページング処理により再クエリーされるため、IQuerableインターフェイスを実装する必要がある。

  2. View側にはページングされたモデルのリストを表示するための以下のようなコントロールが必要になる。

  • トータルページ数と現在のページインデックスを表示するラベル。
  • 前方・後方にページをめくるボタン あるいは・・・
  • 「1, 2, 3, 4, ・・・」のように先頭からのページインデックスを(それらのページへの)リンクタグ付きで示すリスト。

ポイントとなるのはこれらの<ページング用のビューコントロールが元の一覧表示画面の検索機能や並べ替え機能に最小限の影響しか与えないようにすることである。

【説明の進め方】

このドキュメントの前提として、既にアプリケーションには一覧表示画面(Index.cshtml)が用意されており、そこには検索機能とソート機能が実装されているものとする。
そのサンプルとして「検索+ソート機能付き一覧画面(MVC)・・・ASP.NET Core開発ノウハウ 4-2」にて実装したPersonモデルの検索・ソート条件付き一覧表示画面にページング機能を実装する手順を示していく。(https://qiita.com/TR-MF/items/e5ba963be872ecdfe928)


2. ページング実装拡張リストクラス

プロジェクト内の\Commonまたは\Utility等の任意のフォルダに拡張リスト用クラス「PaginatingList<T>」クラスを作成する。

\Common\PaginatedList.cs

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

namespace ANCEntry_EFMvcApp.Common
{

    /// <summary>
    /// 一覧表示用UIに任意のモデルデータを表示する際にPaging機能を与えるList
    /// </summary>
    /// <typeparam name="T">UIに一覧表示するデータのモデルクラス</typeparam>
    public class PaginatedList<T>: List<T>
    {
        /// <summary>
        /// 現在のページのインデックス
        /// </summary>
        public int PageIndex { get; private set; }

        /// <summary>
        /// トータルページ数
        /// </summary>
        public int TotalPages { get; private set; }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="items">当該ページに表示するレコードリスト</param>
        /// <param name="count">元々リストに含まれていた総レコード数</param>
        /// <param name="pageIndex">現在表示するページのインデックス</param>
        /// <param name="pageSize">1ページに表示するサイズ</param>
        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            this.PageIndex = pageIndex;
            this.TotalPages = (int)Math.Ceiling(count / (double)pageSize);
            this.AddRange(items);
        }

        /// <summary>
        /// 現在表示されているページの前にまだ表示するページが存在するか?
        /// </summary>
        public bool HasPreviousPage
        {
            get
            {
                return (PageIndex > 1);
            }
        }

        /// <summary>
        /// 現在表示されているページの後方にまだ表示するページが存在するか?
        /// </summary>
        public bool HasNextPage
        {
            get
            {
                return (PageIndex < this.TotalPages);
            }
        }

        /// <summary>
        /// PagenatedListのコアメソッド。
        /// ソースリストをページサイズによりページ分けして、指定された
        /// インデックスのページを表示するリストを返す。
        /// </summary>
        /// <param name="source">指定されたページ用のリストを取り出す元となるリスト</param>
        /// <param name="pageIndex">取得するページ番号</param>
        /// <param name="pageSize">1ページに表示するレコード数</param>
        /// <returns>ページに表示するデータリスト</returns>
        public static async Task<PaginatedList<T>> CreateAsync (
            IQueryable<T> source, int pageIndex, int pageSize
            )
        {
            var count = await source.CountAsync().ConfigureAwait(false);
            var items = await source.Skip((pageIndex - 1) * pageSize)
                .Take(pageSize)
                .ToListAsync()
                .ConfigureAwait(false);
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

【解説】

  1. PaginatingListのメインは静的な非同期メソッドCreateAsyncである。これが唯一コントローラの一覧画面用アクションメソッド(Index)からコールされるメソッドである。
    • CreateAsyncメソッドは内部でIQuerableな母集団レコードに対して、まずSkipメソッドでカレントページの前方にある表示しないレコードをスキップし、次にTakeメソッドで当該ページに表示するコードを抽出する。

    • async Task メソッドにする場合のセオリーとして、awaitする非同期メソッドを ConfigureAwait(false) としてデッドロックを回避する。
      ConfigureAwait(false)はVisual Studioのコードヒントによりコード追加をレコメンドされる。
  2. コンストラクタはCreateAsyncからのみコールされる。従って静的メソッドCreateAsyncは指定されたページに表示するモデルデータのリストと、現在のページインデックス(=PageIndex)、総ページ数(=TotalPages)、現在のページの前後に表示するページがあるか?(=HasPreviousPage、HasNextPage)といった情報を統合して有するモデルを返すことになる。

3. モデルへのページング拡張リストの組み込み

前提より一覧表示画面(Index.cshtml)には検索機能とソート機能が実装されているので、モデルはこれらの設定をUIとやり取りするためのプロパティーやメソッドを有している。
このモデルのメンバープロパティーである一覧出力対象モデルリストをPagenatedListにセットする。

\Models\PersonSearchModel.cs

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

namespace ANCEntry_EFMvcApp.Models
{
    public class PersonSearchModel
    {
        // public List<Person> People { get; set; }
        /// <summary>
        /// ビュー側に一覧表示するモデル(=Person)のリスト。
        /// これをページング対応クラス(PaginatedList)にする。
        /// </summary>
        public PaginatedList<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; }

        // ・・・ 以下省略 ・・・
    }
}

【解説】

  1. ここでの改修はごく僅か。元々Listであった出力対象リストの型をPaginatedListに変更するだけ。
  • モデル自体にはページング機能を備えるのではなく、モデルメンバーの一覧表示対象リストにページング機能を実装すると考える。

  • 一覧表示画面は1画面あたり何レコード表示可能で、トータル何ページになり、現在何ページ目が表示されているか?といった事項は全てPagenatedList側に保持される。

4. コントローラでのページング拡張リストの使用

前提より一覧表示画面(Index.cshtml)には検索機能とソート機能が実装されているので、コントローラのIndexアクションメソッドは既に引数として検索値やソート条件を引数として受け付けている。
ここにページングを行うための引数としてページインデックス、即ち「何ページ目を表示するか?」を追加する。

\Controllers\PersonController.cs

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

        // ・・・ 途中省略 ・・・
        // この部分にIQuerable<Person>selectedに、引数crt_nameとcrt_ageで指定された検索条件でデータを検索し、
        // sortfiledに指定されたソート条件で並べ替えを行った結果をセットする処理が記載されている。
        // ・・・

        var people_pagelist = await PaginatedList<Person>.CreateAsync(
            selected.AsNoTracking(), pageIndex ?? 1, PageSize
            ).ConfigureAwait(false);

        var myModel = new PersonSearchModel
        {
            People = people_pagelist,
            Crt_Name = crt_name,
            Crt_Age = crt_age,
            SortField = sortfield,
        };

        return View(myModel);
    }

【解説】

  1. 追加する引数indexは未指定の場合もあるためint?(=Null許可型)とする。 ここに値がセットされないのは検索条件やソート条件が変更されたときで、その場合には必ず1ページ目を表示する。(そのためこの値がnullであれば1が指定される。)

  2. 検索条件+ソート条件の指定で抽出と並べ替えが完了しているIQuerableを引数にしてPaginatedListのコンストラクタを呼び出す。
    • selected.AsNoTracking()はこれに続くページング処理が、データの更新を行わないため、別のスレッドにこれ以降の当該データリストのトラッキングを行わなくても良いことを伝え、パフォーマンスを改善する。

    • ConfigureAwait(false)はコードアシスタントによりレコメンドされた処理で、リソースのデッドロックを回避するためのもの。
  3. 最後にページ用モデル生成時に、上のコンストラクタ呼び出しで生成されたページング機能付きリストをメンバーとして引き渡す。後はViewとPaginatedListクラスそのものが全部やってくれる。

4. ビューでのページング拡張リストの使用

元となるビュー(Index.cshtml)では、検索条件入力エリア及び、一覧画面表示用テーブルのヘッダーにソート条件設定用のリンクを設定していた。これに新たにページングを実装するためのコントロールを加える。
と言っても元々実装されていた検索条件・ソート条件設定用のコントロールには一切手を加えない。

\Views\People\Index.cshtmls

@model ANCEntry_EFMvcApp.Models.PersonSearchModel

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

<h1>Find</h1>
<p>
    <a asp-action="Index">一覧へ</a>
</p>

@* 検索条件入力エリア *@
<form asp-controller="People" asp-action="Index" method="get">
    ・・・ (検索条件入力用のコントロールが書かれている) ・・・
</form>

<table class="table">
    <thead>
        <tr>
            <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].PersonID))">
                    @Model.GetSortFieldDisplayName(Html.DisplayNameFor(model => model.People[0].PersonID))
                </a>
            </th>
            ・・・ 以下、上の例に倣ってFirstName, LastName, EMail, Ageの各項目にソート条件設定用の
            ・・・ タグヘルパー付きの<a>を内包する<th![4-3-1.Index先頭ページ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/583684/d8e5afd4-7df1-7d01-2fc1-6e726791727d.jpeg)
>を書いていく(記載省略)

        </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>
・・・ ここまでは何も手を加えない。

@* モデルより現在表示されているページの前後移動が可能であるかをコードセクションの変数に格納 *@
@{
    var prevDisabled = !Model.People.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.People.HasNextPage ? "disabled" : "";
}

<div>
    @* 先頭ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="1"
       class="btn btn-primary @prevDisabled">
        Top
    </a>
    @* 前ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.PageIndex - 1)"
       class="btn btn-primary @prevDisabled">
        Previous
    </a>
    @* 後ページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.PageIndex + 1)"
       class="btn btn-primary @nextDisabled">
        Next
    </a>
    @* 最後のページへ移動するボタン *@
    <a asp-controller="People" asp-action="Find2"
       asp-route-Crt_Name="@Model.Crt_Name"
       asp-route-Crt_Age="@Model.Crt_Age"
       asp-route-SortField="@Model.SortField"
       asp-route-pageIndex="@(Model.People.TotalPages)"
       class="btn btn-primary @nextDisabled">
        Last
    </a>
    @* 「カレントページインデックス / トータルページ数」を表示する *@
    <span class="border">@Model.People.PageIndex  /  @Model.People.TotalPages</span>
</div>

【解説】

  1. Viewの後半の前後のページ移動用リンク<a>とカレントページ表示用の|、及び直前の前後ページ移動可否を取得するコードブロックが全て。

  2. 前後ページ移動リンクにのみasp-route-pageIndexタグヘルパーにて、コントローラーのIndexアクションメソッドに引き渡すページインデックスがパラメータ名=pageIndexとして指定される。

やることはたったこれだけ!


5. 実行結果

1. 先頭のページ

検索・ソート条件なしの場合。先頭ページが表示される。Top/Previousリンクは使用不可。
4-3-1.Index先頭ページ.jpg

2. 中間のページ

次へボタンを2回クリックして3ページ目を表示したところ。前後へのページ遷移可能。
4-3-2.Index中間ページ.jpg

3. ソートを実行した直後

この状態でテーブルヘッダー行の「LastName」をクリックして姓でソートを実行した直後。並べ替えが実行され、先頭ページに移動する。

4-3-3.ソート実行直後.jpg

6. 今後の課題

以下のような機能を実装すること
1. 前後のページに移動するボタンではなく、表示可能なページ番号を列挙して、それぞれのページへのリンクを実装できるようなPaginatedListを作成する。

2. ページ数が10を超えたら表示しきれないページは「・・・」で表す。

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

【C#】プロパティ名を文字列として取得

久しぶりにWPFを使っていてViewModelのPropertyChangedを発行する際の
プロパティ名を固定値ではなく動的に取得するようにしておきたかったのでメモ。
#使わなくなるとすぐ忘れてしまったので…

サンプル
public class HogeModel : INotifyPropertyChanged
{
    int _param1;

    public int Param1
    {
        get { return _param1; }
        set { _param1 = value; OnPropertyChanged(); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

[CallerMemberName]で呼び出し元のメソッド名を取得出来るので
単純に自身の変更通知を行う場合は引数を指定しなくても
勝手に通知してくれます。
別プロパティの変更通知を行う場合や
通常のメソッド内で通知する場合は諦めてnameof演算子で取得するしかありませんが…。

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

C#で簡易手書き文字認識システムを作った

C#で数字だけを認識する、手書き文字認識システムを作りました。
非常に簡易的なものです。
以下の説明は、VisualStudioを使用して作成する場合の説明になります。

TegakiLib.cs
using System;
using System.Drawing;
using System.Windows.Forms;


namespace TegakiLib
{
    public class Tegaki : PictureBox
    {
        private Bitmap canvas;
        private Graphics g;
        private Pen p;
        private bool writing = false;
        private int pointCount = 0;
        private bool isInit = true;
        private int initPositionX = 0;
        private int initPositionY = 0;

        /// <summary>
        /// up=1,down=2
        /// left=1,right=2
        /// </summary>
        private int[][,] hanbetu = new int[10][,];
        private int[] refIndex = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

        public Action<int> exec;

        public Tegaki()
        {
            exec = (int i) => { };
            InitTegaki();
        }
        public Tegaki(Action<int> func)
        {
            exec = func;
            InitTegaki();
        }

        private void InitTegaki()
        {
            canvas = new Bitmap(500, 500);
            g = Graphics.FromImage(canvas);
            p = new Pen(Color.Cyan, 5);

            hanbetu[0] = new int[,] { { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[1] = new int[,] { { 2, 0, 0, 0 }, { 2, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[2] = new int[,] { { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[3] = new int[,] { { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[4] = new int[,] { { 0, 1, 0, 0 }, { 1, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[5] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[6] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[7] = new int[,] { { 1, 0, 0, 0 }, { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[8] = new int[,] { { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[9] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            writing = true;
            base.OnMouseDown(e);
        }

        protected override void OnMouseLeave(EventArgs e)
        {
            drawFin();
            base.OnMouseLeave(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            mouseMoving();
            base.OnMouseMove(e);
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            drawFin();
            base.OnMouseUp(e);
        }

        private void drawFin()
        {
            writing = false;
            isInit = true;
            int j;

            g.Clear(Color.FromArgb(64, 0, 64));
            this.Image = canvas;
            for (int i = 0; i <= 9; i++)
            {
                refIndex[i] = 0;
                for (j = 0; j < hanbetu[i].Length / 4; j++)
                {
                    hanbetu[i][j, 2] = 0;
                    hanbetu[i][j, 3] = 0;
                }
            }
        }

        private void mouseMoving()
        {
            if (!writing) return;

            int up = 1, down = 2, left = 1, right = 2;
            int ud = 10, lr = 10;
            int writtenNumber = 0;
            bool m = false;

            Point point = this.PointToClient(Control.MousePosition);

            g.DrawRectangle(p, point.X, point.Y, 5, 5);
            this.Image = canvas;

            if (isInit)
            {
                initPositionX = point.X;
                initPositionY = point.Y;
                isInit = false;
            }
            pointCount++;
            if (pointCount >= 5)
            {
                if ((point.X - initPositionX) >= 30)
                {
                    lr = right;
                    isInit = true;
                }
                if ((point.X - initPositionX) <= -30)
                {
                    lr = left;
                    isInit = true;
                }
                if ((point.Y - initPositionY) >= 30)
                {
                    ud = down;
                    isInit = true;
                }
                if ((point.Y - initPositionY) <= -30)
                {
                    ud = up;
                    isInit = true;
                }
                pointCount = 0;
                //isInit = true;

                for (int i = 0; i <= 9; i++)
                {
                    if (hanbetu[i][refIndex[i], 0] == ud || hanbetu[i][refIndex[i], 1] == lr)
                    {
                        hanbetu[i][refIndex[i], 2] = point.X;
                        hanbetu[i][refIndex[i], 3] = point.Y;

                        refIndex[i]++;
                        if (hanbetu[i][refIndex[i], 0] == 9 || hanbetu[i][refIndex[i], 1] == 9)
                        {
                            switch (i)
                            {
                                case 0:
                                    if ((hanbetu[0][0, 2] < hanbetu[0][2, 2]) && (hanbetu[0][1, 3] > hanbetu[0][3, 3]) &&
                                       (hanbetu[0][0, 3] - 10 > hanbetu[0][3, 3]))
                                    {
                                        writtenNumber = 0;
                                        m = !m;
                                    }
                                    break;
                                case 1:
                                    if (((hanbetu[1][0, 2] + 50 >= hanbetu[1][2, 2]) && (hanbetu[1][0, 2] - 50 <= hanbetu[1][2, 2])) &&
                                            (hanbetu[4][2, 2] == 0) && (hanbetu[4][2, 3] == 0) &&
                                            (hanbetu[9][4, 2] == 0) && (hanbetu[9][4, 3] == 0) &&
                                            (hanbetu[7][0, 2] == 0) && (hanbetu[7][0, 3] == 0))
                                    {
                                        writtenNumber = 1;
                                        m = !m;
                                    }
                                    break;
                                case 2:
                                    writtenNumber = 2;
                                    m = !m;
                                    break;
                                case 3:
                                    if ((hanbetu[3][0, 3] < hanbetu[3][1, 3]) && (hanbetu[3][2, 3] < hanbetu[3][3, 3]))
                                    {
                                        writtenNumber = 3;
                                        m = !m;
                                    }
                                    break;
                                case 4:
                                    if ((hanbetu[4][1, 3] < hanbetu[4][0, 3]) && (hanbetu[4][1, 2] < hanbetu[4][2, 2]))
                                    {
                                        writtenNumber = 4;
                                        m = !m;
                                    }
                                    break;
                                case 5:
                                    writtenNumber = 5;
                                    m = !m;
                                    break;
                                case 6:
                                    if ((hanbetu[6][1, 2] < hanbetu[6][0, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][2, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][3, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][4, 2]) &&
                                       (hanbetu[6][0, 3] + 30 < hanbetu[6][4, 3]))
                                    {
                                        writtenNumber = 6;
                                        m = !m;
                                    }
                                    break;
                                case 7:
                                    if ((hanbetu[9][3, 2] == 0) && (hanbetu[9][3, 3] == 0))
                                    {
                                        writtenNumber = 7;
                                        m = !m;
                                    }
                                    break;
                                case 8:
                                    if ((hanbetu[8][0, 3] < hanbetu[8][1, 3]) && (hanbetu[8][2, 3] > hanbetu[8][3, 3]))
                                    {
                                        writtenNumber = 8;
                                        m = !m;
                                    }
                                    break;
                                case 9:
                                    if (hanbetu[9][0, 3] < hanbetu[9][2, 3])
                                    {
                                        writtenNumber = 9;
                                        m = !m;
                                    }
                                    break;
                            }
                            if(m) exec(writtenNumber);
                        }
                    }
                }
            }
        }
    }
}

PictureBoxクラスの派生Tegakiクラスです。
上のソースコードをそのままコピーして、DLLにもできますし、プロジェクトの一部にしてもかまいません。
フォームのデザイン画面を開いてみると、ツールボックスにTegakiが追加されます。

使い方(プログラム例)

00001.png

Tegakiのサイズは500✕500のみ対応しています。プロパティでサイズを500✕500にしてください。
背景色は、何でもいいですが、勝手にRGB(64,0,64)に変わってしまいます。(手抜きですが・・・)
他にもラベルを2つ追加しました。

Form2.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TegakiNumber
{
    public partial class Form2 : Form
    {
        int j = 0;
        public Form2()
        {
            InitializeComponent();
            tegaki1.exec = exec;
        }

        private void exec(int i)
        {
            label1.Text = "現在の判別値" + i.ToString();
            j = i;
        }

        private void tegaki1_MouseUp(object sender, MouseEventArgs e)
        {
            label2.Text = "確定した判別値" + j.ToString();
        }
    }
}

フォームのソースコードです。
コンストラクターで、
tegaki1.exec = exec;
とありますが、Tegakiクラスの中でpublic Action<int> exec;と定義されています。
これはint型を1つ引数にとって、戻り値を返さない関数を意味しています。

つまりTegaki.execForm2.exec(int i)を渡しています。
この関数は、手書き入力中に、今現在判別している数字が変化したら実行されます。
その数字が引数iに渡されます。
label1は、入力中の現在の判別値を表示します。

この簡易文字認識システムは、マウスクリックからマウスが離されるまでの軌跡で判断します。
よって、label2は、tegaki1MouseUpイベントで、確定した判別値を返します。

使い方(数字の書き方)

先ほど説明したように、マウスクリックからマウスが離されるまでの軌跡で判断します。
つまり、一筆書きが基本です。
「4」は、
00002.png
の順に書きます。
「5」は、
00003.png
と書きます。
「7」は、
00004.png
最初の縦棒は必須です。

実際の実行中の画面

00006.png

「3」を書いてる途中の画面です。まだ途中なので、現在の判別値が「2」と判定されています。

00005.png

「6」を書いたときの画面です。(マウスクリックを離すと手書き文字が消えるので、確定した判別値が前回のままになっています。)

以上、数字だけに対応した簡易的な手書き文字認識システムでした。

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

脱出ゲームスクリプトメモ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;//書き加えないとToggleが使えない

public class tutorialScript : MonoBehaviour {

public GameObject wall1;
public GameObject wall2;
public GameObject wall3;
public GameObject wall4;

public GameObject close_door;//閉じた状態のドア
public GameObject open_door;//開いたドア
public GameObject close_treasurebox1;//閉じた宝箱1
public GameObject open_treasurebox1;//開いた宝箱1
public GameObject close_treasurebox2;//閉じた宝箱2
public GameObject open_treasurebox2;//開いた宝箱2

public GameObject key1;//カギ1を格納
public GameObject key2;//カギ2を格納

public Toggle key1_toggle;//カギ1を格納、アイテム選択の状態を取得するのに必要
public Toggle key2_toggle;//カギ2を格納、アイテム選択の状態を取得するのに必要

public GameObject calender_kakudai;//カレンダーの拡大画面
public GameObject treasurebox2_kakudai;//宝箱2の数字入力画面

public GameObject itemgetandclear;//下3つを表示するための全画面の透明な土台
public GameObject key1_get;//カギ1入手時に表示
public GameObject key2_get;//カギ2入手時に表示
public GameObject clear_text;//ドアを開けた時に表示

public GameObject left0;
public GameObject left1;
public GameObject left2;
public GameObject left3;
public GameObject left4;
public GameObject left5;
public GameObject left6;
public GameObject left7;
public GameObject left8;
public GameObject left9;

public GameObject center0;
public GameObject center1;
public GameObject center2;
public GameObject center3;
public GameObject center4;
public GameObject center5;
public GameObject center6;
public GameObject center7;
public GameObject center8;
public GameObject center9;

public GameObject right0;
public GameObject right1;
public GameObject right2;
public GameObject right3;
public GameObject right4;
public GameObject right5;
public GameObject right6;
public GameObject right7;
public GameObject right8;
public GameObject right9;

void Update()
{
    //613と入力し、かつ宝箱2が閉じているとき
    if (left6.activeInHierarchy == true && center1.activeInHierarchy == true &&
        right3.activeInHierarchy == true&& close_treasurebox2.activeInHierarchy == true)
    {
        close_treasurebox2.SetActive(false);
        open_treasurebox2.SetActive(true);
        treasurebox2_kakudai.SetActive(false);
        itemgetandclear.SetActive(true);
        key1_get.SetActive(true);
    }
}

public void gotowall1()
{
    wall1.SetActive(true);
    wall2.SetActive(false);
    wall3.SetActive(false);
    wall4.SetActive(false);
}

public void gotowall2()
{
    wall1.SetActive(false);
    wall2.SetActive(true);
    wall3.SetActive(false);
    wall4.SetActive(false);
}

public void gotowall3()
{
    wall1.SetActive(false);
    wall2.SetActive(false);
    wall3.SetActive(true);
    wall4.SetActive(false);
}

public void gotowall4()
{
    wall1.SetActive(false);
    wall2.SetActive(false);
    wall3.SetActive(false);
    wall4.SetActive(true);
}

//ヒエラルキーの閉じたドアに取り付ける
public void doortap()
{
    if (key2.activeInHierarchy == true && key2_toggle.isOn == true)
    {
        key2.SetActive(false);
        close_door.SetActive(false);
        open_door.SetActive(true);
        itemgetandclear.SetActive(true);
        clear_text.SetActive(true);
    }
}

//ヒエラルキーのカレンダーに取り付ける
public void calendertap()
{
    calender_kakudai.SetActive(true);
}
//ヒエラルキーのカレンダー拡大画像のボタンに取り付ける
public void calender_returntap()
{
    calender_kakudai.SetActive(false);
}

//閉じた宝箱1に取り付ける
public void treasurebox1tap()
{
    if (key1.activeInHierarchy == true && key1_toggle.isOn == true)
    {
        key1.SetActive(false);
        close_treasurebox1.SetActive(false);
        open_treasurebox1.SetActive(true);
        itemgetandclear.SetActive(true);
        key2_get.SetActive(true);
    }
}

//閉じた宝箱2に取り付ける
public void treasurebox2tap()
{
    treasurebox2_kakudai.SetActive(true);
}

//数字入力画面の下部の戻るボタンに取り付ける
public void treasurebox2_returntap()
{
    treasurebox2_kakudai.SetActive(false);
}

public void leftbuttontap()
{
    if (left0.activeInHierarchy == true)
    {
        left0.SetActive(false);
        left1.SetActive(true);
    }
    else if (left1.activeInHierarchy == true)
    {
        left1.SetActive(false);
        left2.SetActive(true);
    }
    else if (left2.activeInHierarchy == true)
    {
        left2.SetActive(false);
        left3.SetActive(true);
    }
    else if (left3.activeInHierarchy == true)
    {
        left3.SetActive(false);
        left4.SetActive(true);
    }
    else if (left4.activeInHierarchy == true)
    {
        left4.SetActive(false);
        left5.SetActive(true);
    }
    else if (left5.activeInHierarchy == true)
    {
        left5.SetActive(false);
        left6.SetActive(true);
    }
    else if (left6.activeInHierarchy == true)
    {
        left6.SetActive(false);
        left7.SetActive(true);
    }
    else if (left7.activeInHierarchy == true)
    {
        left7.SetActive(false);
        left8.SetActive(true);
    }
    else if (left8.activeInHierarchy == true)
    {
        left8.SetActive(false);
        left9.SetActive(true);
    }
    else if (left9.activeInHierarchy == true)
    {
        left9.SetActive(false);
        left0.SetActive(true);
    }
}

public void centerbuttontap()
{
    if (center0.activeInHierarchy == true)
    {
        center0.SetActive(false);
        center1.SetActive(true);
    }
    else if (center1.activeInHierarchy == true)
    {
        center1.SetActive(false);
        center2.SetActive(true);
    }
    else if (center2.activeInHierarchy == true)
    {
        center2.SetActive(false);
        center3.SetActive(true);
    }
    else if (center3.activeInHierarchy == true)
    {
        center3.SetActive(false);
        center4.SetActive(true);
    }
    else if (center4.activeInHierarchy == true)
    {
        center4.SetActive(false);
        center5.SetActive(true);
    }
    else if (center5.activeInHierarchy == true)
    {
        center5.SetActive(false);
        center6.SetActive(true);
    }
    else if (center6.activeInHierarchy == true)
    {
        center6.SetActive(false);
        center7.SetActive(true);
    }
    else if (center7.activeInHierarchy == true)
    {
        center7.SetActive(false);
        center8.SetActive(true);
    }
    else if (center8.activeInHierarchy == true)
    {
        center8.SetActive(false);
        center9.SetActive(true);
    }
    else if (center9.activeInHierarchy == true)
    {
        center9.SetActive(false);
        center0.SetActive(true);
    }
}

public void rightbuttontap()
{
    if (right0.activeInHierarchy == true)
    {
        right0.SetActive(false);
        right1.SetActive(true);
    }
    else if (right1.activeInHierarchy == true)
    {
        right1.SetActive(false);
        right2.SetActive(true);
    }
    else if (right2.activeInHierarchy == true)
    {
        right2.SetActive(false);
        right3.SetActive(true);
    }
    else if (right3.activeInHierarchy == true)
    {
        right3.SetActive(false);
        right4.SetActive(true);
    }
    else if (right4.activeInHierarchy == true)
    {
        right4.SetActive(false);
        right5.SetActive(true);
    }
    else if (right5.activeInHierarchy == true)
    {
        right5.SetActive(false);
        right6.SetActive(true);
    }
    else if (right6.activeInHierarchy == true)
    {
        right6.SetActive(false);
        right7.SetActive(true);
    }
    else if (right7.activeInHierarchy == true)
    {
        right7.SetActive(false);
        right8.SetActive(true);
    }
    else if (right8.activeInHierarchy == true)
    {
        right8.SetActive(false);
        right9.SetActive(true);
    }
    else if (right9.activeInHierarchy == true)
    {
        right9.SetActive(false);
        right0.SetActive(true);
    }
}

//ヒエラルキーのアイテム入手、クリアの土台パネルに取り付ける
public void itemgetandcleartap()
{
    if (key1_get.activeInHierarchy == true)
    {
        itemgetandclear.SetActive(false);
        key1_get.SetActive(false);
        key1.SetActive(true);
    }
    if (key2_get.activeInHierarchy == true)
    {
        itemgetandclear.SetActive(false);
        key2_get.SetActive(false);
        key2.SetActive(true);
    }
}

}

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

.NET Core + Vue.jsのプロジェクト作成

自分用のメモ代わりにやったことを書いておきます。
ほぼ参考にさせていただいたページのままですが、そちらのほうではMVCプロジェクトで作成していますが、ここではWebAPIプロジェクトで作成するので若干の違いがあります。

環境

  • Visual Studio 2019
  • .NET Core 3.1.1
  • vue cli 4.2.2

プロジェクト作成

ASP.NET Core WebAPIプロジェクトを作成

とりあえず認証等は何もつけずにAPIのみ

Vue.jsプロジェクト作成

作成したプロジェクトのフォルダで以下のコマンドを実行

vue create client-app

default (babel, eslint)を選択

.NET Coreプロジェクトファイルの編集

csprojファイルを以下のように編集

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="client-app\**" />
    <Content Remove="client-app\**" />
    <None Include="client-app\**" Exclude="client-app\node_modules\**" />
  </ItemGroup>

  <Target Name="ExecNpmInstall" BeforeTargets="Build" Condition="'$(Configuration)' == 'Debug' And !Exists('client-app\node_modules')">
    <Exec WorkingDirectory="client-app\" Command="npm install" />
  </Target>

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <Exec WorkingDirectory="client-app" Command="npm install" />
    <Exec WorkingDirectory="client-app" Command="npm run build" />

    <ItemGroup>
      <DistFiles Include="client-app\dist\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

</Project>

.NET Core側のSPA用Configとデバッグ用コードの追加

SPA用のパッケージが入っていないのでnugetで以下のパッケージをインストール

  • Microsoft.AspNetCore.SpaServices.Extensions

ConfigureServicesにSPA用の記述を追加

Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            // 追加
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = @"client-app/dist";
            });
        }

ConfigureにSPA用の記述を追加

Startup.cs
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            // 追加
            app.UseSpaStaticFiles();
            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "client-app";
                if (env.IsDevelopment())
                {
                    spa.UseProxyToSpaDevelopmentServer(async () =>
                    {
                        var pi = new ProcessStartInfo
                        {
                            FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd" : "npm",
                            Arguments = $"{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c npm " : "")}run serve",
                            WorkingDirectory = "client-app",
                            RedirectStandardError = true,
                            RedirectStandardInput = true,
                            RedirectStandardOutput = true,
                            UseShellExecute = false,
                        };
                        var p = Process.Start(pi);
                        var lf = app.ApplicationServices.GetService<ILoggerFactory>();
                        var logger = lf.CreateLogger("npm");
                        var tcs = new TaskCompletionSource<int>();
                        _ = Task.Run(() =>
                        {
                            var line = "";
                            while ((line = p.StandardOutput.ReadLine()) != null)
                            {
                                if (line.Contains("DONE  Compiled successfully in "))
                                {
                                    tcs.SetResult(0);
                                }

                                logger.LogInformation(line);
                            }
                        });
                        _ = Task.Run(() =>
                        {
                            var line = "";
                            while ((line = p.StandardError.ReadLine()) != null)
                            {
                                logger.LogError(line);
                            }
                        });
                        await Task.WhenAny(Task.Delay(20000), tcs.Task);
                        return new Uri("http://localhost:8080");
                    });
                }
            });
        }

一応ここまでで動作はしますが、デフォルトだとデバッグ実行した時にWeatherForecastにアクセスするようになっているためプロジェクトのプロパティで以下の画像のブラウザの起動の右にあるテキストボックスを空にします。
image.png

実行

デバッグ実行して問題がなければVue.jsプロジェクトのほうのページが開きます。
image.png

参考

https://blog.okazuki.jp/entry/2019/06/17/132755

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

[paiza レベルアップ問題集] 日付セット 曜日 (paizaランクB相当)

問題元

概要

年月日が与えられるので、その日付の曜日を計算する。
1800年1月1日は水曜日

コード

static void Main(string[] args)
        {
            string[] input = Console.ReadLine().Trim().Split(' ');
            int y = int.Parse(input[0]);
            int m = int.Parse(input[1]);
            int d = int.Parse(input[2]);
            int normalYear = 30 * 4 + 28 + 7 * 31;
            int leapYear = normalYear + 1;
            //1800年1月1日からy年m月d日の経過日数を計算
            int NumberOfDays = 0;
            //1800年1月1日から(y-1)年の大晦日までの日数を計算
            if(y > 1800)
            {
                NumberOfDays += normalYear - 1;
                for(int i =1801; i < y; i++)
                {
                    if (IsLeapYear(i))
                    {
                        NumberOfDays += leapYear;
                    }
                    else
                    {
                        NumberOfDays += normalYear;
                    }
                }
                //y年になってからの経過日数を計算
                for (int i = 1; i < m; i++)
                {
                    if (i != 2)
                    {
                        if (i == 4 | i == 6 | i == 9 | i == 11)
                        {
                            NumberOfDays += 30;
                        }
                        else
                        {
                            NumberOfDays += 31;
                        }
                    }
                    else
                    {
                        if (IsLeapYear(y))
                        {
                            NumberOfDays += 29;
                        }
                        else
                        {
                            NumberOfDays += 28;
                        }
                    }
                }
            }
            if(y == 1800)
            {
                NumberOfDays--;
            }
            NumberOfDays += d;
            //経過に日数の7の剰余が
            string dayOfWeek = null;
            switch(NumberOfDays)
            {
                case 0:
                    dayOfWeek = "水";
                    break;
                case 1:
                    dayOfWeek = "木";
                    break;
                case 2:
                    dayOfWeek = "金";
                    break;
                case 3:
                    dayOfWeek = "土";
                    break;
                case 4:
                    dayOfWeek = "日";
                    break;
                case 5:
                    dayOfWeek = "月";
                    break;
                case 6:
                    dayOfWeek = "火";
                    break;
            }
            Console.WriteLine(dayOfWeek + "曜日");

        }

        static bool IsLeapYear(int y)
        {
            //400で割り切れるうるう年
            if (y % 400 == 0)
            {
                return true;
            }
            //100で割り切れるなら平年
            else if (y % 100 == 0)
            {
                return false;
            }
            //4で割り切れるならうるう年
            else if (y % 4 == 0)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

シリーズ

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

環境

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

カレンダーの予定を更新

前回追加した予定を更新してみます。

メソッドは以下のUpdateを使用すれば良さそう。
Google.Apis.Calendar.v3.EventsResource.Update
image.png

今回は、前回の記事のインサートメソッドで追加したイベントを返却するようにし、そのイベント情報を用いて更新するようにします。

        /// <summary>
        /// カレンダーイベントを追加
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        /// <returns>イベント</returns>
        public Event InsertEvent(string calendarId)
        {
            var newEvent = new Event()
            {
                Summary = "Google I/O 2020",
                Location = "神奈川県横浜市",
                Description = "テスト備考",
                Start = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 9:00:00"),
                    TimeZone = "Asia/Tokyo",
                },
                End = new EventDateTime()
                {
                    DateTime = DateTime.Parse("2020/02/28 17:00:00"),
                    TimeZone = "Asia/Tokyo",
                },

                //以下があるとエラーになるので・・・
                //Recurrence = new string[] { "RRULE:FREQ=DAILY;COUNT=2" },
                //Attendees = new EventAttendee[] {
                //    new EventAttendee() { Email = "lpage@example.com" },
                //    new EventAttendee() { Email = "sbrin@example.com" },
                //},
                //Reminders = new Event.RemindersData()
                //{
                //    UseDefault = false,
                //    Overrides = new EventReminder[] {
                //        new EventReminder() { Method = "email", Minutes = 24 * 60 },
                //        new EventReminder() { Method = "sms", Minutes = 10 },
                //    }
                //}
            };

            var request = this.Serive.Events.Insert(newEvent, calendarId);
            var createdEvent = request.Execute();
            Console.WriteLine("Event created: {0}", createdEvent.HtmlLink);

            return createdEvent;
        }

        /// <summary>
        /// イベント更新
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        /// <param name="evt">更新対象イベント</param>
        /// <returns>更新後のイベント</returns>
        public Event UpdateEvent(string calendarId, Event evt)
        {
            evt.Summary = "Google I/O 2020 update";
            evt.Location = "東京都八王子市";
            evt.Start.DateTime = DateTime.Parse("2020/02/28 12:00:00");
            evt.End.DateTime = DateTime.Parse("2020/02/28 21:00:00");

            var request = this.Serive.Events.Update(evt, calendarId, evt.Id);

            return request.Execute();
        }

メインエントリ側

using GoogleAPITest.Calendar;
using System;

namespace GoogleAPITest
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            try
            {
                // カレンダーID
                var calendarId = "カレンダーID";

                // Googleカレンダーテストクラスインスタンス化
                var calApi = new CalendarAPITest(@"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json");

                // イベント読み取り
                calApi.ReadEvents(calendarId);

                // イベント追加
                var evt = calApi.InsertEvent(calendarId);

                // イベント更新
                calApi.UpdateEvent(calendarId, evt);
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Message);
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

動作確認

一度クリアしておいたほうがわかりやすいです。
image.png

実行結果
image.png
ちょっとわかりにくいですね
デバッグ実行して1ステップずつ動かしながら、カレンダーを見るとわかりやすいです。

予定の削除

削除もついでにやってみます。

        /// <summary>
        /// イベント削除
        /// </summary>
        /// <param name="calendarId">カレンダーID</param>
        /// <param name="eventId">イベントID</param>
        public void DeleteEvent(string calendarId, string eventId)
        {
            var request = this.Serive.Events.Delete(calendarId, eventId);
            request.Execute();
        }

メインエントリ

using GoogleAPITest.Calendar;
using System;

namespace GoogleAPITest
{
    /// <summary>
    /// メインクラス
    /// </summary>
    public class Program
    {
        /// <summary>
        /// メインエントリ
        /// </summary>
        /// <param name="args">実行時引数</param>
        public static void Main(string[] args)
        {
            try
            {
                // カレンダーID
                var calendarId = "カレンダーID";

                // Googleカレンダーテストクラスインスタンス化
                var calApi = new CalendarAPITest(@"C:\job\TestProject\GoogleAPITest\testproject-269217-813bf9be17a5.json");

                // イベント読み取り
                calApi.ReadEvents(calendarId);

                // イベント追加
                var evt = calApi.InsertEvent(calendarId);

                // イベント更新
                evt = calApi.UpdateEvent(calendarId, evt);

                // イベント削除
                calApi.DeleteEvent(calendarId, evt.Id);
            }
            catch (Exception err)
            {
                Console.WriteLine(err.Message);
            }
            finally
            {
                Console.Read();
            }
        }
    }
}

実行結果
image.png
※こちらもわかりにくいので、ステップ実行して1行ずつ確認したほうがいいです。
※削除処理で呼び出されるDeleteRequest.Executeはstringを戻り値で返すんですが、削除したイベントIDかと思いきや空文字でした・・・なので、DeleteEventメソッドはvoidにしてます。

おわりに

公式のサンプルでCRUDの基本は学べました。案件まだまにあうなら受けようかな・・・(クラウドソーシング)
GoogleCalendarAPIのお試しはとりあえずここまでにしようと思います。
次のネタがもうない・・・dockerの不明点調べないと・・・

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