WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WinForms アプリで構成情報の上書き (CORE)

by WebSurfer 2023年8月19日 13:26

Windows Forms アプリで接続文字列その他の構成情報がデバッグ版とリリース版で異なるケースがあると思います。ターゲットフレームワークが .NET 6.0 の Windows Forms アプリで、デバッグ版とリリース版での構成情報を別々の json 形式のファイルに格納し、プログラムで構成情報を上書きして使用する方法を書きます。

(注: ターゲットフレームワークが .NET Framework 4.8 の Windows Forms アプリでも Microsoft.Extensions.Configuration.Json を NuGet からインストールして以下の記事と同様なことは可能です)

先の記事「WinForms で構成情報とコンテキストの DI (CORE)」で、Windows Forms アプリでも json 形式の構成ファイルに構成情報を格納し、それから情報を取得してアプリで使用できることを書きました。

(Microsoft のドキュメント「.NET Framework から .NET にアップグレードした後に最新化する」でも、.NET アプリでは App.config ではなくて json 形式のファイルを構成ファイルとして利用することが推奨されているようです)

ASP.NET Core アプリでは、appsettings.json に加えて appsettings.Production.json を追加しておけば、プロジェクトに組み込みの機能が運用環境での環境変数を読み取って、それが Production であれば自動的に appsettings.Production.json から構成情報を取得して上書き・追加を行うようになっています。詳しくは先の記事「環境別の appsettings.json (CORE)」を見てください。

Windows Forms アプリには ASP.NET Core のような組み込みの機能はありませんが、アプリのプログラムで動的に appsettings.json の情報を appsettings.Production.json の情報で上書きすることができます。(注: ASP.NET Core アプリと違って json ファイルの名前は任意です。この記事では ASP.NET Core アプリに合わせていますが)

接続文字列がデバッグ版とリリース版で異なるような場合は、デバッグ版の接続文字列は appsettings.json に設定し、リリース版の接続文字列は appsettings.Production.json に設定しておいて、アプリのプログラムで動的に appsettings.Production.json に設定した接続文字列を使用するということも可能です。

具体例を書きます。まず、以下のような json 形式の構成ファイルを用意します。ファイル名は ASP.NET Core の例に合わせて appsettings.json, appsettings.Production.json としましたが、ASP.NET Core の機能を使うわけではないので、任意です。

appsettings.json

{
  "ConnectionStrings": {
    "NorthwindConnection": "Data Source=(localdb)\\MSSQLLocalDB;Database=NORTHWIND;Integrated Security=True"
  },
  "FilePath": "C:\\Users\\surfe\\Documents",
  "FileName": "development.txt"
}

appsettings.Production.json

{
  "ConnectionStrings": {
    "NorthwindConnection": "Server=MyServer;Database=NORTHWIND;Integrated Security=True"
  },
  "FileName": "production.txt",
  "AdditionalInfo": "project dependent info"
}

上の例で、接続文字列 NorthwindConnection とファイル名 FileName はデバッグ版とリリース版で異なる、パス名 FilePath は共通、AdditionalInfo はリリース版でのみ必要な追加の情報という想定です。

appsettings.json の情報を appsettings.Production.json の情報で上書きする具体例は以下の通りです。説明はコメントに書きましたのでそれを読んでください。(ASP.NET Core アプリのように、環境変数を読み込んでそれにより json ファイルを切り替えるところまでは下のコードには含んでいません。無条件で上書きされるので注意してください)

using Microsoft.Extensions.Configuration;

namespace WinFormsApp5
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // appsettings.json にはデバッグ版の接続文字列その他の
            // 構成情報を含む

            // appsettings.Production.json にはリリース版の接続文字
            // 列、その他リリース版では異なる情報、追加でリリース版
            // に必要な情報を含む

            // IConfiguration (アプリケーションで使用するキー/値ベー
            // スの構成情報) をビルドするための ConfigurationBuilder
            // を生成
            var builder = new ConfigurationBuilder()
                // ファイルベースのプロバイダ (appsettings.json) が
                // 存在するディレクトリを指定。下のコードで指定して
                // いるのはアプリケーションの現在の作業ディレクトリ
                .SetBasePath(Directory.GetCurrentDirectory())
                // 指定されたディレクトリの appsettings.json (JSON
                // 構成プロバイダ) を ConfigurationBuilder に追加
                .AddJsonFile("appsettings.json", optional: false);

            // 加えて、appsettings.Production.json を追加
            builder.AddJsonFile("appsettings.Production.json",
                                optional: false);

            // 追加されたプロバイダは以下のようにして確認できる
            var sources = builder.Sources;

            // ソースに登録されているプロバイダーのキーと値を使用し
            // て IConfiguration をビルド。その時、appsettings.json
            // と同じ名前のキーが appsettings.Production.json にあ
            // る場合は後者の情報で上書きされる
            IConfiguration config = builder.Build();

            // IConfiguration から構成情報を取得
            string? connString = 
                config.GetConnectionString("NorthwindConnection");
            string? filePath = config["FilePath"];
            string? fileName = config["FileName"];
            string? addInfo = config["AdditionalInfo"];
        }
    }
}

結果は以下の通りとなります。appsettings.json の情報が appsettings.Production.json の情報で上書きされているのが分かりますでしょうか?

構成情報の上書き

Tags: , , ,

CORE

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

by WebSurfer 2023年8月2日 15:29

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

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

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

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

