年別アーカイブ: 2009年

42件の投稿

TextBoxの拡張=TextBoxEx

ちょっとした、でもとても重宝しているプログラムをご紹介します。

最初は、私が作ったものではなく、Balenaの「プログラミング Visual Basic.NET」に載っていたものです。

邦訳は2002年日経BPソフトプレスから出版され、付属のCDにソースが入っていました。そのままここに載せると著作権に抵触するかもしれません。Balenaのブログページ(http://www.dotnet2themax.com/blogs/fbalena/)から”Programming Microsoft Visual Basic .NET 2003″Chapter 18を開くと原書のサンプルページがありコードが載っています。

これは、TextBoxを継承したカスタムコントロールです。これによって、データ型や正規表現のチェックが可能です。
以下にこのプログラムの主要な部分を転載します。当然細かいところは分かりませんが、プログラムの雰囲気は分かると思います。

' 拡張されたTextBoxコントロール
Public Class TextBoxEx
    Inherits System.Windows.Forms.TextBox

    Event InvalidKey(ByVal sender As Object, ByVal e As EventArgs)

    Dim m_IsRequired As Boolean
    <Description("入力が必須である場合には、Trueにします。"), Category("検証")>_
    Property IsRequired() As Boolean
	...... 省略
     End Property

    Dim m_ErrorMessage As String
    <Description("エラー時に表示されるメッセージです。"), Category("検証"。")>_
    Property ErrorMessage() As String
	...
    End Property

  ... 省略

    ' ValidateRegexプロパティ。
     <Description("正規表現を使用して、データを検証します。"), Category("検証")>_
    Property ValidateRegex() As String
        Get
            Return m_ValidateRegex
        End Get
        Set(ByVal Value As String)
            ' これが有効な正規表現であることを検査します。
            Try
                If Value <> "" Then
                    Dim dummy As Boolean = Regex.IsMatch("abcde", Value)
                End If
                m_ValidateRegex = Value
            Catch ex As Exception
                MessageBox.Show(ex.Message, "Invalid Property", _
                        MessageBoxButtons.OK, MessageBoxIcon.Error)
            End Try
        End Set
    End Property

    Dim m_ValidateRegex As String

    ' エラーメッセージを表示するオプションのコントロール。
     Dim m_DisplayControl As Control

    <Description("エラーメッセージを表示するコントロールです。"), Category("検証")>_
    Property DisplayControl() As Control
	...... 省略
    End Property

    ' コントロールに設定される値の型です。
    Enum ValidTypes
        Any = 0
        [Byte]
        [Short]
        [Integer]
                     ... 省略
        [DateTime]
    End Enum

    Dim m_ValidType As ValidTypes

    <Description("入力する値のデータ型です。"), Category("検証")>_
    Property ValidType() As ValidTypes
        ......
    End Property

    ' Validateメソッド
     <Description("現在の値が検証テストにパスすると、Trueを返します。">_
    Function Validate(Optional ByVal DisplayMessage As Boolean = True) As Boolean
        Validate = True

        If Me.IsRequired And Me.Text = "" Then
            Validate = False
        End If
        If Validate = True And Me.Text <> "" Then
            Validate = CheckValueType(Me.Text)
        End If
        If Validate = True And Me.ValidateRegex <> "" Then
            Validate = Regex.IsMatch(Me.Text, Me.ValidateRegex)
        End If

        If DisplayMessage And Not (DisplayControl Is Nothing) And Me.ErrorMessage <> "" Then
            If Validate Then
                DisplayControl.Text = ""
            Else
                DisplayControl.Text = Me.ErrorMessage
                DisplayControl.ForeColor = m_ErrroForeColor
            End If
        End If

        If Not Validate And Me.BeepOnError Then Beep()
    End Function

    Function CheckValueType(ByVal o As Object) As Boolean
        Dim res As Object
        Try
            Select Case m_ValidType
                Case ValidTypes.Byte : res = CByte(o)
                Case ValidTypes.Short : res = CShort(o)
                     ... 以下省略
            End Select
              Return True
        Catch
            Return False
        End Try
    End Function

    ' このメソッドは、コントロールがValidatingイベントを発生させるときに実行します。
    Protected Overrides Sub OnValidating(ByVal e As System.ComponentModel.CancelEventArgs)
        If Me.Validate() Then
            MyBase.OnValidating(e)
        Else
            e.Cancel = True
        End If
    End Sub
End Class

このプログラムで一番重要なところは、最後のOnValidatingです。このコントロールにデータを入力し、抜けようとするとこのメソッド(イベント)が起動し、データの整合性をチェックし、誤りがあれば指定のコントロールあるいはErrorProviderにメッセージを表示します。
(このプログラムではエラーメッセをLabel等のコントロールに表示するようになっていますが、私はこの部分をErrorProviderに変える等少し手を入れて使っています。)

このプログラムは、Dllとしてコンパイルしこれを参照設定することで、通常のコンポーネント(TextBox等)と同様に使うことができます。

私は、数字入力および正規表現チェックには必ずこのTextBoxExを使います。

ついでですが、私は郵便番号ではMaskedTextBoxを使いますが、電話番号、携帯電話、Emailの入力にはこのTextBoxExを使います。
郵便番号は、日本中で桁数が同じですが、電話番号では市外、市内局番等桁数が異なりますので、TextBoxExでの正規表現のチェックの方が柔軟性があるからです。
ちなみに、次のような正規表現を使っています(どこかから探してきて自分なりに修正したものです。正誤の保障はありません。Emailはそのまま)。

郵便番号 "^d{3}-d{4}$"
電話番号 "^(d{2,4}-){0,1}d{2,4}-d{4}$"
携帯番号 "^d{2,4}-d{2,4}-d{4}$"
Email "^([w-.]+)@(([[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.)|(([w-]+.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(]?)$"

Entity Framework と MyDataBinding

LINQでは基本的に、厳密に型指定されたオブジェクトを使用します。これのいいところと悪いところがあります。

いいところは、タイプインの途中でインテリセンスがコーディングをサポートしてくれますので、ケアレスミスを防いでくれます。パフォーマンスも多少いいかも知れません。

ところがDBから抽出したデータをコントロールに表示したり、逆にコントロールのデータをDBに保存するときのコードが多少煩わしくなります。
この原因を作っているのは、私が[Data Binding]を嫌っているからです。.NETの[Data Binding]を信頼して使うのであればすべてがハッピーです。

私が[Data Binding]を嫌っているのは、(私が)この動きを十分にコントロールできないからです。私の予想を超えた動きをするので、「使えない」と(もしかして)勝手に思っているのです。また、[Data Binding]でFormatおよびParseを細かくプログラミングできない(と思っている)のも(しかも決定的)一因です。

[Data Binding]を使わないでデータをコントロールに表示するには、次のようにEntity(オブジェクト)のプロパティを一つずつコントロールのTextプロパティにアサインすることになります。

(いま、contextはEDMのインスタンス、BooksはこのEDMに含まれるEntitySet名、Title、AuthorおよびPriceはBook Tableの列名(=Entityのプロパティ名)とします。
また、画面にTextBox、txt書籍名、txt著者名およびtxt価格が配置されているとします。)

    Dim query = (From book In context.Books).First
    txt書籍名.Text = query.Title
    txt著者名.Text = query.Author
    txt価格.Text = query.Price

一般には画面には沢山のコントロールが配置されていますので、上のコードは書きたくありません。

ところでADO.NETでは次のように列名を文字列で書くことができます(いま検索した[DataRow]をdrとします)。

    txt書籍名.Text = dr("Title")
    txt著者名.Text = dr("Author")
    txt価格.Text = dr("Price")

したがって、コントロール[txt書籍名]と列名”Title”等のマップをあらかじめ作成しておけば、このマッピングを使ってコントロールのプロパティにアサインする仕組みを考えることができます。
例えば、このマッピングは9月18日、23日ブログで紹介しました[CtrlBindingCollection]を使います。

[CtrlBindingCollection]のメソッド[Display]を次のように定義します。
(正確ではありません。概念的な説明と理解してください。また以下で、[P_lst]、[P_Control]、[P_列名]はそれぞれ[m_lst]、[m_Control]、[m_列名]に対応したプロパティです。)

  Sub Display(ByVal dr As DataRow)
    For each c As Cell In P_lst
        c.P_Control.Text = dr(c.P_列名)
    End For
  End Sub

そうすれば、

    Dim CBC As New CtrlBindingCollection
    With CBC
     .Bind(txt書籍名, “Title”, “String”, True)
    .Bind(txt著者名, “Author”, “String”, True)
    .Bind(txt価格, “Price”, “Integer”, True)
    End With

(ここで、Bindメソッドの第一引数はコントロール、第二引数はそのコントロールに対応したDBテーブルの列名、第三引数はそのデータ型–説明のため”String”等の文字列で書いていますが、実際には独自に定義したデータ型のEnum値です–、第四引数はエンターキーで次のコントロールに移動するかどうかのフラグです)

を実行しておけば、

CBC.Display(dr)

でコントロールにデータを表示することができます(予めdrは読み込まれたDataRowだとします)。
逆にDB保存用のメソッドを作成することで、データのDBへの書き出しを一つのメソッドのコールで完了することができます。
この[CtrlBindingCollection]を十分練って汎用的にしておけば、このコレクションに登録したコントロールを一斉に操作することができます。

では、厳密に型指定されたオブジェクトを基本としているLINQではどうする。

先の例で、[txt書籍名]と [query.Title]とのマップを作ってみても何の役にも立ちません([txt書籍名]と dr(“Title”)
のマップを作っても役に立たないのと同じです)。
なぜなら、[query.Title]はインスタンスのプロパティですので(評価されて例えば”坂之上の雲”であり)、これと[txt書籍名]とのマップを保存してみても意味がないのです。

結論からいえば、解決策は[Programming Entity Framework]の17章に書かれています。

Entity Framework 奮闘記

LINQの勉強を始めたのは今年6月です。その後最新技法が[Linq to Entity]であると分かり、舵を[Entity Framework](以下EFといいます)の勉強に切り替えました。しかし[LINQ to Entity]の手ごろな教科書がありません。

LINQの勉強で4冊の本を買いました。

1. [プログラミング LINQ] Microsoft Press
2. [LINQ in Action] Manning
3. [Programming Entity Framework] O’Reilly
4. [murach’s ADO.NET 3.5 LINQ and the Entity Framework with VB 2008] Marach

2は教科書としてはよく書かれていますが、残念なことに2008年1月の出版ですので、昨年秋リリースされたEFの話題がありません。

1は和訳ですが、翻訳が悪いとの印象で必要な箇所の拾い読みをしています。原書は2008年の出版で、EFの解説が余りありません。訳書で付録としてEFの話題を追加していますが、どちらにしても入門程度の解説です。

4は各見開きの左ページは解説、右ページは表やコード、と徹底した構成になっていて、読みやすさを第一にしていますが、内容は入門程度です。

EFの解説書としては3が最適ですが、細かい字で構成も読みにくい。著者はおそらく教科書として書く気はまったくなく、「分かるやつは分かる」と至ってマイペースで書いていますので、私の力では800ページ弱のこの分厚い本を完全に理解するには半年程度必要かもしれません(当然勉強にいつまでも時間をとることはできません)。

しかし、考えてみればVisual Sdudioがもっともいい先生です。いつも文句も言わず根気よく私の解答の採点をしてくれます。ともかくプログラムを書いてVisual Sudioで格闘していれば大体話は分かってきます。

さて、こんな状態でともかくEFのプログラムを書いています。

LINQのいい点はDBアクセスプログラムが驚くほどが簡単に短くなったということ、
また、一度プログラムに読み込んだデータに対して、Queryを発行できますので(これはLINQの開発の目的のひとつでしょうが)、強力なデータ操作技術であることは間違いありません。

悪い点は(というより、困った点といった方がいいかも知れません)、一言で言えば「完成度が低い」ということです。
たとえば、ウィザードを使って[Entity Data Model](以下EDMといいます)を作成すると、データベースのテーブルに対応したクラスをプログラム上に作成します。この操作はデザイン画面で確認できますが、このモデルのメンテナンスがうまくない。
ウィザードで、あるテーブルをEDMに読み込んで、都合で削除したとします。再度そのテーブルをこのEMDに追加することができません。どうしてもこのEDMに入れたいのであれば、このEDMをゼロから再構築しなければいけません。これはこれで大騒ぎになります。

プロジェクトで一つのEDMだけ使うことにして全部のテーブルを読み込むのも一つの解決策かも分かりません。私は経験が少ないので、全部を一つのEDMにするのがいいかどうか分かりません。プロジェクトで数10以上のテーブルを使うのは珍しいことではないでしょう。
これを一度に一つのEDMにするのは、「パフォーマンスの点でどうなのか」、「大きなEDMのメンテナンスで手のつけられない大騒ぎになるのではないか」、逆にEDMを小割りにすると、EDM相互の関連が必要になったとき、また別の厄介な問題が発生し、どう処理するか。
どちらの道がいいのかまだ判断がつきません。

[LINQ to Entity]の勉強は何故か「しんどい」。たぶん理由のひとつは、[Object]とか[Entity]とか[Query]とかの普通名詞がいろいろ組み合わさって別の概念になっている。[ObjectEntity]とか[ObjectEnties]とか[ObjectEntry]とか[ObjectStateEntry]とか。何が何だか混乱します。

先にも書きましたが、現在もっともしっかり書かれた解説書は、Julia Lermanの[Programming Entity Framework] で、結局最後はこの本の解説を読むことになりますが、この本はEFの基本的枠組みの構成なりコンセプトなりのしっかりした解説をしないままに、多くの紙面を費やして様々な技術を解説していきますので、読むほうは「何がどうなっているのだ」としんどいばかりです。
来年になれば、もっといい教科書がでてくるのでしょうが、今はこの状態で進んでいくしかないのでしょう。

このブログでもEFをもう少し勉強して内容のあるお話を書ければと思います。

日本語入力でのエンターキーの処理

日本語入力でないことが分かっている場合、[Enter]キーが押されたら次のコントロールにフォーカスを移すプログラムは前回ご紹介しました。

日本語入力の途中では、これではいけません。
この場合の[キー]は、キーを押したとき発生する[KeyDown]、[KeyPress]、[KeyUp]イベントです。
(日本語入力でない)通常の場合は、[Enter]キーを押したときイベントハンドラーに送られてくる[KeyEventArgs]の[KeyValue]は[Keys.Enter]ですが、日本語入力の「途中では」(KeyUpイベント以外では)別のコードが送られてきます(この時のKeyValueの値が何であったか忘れました)。
日本語入力の途中で[Enter]を押して、漢字やひらがな等日本語を確認した後は、上でいう「通常の状態」になり、再度[Enter]を押すと[KeyDown]、[KeyPress]、[KeyUp]に[Keys.Enter]が送られてきます。

したがって、連続した[KeyDown]、[KeyPress]、[KeyUp]イベントで、何れも[Keys.Enter]が送られてくれば次のコントロールに移動し、それ以外は移動しないことにすればいいのです。

あとは、これをどのようにコード化するかです。

私たちはinteger二つが入るQueue(行列)を使いました。
[KeyDown]等のイベントが発生したとき、次々にこのキューに確認のコードを入れていきます。そして連続した[KeyDown]、[KeyPress]、[KeyUp]イベントが[Keys.Enter]を受け取ったことを確認すれば、コントロールを移動します。

Enum EnterKeyCode
    Down
    Press
    Up
    NG
End Enum

Public Class clsCheckEnterKey

    Private Shared que As New Queue(Of Integer)(2)

    Shared Sub TestEnter(ByVal sender As Object, ByVal KeyCode As Integer)
      If KeyCode = EnterKeyCode.Up Then
          If que.Count = 2 Then
              If que.Dequeue = EnterKeyCode.Down Then
                  If que.Dequeue = EnterKeyCode.Press Then
                      sender.findform.SelectNextControl(CType(sender, Control), _
                          True, True, True, True)
                   End If
              End If
          End If
      End If
      If que.Count = 2 Then
          que.Dequeue()
      End If
      que.Enqueue(KeyCode)
    End Sub
End Class

これをイベントハンドラーからコールする形にします。

    Public Shared Sub KeyDown _
	(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs)
        If e.KeyValue = Keys.Enter Then
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.Down)
        Else
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.NG)
        End If
    End Sub

    Public Shared Sub KeyUp _
	(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs)
        If e.KeyValue = Keys.Enter Then
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.Up)
        Else
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.NG)
        End If
    End Sub

    Public Shared Sub KeyPress _
	(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs)
        If Asc(e.KeyChar) = Keys.Enter Then
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.Press)
        Else
            clsCheckEnterKey.TestEnter(sender, EnterKeyCode.NG)
        End If
    End Sub

更に特定のコントロールにイベントハンドラーとして登録します。
このコードは(9月18日付けでご紹介しました)[CtrlBindingCollection]からコールします。

    Public Shared Sub SetEnterKeyHandler(ByRef ctrl As Control)
    AddHandler ctrl.KeyDown, AddressOf KeyDown
    AddHandler ctrl.KeyPress, AddressOf KeyPress
       AddHandler ctrl.KeyUp, AddressOf KeyUp
    End Sub

使い方は前回の単純なフォーカス移動のプログラムと同じです。

次のコントロールに移動するイベントハンドラ

テキストボックスにデータを入力し、[Enter]キーを押すと次のコントロールに移動する。という仕組みは頻繁に使います。

VB6(Visual Studio 6)では、フォームのデザイン時にこの動作を設定することができましたが、.NETではデザイン時でのこの設定がなくなっています(タブキーでの移動はあります)。

(以下のコードは実際に動作していますが、説明の邪魔になる部分を削除したり手を加えています。従ってこのままでは正しく動作しないことがあります。当ブログのコードは今後とも同様です。
掲載しますコードはベストだと思っているわけではありません。私たちの試行錯誤の結果、ともかく要求機能を満足するものです。
コードがもし読者の皆さんのお役に立つのであれば、コードのダウンロードを別途検討したいと思います。)

