web-dev-qa-db-ja.com

モジュールではなくユーザーフォームにコードを配置することに不利な点はありますか?

「通常の」モジュールではなくVBAユーザーフォームにコードを配置することには欠点がありますか?

これは簡単な質問かもしれませんが、webとstackoverflowを検索しているときに、それに対する明確な答えは見つかりませんでした。

Background: Excel-VBAでデータベースのフロントエンドアプリケーションを開発しています。別のフィルターを選択するには、別のユーザーフォームがあります。一般的なプログラム設計の方が良いと思います:(1)制御構造を別のモジュールに配置する OR (2)次のコードを配置するuserformまたはuserformのアクション

例を挙げましょう。フィルターとフォームをトリガーするActive-Xボタンがあります。

バリアント1:モジュール

コマンドボタンで:

Private Sub CommandButton1_Click()
  call UserInterfaceControlModule
End Sub

モジュール内:

Sub UserInterfaceControllModule()
Dim decisionInput1 As Boolean
Dim decisionInput2 As Boolean

UserForm1.Show
decisionInput1 = UserForm1.decision

If decisionInput1 Then
  UserForm2.Show
Else
  UserForm3.Show
End If

End Sub

バリアント1では、制御構造は通常のモジュール内にあります。また、次に表示するユーザーフォームに関する決定は、ユーザーフォームから切り離されています。次に表示するユーザーフォームを決定するために必要な情報は、ユーザーフォームから取得する必要があります。

バリアント2:ユーザーフォーム

CommadButtonで:

Private Sub CommandButton1_Click()
  UserForm1.Show
End Sub

Userform1の場合:

Private Sub ToUserform2_Click()
  UserForm2.Show
  UserForm1.Hide
End Sub

Private Sub UserForm_Click()
  UserForm2.Show
  UserForm1.Hide
End Sub

バリアント2では、制御構造は直接ユーザーフォームにあり、各ユーザーフォームにはその後の内容に関する指示があります。

私は方法2を使用して開発を開始しました。これが誤りであり、この方法に重大な欠点がある場合は、それよりも早く知りたいです。

免責事項私は 記事 ビクターK リンク先 と書きました。私はそのブログを所有し、それが対象としているオープンソースのVBIDEアドインプロジェクトを管理しています。

どちらの選択肢も理想的ではありません。基本に立ち返って。


異なるフィルターを選択するには、異なる(sic)ユーザーフォームがあります。

仕様では、ユーザーがさまざまなフィルターを選択できる必要があることを要求しており、UserFormを使用してUIを実装することを選択しました。これまでのところ、とても良い...そしてそれはそこからすべて下り坂です。

プレゼンテーションの懸念事項以外の原因となるフォームを作成することは、よくある間違いであり、その名前にはSmart UI[anti-] pattern、およびそれの問題はそれがスケールしないということです。これは、プロトタイピング(つまり、「うまくいく」ようにすばやく作業する-恐ろしい引用に注意してください)に最適であり、何年にもわたって維持する必要のあるものにはあまり適していません。

おそらくこれらのフォームを見て、160のコントロール、217のイベントハンドラ、および3つのプライベートプロシージャがそれぞれ2000行のコードで終了します。これはSmart UIのスケーリングが非常に悪く、それが唯一の可能性ですその道の結果。

ご覧のとおり、UserFormはクラスモジュールです。objectblueprintを定義します。オブジェクトは通常instantiatedになりたいですが、誰かが_MSForms.UserForm_のすべてのインスタンスに事前宣言されたIDを付与するという天才的なアイデアを持っていました。つまり、基本的に無料でグローバルオブジェクトを取得します。

すごい!番号?番号。

_UserForm1.Show
decisionInput1 = UserForm1.decision

If decisionInput1 Then
  UserForm2.Show
Else
  UserForm3.Show
End If
_

_UserForm1_が「X'd-out」の場合はどうなりますか?または、_UserForm1_がUnloadedの場合は?フォームがQueryCloseイベントを処理していない場合、オブジェクトは破棄されますが、これはdefault instanceであるため、VBAは直前に、自動的に/サイレントに新しいインスタンスを作成しますコードは_UserForm1.decision_を読み取ります-その結果、_UserForm1.decision_の初期グローバル状態は何でも取得できます。

それがdefault instanceではなく、QueryCloseが処理されなかった場合、破棄されたオブジェクトの_.decision_メンバーにアクセスすると、古典的なrun- nullオブジェクト参照にアクセスすると、エラー91が発生します。

_UserForm2.Show_と_UserForm3.Show_はどちらも同じことを行います:発火して忘れます-何が起こっても、それが何であるかを正確に知るには、フォームのそれぞれのコードでそれを掘り下げる必要があります後ろに。

言い換えると、フォームはshowを実行しています。彼らは、データを収集し、そのデータを提示し、ユーザー入力を収集し、そしてそれを使って実行する必要があるあらゆる作業を行う責任があります。これが「スマートUI」と呼ばれる理由です。UIはすべてを認識しています。

もっと良い方法があります。 MSFormsは.NETのWinForms UIフレームワークのCOM祖先であり、祖先がその.NET後継者と共通しているのは、有名なModel-View-Presenter(MVP )パターン。


モデル

それがdataです。基本的に、それはフォームからアプリケーションロジックが知る必要があるです。

  • _UserForm1.decision_それで行こう。

新しいクラスを追加し、それをFilterModelなどと呼びます。非常に単純なクラスである必要があります:

_Option Explicit

Private Type TModel
    SelectedFilter As String
End Type
Private this As TModel

Public Property Get SelectedFilter() As String
    SelectedFilter = this.SelectedFilter
End Property

Public Property Let SelectedFilter(ByVal value As String)
    this.SelectedFilter = value
End Property

Public Function IsValid() As Boolean
    IsValid = this.SelectedFilter <> vbNullString
End Function
_

必要なのはそれだけです。フォームのデータをカプセル化するクラスです。クラスは、いくつかの検証ロジックなどを担当できますが、collectデータではなく、ユーザーへのpresentではありません。consumeそれもしません。isデータです。

ここにはプロパティは1つしかありませんが、さらに多くのプロパティが考えられます。フォーム上の1つのフィールド=> 1つのプロパティと考えてください。

モデルは、フォームがアプリケーションロジックから知る必要があるものでもあります。たとえば、フォームにいくつかの可能な選択を表示するドロップダウンが必要な場合、モデルはそれらを公開するオブジェクトになります。


景色

それがあなたのフォームです。これは、コントロールについての知識、modelへの書き込みおよび読み取りを担当します。それがすべてです。ここではダイアログを見ています。それを表示し、ユーザーが入力して閉じ、プログラムがそれに作用します-フォーム自体はデータでdo何もしません収集します。モデルはそれを検証し、フォームは無効にすることを決定する場合があります Ok モデルがそのデータが有効で問題ないと言うまでボタンをクリックしますが、いかなる状況でもUserFormはワークシートから読み取りまたは書き込みを行います。データベース、ファイル、URLなど。

フォームのコードビハインドは非常に単純です。UIをモデルインスタンスに結び付け、必要に応じてボタンを有効または無効にします。

覚えておくべき重要なこと:

  • Hide、しないでくださいUnload:ビューはオブジェクトであり、オブジェクトは自己破壊しません。
  • [〜#〜]決して[〜#〜]フォームのdefault instanceを参照してください。
  • 常にQueryCloseを再び処理して、自己破壊的なオブジェクトを回避します(フォームの「Xアウト」はインスタンスを破壊します)。

この場合、コードビハインドは次のようになります。

_Option Explicit
Private Type TView
    Model As FilterModel
    IsCancelled As Boolean
End Type
Private this As TView

Public Property Get Model() As FilterModel
    Set Model = this.Model
End Property

Public Property Set Model(ByVal value As FilterModel)
    Set this.Model = value
    Validate
End Property

Public Property Get IsCancelled() As Boolean
    IsCancelled = this.IsCancelled
End Property

Private Sub TextBox1_Change()
    this.Model.SelectedFilter = TextBox1.Text
    Validate
End Sub

Private Sub OkButton_Click()
    Me.Hide
End Sub

Private Sub Validate()
    OkButton.Enabled = this.Model.IsValid
End Sub

Private Sub CancelButton_Click()
    OnCancel
End Sub

Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
    If CloseMode = VbQueryClose.vbFormControlMenu Then
        Cancel = True
        OnCancel
    End If
End Sub

Private Sub OnCancel()
    this.IsCancelled = True
    Me.Hide
End Sub
_

それが文字通りフォームのすべてです。 データがどこから来たのか、それをどうするのかを知る責任はありません


プレゼンター

それがドットをつなぐ「接着剤」オブジェクトです。

_Option Explicit

Public Sub DoSomething()
    Dim m As FilterModel
    Set m = New FilterModel
    With New FilterForm
        Set .Model = m 'set the model
        .Show 'display the dialog
        If Not .IsCancelled Then 'how was it closed?
            'consume the data
            Debug.Print m.SelectedFilter
        End If
    End With
End Sub
_

モデル内のデータがデータベースまたはワークシートから取得する必要がある場合は、それだけを行うクラスインスタンス(はい、anotherオブジェクト!)を使用します。

呼び出しコードは、ActiveXボタンのクリックハンドラで、プレゼンターをNew- ingし、そのDoSomethingメソッドを呼び出すことができます。


これは、OOPについて知っていることのすべてではありません(インターフェイス、ポリモーフィズム、テストスタブ、ユニットテストについては触れていません))が、客観的にスケーラブルなコードが必要な場合は、 MVPのうさぎの穴を掘り下げ、真にオブジェクト指向のコードがVBAにもたらす可能性を探りたいと思います。


TL; DR:

コード(「ビジネスロジック」)は、フォームのコードビハインドでは、belongではなく、数年にわたって拡張および維持されることを意味するコードベースではありません。

「バリアント1」では、モジュール間を行き来し、プレゼンテーションの懸念がアプリケーションロジックと混在しているため、コードを追跡するのは困難です。指定されたボタンAまたはボタンBを表示するために他のどのフォームが押されたかを知るのはフォームの仕事ではありません。代わりに、それはpresenterにユーザーが何をすべきかを知らせ、それに応じて行動するべきです。

「バリアント2」では、すべてがユーザーフォームのコードビハインドに隠されているため、コードを追跡するのは困難です。そのコードに掘り下げない限り、アプリケーションロジックがわからないので、purposelyプレゼンテーションとビジネスロジックの問題が混在しています。それはexactly「スマートUI」アンチパターンが行うことです。

つまり、バリアント1はバリアント2よりもわずかに優れています。これは、少なくともロジックがコードビハインドに含まれていないためですが、runningの代わりに/ **/runningであるため、依然として「スマートUI」です。何が起こっているのかを発信者に伝える

どちらの場合も、フォームのデフォルトインスタンスに対するコーディングは有害です。これは、状態をグローバルスコープに配置するためです(誰でもデフォルトインスタンスにアクセスし、コード内のどこからでもその状態に対して何でも実行できます)。

フォームをオブジェクトと同じように扱います。インスタンス化してください!

どちらの場合も、フォームのコードはアプリケーションロジックと緊密に結びついており、プレゼンテーションの懸念と絡み合っているため、現在の状況の1つの側面さえカバーする単一の単体テストを書くことは完全に不可能です。 MVPパターンを使用すると、コンポーネントを完全に分離し、インターフェースの背後でそれらを抽象化し、責任を分離し、すべての機能をカバーし、仕様を正確に文書化する数十の自動ユニットテストを記述できます。コードは独自のドキュメントになります

49
Mathieu Guindon