WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Dispose パターン

by WebSurfer 31. May 2019 18:30

Visual Studio Community 2015(以下、VS2015 と書きます)には Dispose パターンに準拠したコードの骨組みを自動生成する機能が追加されています。

Dispose パターンの実装

上の画像がそれで、Disposable とタイプすると電球マークが表示されるので、その横の▼印をクリックすると Disposeパターンを選択できるようになります。

ここで、[Dispose パターンを使ってインターフェイスを実装します]にマウスをポイントすると以下の画像(Dispose パターンの骨組みのコード)が表示され、クリックするとそのコードがクラスに挿入されます。

([... 明示的に ...]の方を選ぶと、Dispose メソッドが IDisposable.Dispose と明示的に実装されます。その場合、クラスインスタンスから Dispose には直接アクセスできませんので注意してください。明示的の使い道は調べてなくて不明です)

コードの実装

そういう機能が追加されていることを知ってました? 実は自分は最近まで知らなかったです。(汗) VS2010 にはその機能はありませんでしたので、VS2012 ~ VS2015 のどれかから追加されたようです。

Dispose パターンの実装については、Microsoft のドキュメント「Dispose メソッドの実装」などに書かれていますが、読んでもよく分かりませんでした。(笑)

(ちなみに、VS2015 で自動生成されるのは上の Microsoft のドキュメントの「Dispose パターンには 2 種類あります」以下に書いてある後者の Object.Finalize メソッドをオーバーライドする方です)

VS2015 で自動生成されたコードと、ネットの情報「確保したリソースを忘れずに解放するには?」や「C# のファイナライザ、Dispose() メソッド、IDisposable インターフェースについて」などを読んで、やっと Dispose パターンを理解できたような気がします。

Dispose パターンの実装が必要なのは、アンマネージドリソースを使用するクラスのみです。アンマネージドリソースの種類で一般的なのは、ファイル、ウィンドウ、ネットワーク接続、データベース接続などのオペレーティングシステムリソースをラップしたオブジェクトです。

.NET Framework のクラスライブラリの中にも、例えば FileStream のようにファイルを開くために Windows API を呼び出してファイルハンドルを保持するというように、アンマネージドリソースを利用するものがあります。

そういうクラスは、IDisposable インターフェイスを継承して Dispose パターンを使った実装がされていて、Dispose メソッドでアンマネージドリソースを開放できるようになっているはずです。

ソースを見たわけではないので、.NET Frameowrk のクラスライブラリの全部が全部そうなのかは分かりませんが、Dispose メソッドを実装しているクラスは内部でアンマネージドリソースを使っていると考えて、そのオブジェクトが使用されなくなった時点で Dispose メソッドを呼び出すことを基本とすべきと思います。

自分で作るカスタムクラスの場合、マネージドリソースしか保持しない場合は Dispose パターンの実装は不要です。必要なのはアンマネージドリソースを保持する場合のみですが、それには以下のケースがあると思います。

  1. Dispose パターンを実装した .NET のクラスのインスタンスを保持している。
  2. クラス内でアンマネージドリソースを取得し、それを保持している。  

VS2015 のウィザードが生成する Dispose パターンの骨組みのコードを利用して、上記 1 および上記 1 + 2 のケースでコードを実装してみます。

上記 1 のケース

Dispose パターンを実装した .NET のクラスの例として DataSet を使いました。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;

    public DisposableSample()
    {
        myDataSet = new DataSet();
        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
    }
    
    // ・・・中略・・・
}

上記 1 + 2 のケース

アンマネージド(= GC では解放できない)リソースの例として SqlConnection で接続プールから取得する接続を使いました。接続プールはデフォルトで有効で、SqlConnection を Open すると接続プールから接続を取得してきます。

public class DisposableSample : IDisposable
{
    private DataSet myDataSet;
    private SqlConnection connection;

    public DisposableSample()
    {
        myDataSet = new DataSet();

        // アンマネージドリソースを取得
        string connString = ConfigurationManager.
                            ConnectionStrings["MyDB"].
                            ConnectionString;
        connection = new SqlConnection(connString);
        connection.Open();

        // ・・・中略・・・
    }

    private bool disposedValue = false; // 重複呼出の検出用

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // .NET のオブジェクトを解放
                myDataSet.Dispose();
            }

            // アンマネージドリソースを解放
            // SqlConnection の Dispose と Close は同じ
            connection.Dispose();

            disposedValue = true;
        }
    }

    // Dispose し忘れても、ガベージコレクタが働いたときに
    // ファイナライザが呼ばれるので、そこでアンマネージド
    // リソースを解放できるよう以下のコードを実装する。
    // C++ のデストラクタの構文だが、C# ではこれによりフ
    // ァイナライザをオーバーライドすることになるらしい
    ~DisposableSample() 
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);

        // 上の Dispose(bool disposing) メソッドでアンマネー
        // ジドリソースを解放した場合はファイナライザが実行
        // されないようにする
        GC.SuppressFinalize(this);
    }

    // ・・・中略・・・
}

上のコードのアンマネージドリソースの取得で、実際にこのような形で接続を保持するケースはないのかもしれませんが、他に具体例が思いつかなかったのでこうしてみました。

プログラマが Dispose() するのを忘れた場合でもガベージコレクタは働きます。ガベージコレクタではアンマネージドリソースは開放できませんが、その際ファイナライザが呼び出されます。なので、最悪でもファイナライザでアンマネージドリソースを開放できるようにしています。

Dispose() メソッドが呼ばれ、それから Dispose(bool disposing) メソッドが呼ばれてアンマネージドリソースを解放した場合は GC.SuppressFinalize(this) を呼び出します。 ファイナライザの実行はパフォーマンスに大きな影響を与えるそうですが、GC.SuppressFinalize メソッドを呼び出しておくと、ファイナライザの呼び出しは行われなくなります。

Tags: , , ,

.NET Framework

XML ファイルを DataGridView に表示

by WebSurfer 26. April 2019 12:14

XML ファイルからデータを取得して Windows Forms アプリの DataGridView に表示し、それをユーザーが編集して結果を XML ファイルに書き戻すサンプルを書きます。

XML ファイルを DataGridView に表示

ASP.NET Web Forms アプリの場合は先の記事「XML ファイルの更新操作」に書きましたのでそちらを見てください。

アプリの基本的な構成は、DataGridView ⇔ BindingSource ⇔ DataSet ⇔ XML ファイルとしています。

DataSet.ReadXml メソッドで XML ファイルからデータを DataSet に読み込み、BindingSource 経由で DataGridView に表示。ユーザーが DataGridView に表示されたデータを編集後(編集結果は DataSet に反映されます)、ボタンクリックで DataSet.WriteXml メソッドにより編集結果を XML ファイルに書き戻すという操作を行います。

Windows Forms アプリの場合、ASP.NET Web Froms アプリとは違って、DataSet や DataGridView などすべてのインスタンスをユーザーの PC メモリ上に保持できますので、上記の操作が可能になります。

コードは、表示・編集・更新を行うためのごく基本的な部分だけですが、以下の通りです。

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 WindowsFormsApplication1
{
    public partial class Form6 : Form
    {
        private DataGridView dataGridView1;
        private BindingSource bindingSource1;
        private DataSet dataset1;
        private string dir;
        private string file;

        public Form6()
        {
            InitializeComponent();            
        }

        private void Form6_Load(object sender, EventArgs e)
        {
            this.dataGridView1 = new DataGridView();
            this.dataGridView1.Dock = DockStyle.Fill;
            this.bindingSource1 = new BindingSource();
            this.dataGridView1.DataSource = this.bindingSource1;
            this.Controls.Add(this.dataGridView1);

            this.dataset1 = new DataSet();
            this.dir = @"XML ファイルのあるフォルダ";
            this.file = "XML ファイル";
            this.dataset1.ReadXml(dir + file);

            this.bindingSource1.DataSource = dataset1.Tables[0];
        }

        private void button1_Click(object sender, EventArgs e)
        {
            this.Validate();
            this.bindingSource1.EndEdit();
            this.dataset1.WriteXml(dir + file, 
                                   XmlWriteMode.WriteSchema);
            this.dataset1.AcceptChanges();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            this.bindingSource1.RemoveCurrent();
        }
    }
}

XML ファイルはスキーマ付きにしています。その結果、DataTable の各列の DataType はスキーマに応じて型が設定され、例えば bool 型の場合はチェックボックスが表示されます。

DataSet.WriteXml で DELETE 操作を行うためには DataTable の当該行の DataRow.RowState プロパティを DataRowSate.Deleted に設定する必要があります。それを BindingSource.RemoveCurrent メソッドで行っています。

Tags: , , , ,

.NET Framework

int 型プロパティの検証、エラーメッセージ

by WebSurfer 24. March 2019 16:06

ASP.NET MVC で、モデルのプロパティが int 型の場合の検証とエラーメッセージに関する注意点を書きます。

検証、エラーメッセージ

モデルのプロパティが int 型の場合、クライアント側の検証を有効にしておけば、RequiredAttribute, RegularExpressionAttribute は付与しなくても input 要素には data-val-required="xxx フィールドが必要です。", data-val-number="フィールド xxx には数字を指定してください。" という属性が付与され、入力に応じてそれらのエラーメッセージが表示されます。

プロパティに RequiredAttribute が付与され ErrorMessage が設定されている場合は、data-val-required 属性に設定される文字列が ErrorMessage に置き換わります。(ちなみに、プロパティが int? 型の場合は data-val-required 属性そのものが付与されません)

上記は TextBoxFor, EditorFor いずれを使っても同じです。

ただし、EditorFor を使うと input 要素の type 属性が "number" となるので、それによりブラウザ依存の動きが出るのに要注意です。(TextBoxFor を使った場合は type 属性は "text" となります)

input 要素の type 属性が "number" となると、例えば、Chrome は数字以外の入力は受け付けなくなりますが、IE11 は最初の文字が数字であれば後に続く文字は何でも入力できてしまうという違いが出ます。

さらに、プロパティに RegularExpressionAttribute を追加して数字か否かをチェックするようにしても、input 要素の type 属性が "number" となっていると無視されます。

その場合動きはブラウザ依存になり、"1x" というような入力を受け付ける IE11 では data-val-number 属性に設定されたメッセージが、Firefox では data-val-required 属性に設定されたメッセージが表示されます。Chrome は "1x" というような文字は入力できませんが、"1..." という文字列は受け付けるので、その場合は Firefox と同様に data-val-required 属性に設定されたメッセージが表示されます。

EditorFor ではなく TextBoxFor を使えば input 要素の type 属性は "text" となって、RegularExpressionAttribute による検証が行われ、検証 NG の場合は ErrorMessage に設定したメッセージが表示されます。

input 要素の type 属性が "number" となることによりブラウザ依存の動きとなって期待と異なるエラーメッセージが出るのを避けるためには以下の対応が必要です:

  1. TextBoxFor を使って input 要素の type 属性が "text" となるようにし、さらに
  2. RegularExpressionAttribute で数字か否かの検証を行う。  

以上はクライアント側での検証の話です。サーバー側での検証によるエラーメッセージは上記とは異なります。上の画像の「価格2 (int)」のエラーメッセージを見てください。

クライアント側での検証を無効にして "2000x" という文字列を送信していますが「値 '2000x' は 価格2 (int) に対して無効です。」というエラーメッセージが出ています。

それは EditorFor (type="number") でも TextBoxFor (type="text") でも同じで、数字として不正な文字が混ざって POST されると、モデルバインディングの際 int 型にパースできないということで、RegularExpressionAttribute による検証が行われる前に検証 NG となって、そのエラーメッセージが出るようです。

RegularExpressionAttribute の ErrorMessage に設定したメッセージが表示されて欲しいのですが、int 型にパースできない文字列が POST されては何ともならないようです。ただし、このエラーメッセージを書き換える方法はあります。

マイクロソフト公式解説書「プログラミング ASP.NET MVC」の p186「エラーメッセージを制御する」に書いてあったことですが、ModelStateDictionary に含まれる ModelState は同じ Key でマージした方に上書きされます。具定例は以下のコードの通りです。上の画像の「ID (int)」がこのコードによる書き換え結果です。

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult WankumaEdit(Keiyaku model)
{
  if (ModelState.IsValid)
  {
    // DB の編集処理
    return RedirectToAction("Index");
  }

  // デバッグ用
  ModelStateDictionary dictionary = ModelState;

  // ValidationSummary(true) に表示するために追加
  var newDictionary = new ModelStateDictionary();
  newDictionary.AddModelError("",
    "ValidationSummary に表示するために追加。");
  ModelState.Merge(newDictionary);

  // エラーメッセージを書き換えることはできる。
  // 「プログラミング ASP.NET MVC」の p186「エラーメッセージ
  // を制御する」参照。同じ Key でマージした方に上書きされる
  ModelState state = dictionary["KeiyakuID"];

  if (state.Errors.Count > 0)
  {
    string msg = state.Errors[0].ErrorMessage;
    if (msg.StartsWith("値"))
    {
      // マージすると Value が null になるので書き戻すために
      // 取得しておく
      ValueProviderResult value = state.Value;
      var newDictionary2 = new ModelStateDictionary();
      newDictionary2.AddModelError("KeiyakuID",
        "入力不正(デフォルトの「値 'xx' は ID に対して" +
        "無効です。」を書き換え)");
      ModelState.Merge(newDictionary2);

      // Value を書き戻す。そうしないと再描画されたとき元の
      // ユーザー入力が表示されず 0 になってしまう
      ModelState["KeiyakuID"].Value = value;
    }
  }
  return View(model);
}

Tags: , ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  October 2019  >>
MoTuWeThFrSaSu
30123456
78910111213
14151617181920
21222324252627
28293031123
45678910

View posts in large calendar