プログラムでコントロールの[KeyUp]イベントハンドラーで、[Enter]キーが押されたら、次のコントロールに移動するコードを書きます。

    Public Shared Sub SimpleKeyUp(ByVal sender As Object, _
                  ByVal e As System.Windows.Forms.KeyEventArgs)
        If e.KeyValue = Keys.Enter Then
	 Dim frm As Form = sender.findform
          Dim ctrl As Control = CType(sender, Control)
          Dim blnForwd As Boolean = True

          frm.SelectNextControl(ctrl, blnForwd, True, True, True)
        End If
    End Sub

このプログラムで注目すべきは、[sender](実はコントロール)から、それが配置されている[Form]を補足でき、このフォームで次の[TabIndex]のコントロールにフォーカスを移すことができることです(TabIndexの順番はデザイン時に設定します)。

そしてこのコードは、下のように当該コントロールに[KeyUp]イベントハンドラーとして登録します。

    Public Shared Sub SetSimpleEnterKeyHandler(ByRef ctrl As Control)

        If TypeOf (ctrl) Is TextBoxBase Then
            RemoveHandler CType(ctrl, TextBoxBase).KeyUp, AddressOf SimpleKeyUp
            AddHandler CType(ctrl, TextBoxBase).KeyUp, AddressOf SimpleKeyUp
        ElseIf TypeOf (ctrl) Is ButtonBase Then
            RemoveHandler CType(ctrl, ButtonBase).KeyUp, AddressOf SimpleKeyUp
            AddHandler CType(ctrl, ButtonBase).KeyUp, AddressOf SimpleKeyUp
        ElseIf TypeOf (ctrl) Is ListControl Then
            RemoveHandler CType(ctrl, ListControl).KeyUp, AddressOf SimpleKeyUp
            AddHandler CType(ctrl, ListControl).KeyUp, AddressOf SimpleKeyUp
        End If

        If TypeOf (ctrl) Is CheckBox Then
            RemoveHandler CType(ctrl, CheckBox).KeyUp, AddressOf ChkInstKeyUp
            AddHandler CType(ctrl, CheckBox).KeyUp, AddressOf ChkInstKeyUp
        End If
    End Sub

さらにこのコードは、前回ご紹介しました、[CtrlBindingCollection]クラスの[Cell]サブクラスのコンストラクターでコールされています。

その結果、

Dim CBC As New CtrlBindingCollection
With CBC
  .Bind(TextBox1, “氏名”, “String”, True)
  .Bind(TextBox2, “郵便番号”, “String”, True)
  .Bind(TextBox3, “住所”, “String”, True)
  .Bind(TextBox4, “年齢”, “int”, False)
End With
CBC.テキストクリア
CBC.Display(Datarow)

で[TextBox1]、[TextBox2]、[TextBox3]で[Enter]キーを押すと次のコントロールに移動するイベントハンドラーを割り当て、全コントロールをクリアし、それらのコントロールに[Datarow]の値をデータ型を考慮しながら表示することができます(もちろん別途Displayメソッドと、インスタンスDatarowを準備しておきます)。

ところで、日本語入力しているとき[Enter]キーを押すと直ちに次のコントロールに移動されては困ります。
日本語入力の途中では、通常ひらがなで入力して漢字変換が正しく表示されたところで、確定の[Enter]を押します。漢字入力はまだ続きますので、ここで次に移動したのではいけません。

次回はこれを正しく処理するコードをご紹介します。