Northwind Orders テーブル

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

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

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

テンプレート

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

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

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

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

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

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

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

DbContext ダイアグラム

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

(3) Controller と View の作成

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

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

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

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

スキャフォールディング

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

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

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

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

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

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

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

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

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

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

        // ・・・中略・・・

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

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

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

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

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

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

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

(5) Search.cshtml

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

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

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

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

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

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

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

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

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

                dataType: "html",

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

        //]]>
    </script>
}

(6) SearchResult.cshtml

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

@model IEnumerable<MvcNet7App.Models.Order>

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

Tags: , , , ,

CORE

ASP.NET MVC の View での CS8602 対応

by WebSurfer 2023年8月1日 18:24

ASP.NET Core MVC アプリで、以下の画像のように View でナビゲーションプロパティ経由でデータを取得するコードを書くと、プロパティの型が null 許容参照型の場合は "CS8602: null 参照の可能性があるものの逆参照です" という警告が出ます。これにどう対応するかという話を書きます。

null 許容の警告

(ターゲットフレームワークが .NET 6.0 以降のプロジェクトでは全体で「Null 許容」オプションが有効になっています。リバースエンジニアリングを使って生成する Entity Framework 用のコンテキストクラス、エンティティクラスも、基になるデータベースの構造に合わせて、プロパティに null 許容参照型が使われるようになります)

どう対応するかですが、結論から書きますと、何しなくても問題は出なかったです。上の画像のコードで Category, Supplier が null でも NullReferenceException がスローされることはなくアプリは完了し、ブラウザ上ではそれらの項目は空白となります。

以下に、そのあたりのことをもう少し詳しく書きます。

上の画像の View には Controller から IEnumerable<Product> 型の Model が渡されています。そのコードは以下の通りです。Include を使って Category, Supplier も取り込んでいるところに注意してください。

var northwindContext = _context.Products
                       .Include(p => p.Category)
                       .Include(p => p.Supplier);

return View(await northwindContext.ToListAsync());

上の画像で警告が出ているコードの item というのは Product クラスのオブジェクトになります。

この記事の例では、既存の SQL Server サンプルデータベース Northwind の Products, Suppliers, Categories テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを作成して使っており、そのエンティティクラスの一つが Product クラスです。

Product クラスの基になる SQL Server の Products テーブルは下の画像の構成となっています。赤枠で示した SupplierID, CategoryID フィールドは NULL 可で、Suppliers, Categories テーブルに FK 制約が張られています。

SQL Server の Products テーブル

このテーブルからリバースエンジニアリングで生成される Product エンティティクラスの Category, Supplier ナビゲーションプロパティは、以下の通り null 許容参照型となります。

[ForeignKey("CategoryId")]
[InverseProperty("Products")]
public virtual Category? Category { get; set; }

[ForeignKey("SupplierId")]
[InverseProperty("Products")]
public virtual Supplier? Supplier { get; set; }

それゆえ、この記事の一番上の画像のように、View でそれらのナビゲーションプロパティ経由で値を取得しようとすると、"CS8602: null 参照の可能性があるものの逆参照です" という警告が出ます。

Products テーブル の SupplierID, CategoryID フィールドは NULL 可なので、Product エンティティクラスの Category, Supplier ナビゲーションプロパティから取得できる値は null になることがあります。

実際に Products テーブル の SupplierID, CategoryID フィールドを NULL にすると、View のコードで item.Category, item.Supplier は null になります。下の画像がその例です。item の中の Category と Supplier を見てください。

デバッグ画面

ということで、NullReferenceException がスローされないよう対処する必要がある・・・と思っていましたが、実際は、上にも書きましたように、NullReferenceException がスローされることはなくアプリは正常に終了し、ブラウザ上での表示はそれらの項目は空白となります。

ただし、そのあたりのことを書いた Microsoft のドキュメントは見つけることができませんでした。なので、試した結果からそう言っているだけで、どういう状況でも 100% 問題ないかまでは自信がありません。

(Microsoft のドキュメントの「必須のナビゲーション プロパティ」のセクションに "必要な依存が適切に読み込まれている限り (例: 経由 Include)、ナビゲーション プロパティにアクセスすると、常に null 以外が返されます" と書いてありますが、違う話のような気がします)

どうしても対応したいという場合はどうしたらいいでしょうか? Microsoft のドキュメント「null 許容の警告を解決する」に書いてある "変数を逆参照する前に変数が null でないこと確認する" ということになりますが、@Html.DisplayFor の引数のラムダ式上ではそれはできないようです。

ちなみに、三項演算子を使うと "InvalidOperationException: Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions." というエラーになります。null 合体演算子を使って item.Supplier?.CompanyName ?? "" のようにすると "CS8072 式ツリーのラムダに null 伝搬演算子を含めることはできません" というエラーになります。

というわけで、"変数を逆参照する前に変数が null でないこと確認する" には以下のようにすることになりそうです。

<td>
    @{
        var categoryName = (item.Category != null) ?
            item.Category.CategoryName : "";
    }
    @Html.DisplayFor(modelItem => categoryName)
</td>
<td>
    @{
        var supplierName = (item.Supplier != null) ?
            item.Supplier.CompanyName : "";
    }
    @Html.DisplayFor(modelItem => supplierName)
</td>

そこまでやらなくても、! (null 免除) 演算子を使って警告を消すだけでもよさそうな気はしますが。

Tags: , , , ,

CORE

About this blog

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

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar