VBAから見たVB.NETの使いやすさ

この章では、従来版VBVBAなどとの違いで懸案事項に揚がったことなどを説明します。
VB.NETの使用歴も10年以上になりました。   一方でExcelVBAも使うので、いつもその違いを意識するわけですが、 ここでは特にソースコードの記述上での違いや、コードエディタの操作性の違いなどについて説明します。



VB.NETの良いところ」という説明になっていますが、逆に言えば「VBAでは対応されない不満点」でもあります。



もくじ
ここでは、VB.NETにある程度「手慣れて」きた私から見てのVBAより使いやすいと思った点を説明します。
たくさんの項目ができてしまったので「もくじ」を作成しました。



配列で要素数「-1」でReDimができる
AndAlso、OrElseが使える
変数宣言行に直接初期値が投入できる
演算子に「+=」「&=」等が使える
コードエディタで予測補完が利用できる
コンパイルエラーの箇所はコンパイル(又は実行)しなくても判る
コードエディタに「ジャンプ」機能がある
モジュールでFriendスコープが使える
動的イベントが記述しやすい(AddHandler) ※特にコントロール配列時
ループ内で変数宣言ができて、ループ内スコープで動作する
クラスやModule保持変数は終了まで確保される
フォームの複数スクリーンでの表示位置制御が容易にできる
フォーム上でのコントロールの配置がやりやすい
DataGridViewというグリッドコントロールがある
カレンダーコントロール、ProgressBar等が標準で存在する(コントロールの種類が豊富)
ユーザーコントロールが作成できる
日付型の取り得る値の違いと精度(精度が高い)
Variant型のVB.NETでの処遇
修正プログラムの再配置に「ClickOnce」が使える
コメント行の先頭に「TODO:」を書くと「タスク」に一覧表示される
デバッグログやエラーメッセージにプロシージャ名が簡単に埋め込める
モジュールのインポートで「リンク参照」が使える

配列で要素数「-1」でReDimができる
これは私がExcelVBAで「一番不便に思っている」ことです。



配列は「Option Base」を記述しない限り「0」が最小の要素インデックスですが、 「初期化」というイメージからすると「空の配列」なのかどうかが判らないと困ります。
VB.NETでは「ReDim tblValue(-1)」と書くことができて、この時の「tblValue.Length」はゼロとなり「空の配列」だと判ります。 この時、最大要素インデックスは「tblValue.GetUpperBound(0) VBAだとUBound」ですが「-1」が返ります。
一方、ExcelVBAは「ReDim tblValue(-1)」と記述するのは「インデックスが有効ではない」のエラーになってしまい、初期化は「ReDim tblValue(0)」とせざるを得ません。 つまり「UBound(tblValue)」では「空の配列」なのか「1件格納された配列」なのかの区別が判らず、プロシージャ間で受け渡す場合に「最大要素インデックス」を別の引数で渡さなくてはなりません。 「0」が返ったときだけ内容があるかチェックするという方法もあるかも知れませんが、無駄に判断を追加して複雑にするだけだと思います。



今までやったことはなかったのですがExcelVBAで「Option Base 1」を書いたら解決できるのかと思って試しましたが、 これだと「ReDim tblValue(0)」も「インデックスが有効ではない」のエラーになってしまうので何の解決にもなりません。 できたとしても、他の開発言語と処置の基準が異なってしまうので、お勧めもできないことです。



2018年にこの記事を作成していますが、この年は以降の年で「天皇誕生日」が替わるとか、その行事で他の祝日も替わるとか、さらには東京五輪関連で祝日が替わるとかが話題になって、 本サイトではExcelVBAのソースコードに置いていた「祝日判定ロジック」をパラメータ化しそのパラメータを読んで祝日を判定する方法を発表しました。
まあ、祝日が1件もないとか、当月のカレンダーが1件もないとかは現実上はないのですが、配列自体では格納件数の判断ができないので、別の変数で「要素上限インデックス」を確保することとし、 プロシージャをまたぐ時は引数(またはプロパティ)にこれを加えています。

AndAlso、OrElseが使える
これは階層判断にしなければならないか、並列判断で済ませられるかということです。 VB.NETで改善された大きなポイントのひとつだと思います。
VB系言語の仕様では例えばAnd文で先頭の判断で「否」であっても後続の判断文は評価されてしまいます。 例えば「値がNullでなければ数値として比較する」という判断だとすると、単なるAnd文で接合した判断だと、Nullでも数値評価に回ってしまうので実行時エラーとなってしまいます。



ExcelVBAでプログラム作成されている方はこのことはご存じだと思いますから、 「Null比較」の判断文と「値比較」の判断文は階層を換えて(If文を2レベルに分けて)処理されていると思います。



一方、VB.NETでは「AndAlso、OrElse」がサポートされ、この判断文では先頭の判断でFalseになった時は並列に記述されている後方の判断は評価されないという特徴があります。

変数宣言行に直接初期値が投入できる
VB.NETでは変数宣言のその行で定数のように初期値投入ができます。逆に暗黙で初期化はされないので初期値投入せずに参照するとエラーになります。 以下は変数に初期値として「1」をセットするケースです。



[VBA]の場合

Dim intDay As Integer
intDay = 1



[VB.NET]の場合

Dim intDay As Integer = 1

演算子に「+=」「&=」等が使える
こんなようなことです。「翌日へ」のところに着目して下さい。(javaなどでは「当たり前」のようですが)



[VBA]の場合

Sub TEST()
    Dim intYear As Integer
    Dim intMonth As Integer
    Dim intDay As Integer
    intYear = Year(Date)
    intMonth = Month(Date)
    intDay = 1
    ' 今月の1日から末日までの日をループ
    Do While intDay <= Day(DateSerial(intYear, intMonth + 1, 0))
        Debug.Print intDay
        ' 翌日へ
        intDay = intDay + 1
    Loop
End Sub



[VB.NET]の場合

Sub Main()
    Dim intYear As Integer = Today.Year
    Dim intMonth As Integer = Today.Month
    Dim intDay As Integer = 1
    ' 今月の1日から末日までの日をループ
    Do While intDay <= Date.DaysInMonth(intYear, intMonth)
        Console.WriteLine(intDay.ToString)
        ' 翌日へ
        intDay += 1
    Loop
End Sub






データベース参照では、フィールド配置をソース上でコメントで示すために以下のように記述するようにしています。



[VBA]の場合

Dim strSQL As String                                            ' SQL文
' 祝日パラメータマスタを参照
strSQL = "SELECT [GETSU]"                                   ' (00)月
strSQL = strSQL & ",[SYORI_KBN]"                            ' (01)処理区分
strSQL = strSQL & ",[HIDUKE]"                               ' (02)[固定日]日
strSQL = strSQL & ",[FURIKAE_KBN]"                          ' (03)[固定日]振替
strSQL = strSQL & ",[SYUSU]"                                ' (04)[HM]週数
strSQL = strSQL & ",[YOBI]"                                 ' (05)[HM]曜日
strSQL = strSQL & ",[SYUKU_NM]"                             ' (06)祝日名
strSQL = strSQL & ",[STR_YEAR]"                             ' (07)開始年
strSQL = strSQL & ",[END_YEAR]"                             ' (08)終了年
strSQL = strSQL & g_cnsFROM & g_cnsZZZ_HOLIDAY
strSQL = strSQL & " WHERE [TOUROKU_KBN]<>9"
strSQL = strSQL & " ORDER BY [GETSU],CASE [SYORI_KBN] WHEN 2 THEN 0 ELSE [SYORI_KBN] END,[HIDUKE];"



[VB.NET]の場合

' 祝日パラメータマスタを参照
Dim strSQL As String = "SELECT [GETSU]"                     ' (00)月
strSQL &= ",[SYORI_KBN]"                                    ' (01)処理区分
strSQL &= ",[HIDUKE]"                                       ' (02)[固定日]日
strSQL &= ",[FURIKAE_KBN]"                                  ' (03)[固定日]振替
strSQL &= ",[SYUSU]"                                        ' (04)[HM]週数
strSQL &= ",[YOBI]"                                         ' (05)[HM]曜日
strSQL &= ",[SYUKU_NM]"                                     ' (06)祝日名
strSQL &= ",[STR_YEAR]"                                     ' (07)開始年
strSQL &= ",[END_YEAR]"                                     ' (08)終了年
strSQL &= g_cnsFROM & g_cnsZZZ_HOLIDAY
strSQL &= " WHERE [TOUROKU_KBN]<>9"
strSQL &= " ORDER BY [GETSU],CASE [SYORI_KBN] WHEN 2 THEN 0 ELSE [SYORI_KBN] END,[HIDUKE];"
&=」のあるなしでこのような違いになります。

コードエディタで予測補完が利用できる
コーディング中の入力補助についてです。



[VBA]の場合
メンバリスト表示
VBAでも「.」キー打鍵で「自動メンバ表示」によりプロパティリストが補助的に表示されます。



[VB.NET]の場合
メンバリスト表示
VB.NETの方は、途中まで入力すると「予測」的な動作で、宣言している変数(参照スコープ内)まで含めたリストが表示されます。

コンパイルエラーの箇所はコンパイル(又は実行)しなくても判る
コンパイルエラーの表示についてです。



[VBA]の場合
コンパイルエラー
VBAでは、コンパイルもしくは実行してみて初めてコンパイルエラーが表示され、それも1件ずつメッセージボックスで表示されるため、 そのメッセージボックスを閉じて修正し、再度コンパイルするという繰り返し作業になります。



[VB.NET]の場合
コンパイルエラー
VB.NETの方は、逐次バックグラウンドでコンパイルされ、エラー一覧にエラー内容が表示される上、エラー箇所は波線で表示されます。
このエラー一覧から、そのエラー箇所に進むこともできるので、デバッグ作業にはとても便利な機能です。

コードエディタに「ジャンプ」機能がある
「ジャンプ」というのは選択中のコードページの特定行にジャンプする機能です。
ジャンプ
これはVB.NETの方「ジャンプ」を指定したところの画面です。



VBAに関わらずVisualBasicではそもそも行番号を表示させていないので、行番号指定による「ジャンプ」は機能として外されてしまったのかも知れません。 また、VBAでは「ジャンプ」を必要とするほどの巨大なプログラムを作成することがあまりないということかも知れません。



ですが、例えば当サイトの「ダウンロード」にはVB.NETキーワード使用一覧」ExcelVBAキーワード使用一覧」 というものを用意しており、これはプロジェクト内にある指定キーワードがある箇所を別のExcelシ−トに一覧化させる機能です。 その指定キーワードがどのモジュールの何行目にあって、どのプロシージャでどのような記述で使われているかを11行で表示してくれるのですが、 VBEditor上で手っ取り早く「その行」に行きたい時に使うのがこの「ジャンプ」なのです。
ですが、残念ながらVBAの方のVBEditorにはこの機能がありません。

モジュールでFriendスコープが使える
実は私もVB.NETでプログラム作成を手掛けるまでは「Friend」スコープの存在を知りませんでした。(不勉強で...)
FriendスコープがVBAでもクラスであれば使えると知ったのは、さらにその数年後だったというおそまつなところもありました。
VB.NETではモジュールでもFriendスコープが使えるのですが、VBAではモジュールでFriendスコープが使えません。 プロジェクト内でもモジュールを超えて参照を許可する場合は、いきなりPublicスコープにしなければなりません。
これはセキュリティ上で問題なのかも知れません。(VBAでは「Option Private Module」で対応することになります)

動的イベントが記述しやすい(AddHandler) ※特にコントロール配列時
VBAのユーザーフォームでのコントロールの配列化については「ユーザーフォームのコントロールを配列にする。(クラス処理)で説明しています。



VB.NETだったらどうなるか、というサンプルを作成してみます。
ボタンを5つ配置してクリックイベントを1つにまとめるというものです。「Handles」を5行書いてしまうのではなく、予め配列にする方法とします。 コントロール名はデザイナの初期値のままなので、コードを貼り付けて確認できると思います。

'***************************************************************************************************
'   ボタンを5個配置したサンプル                                      Form1(Form)
'
'   作成者:井上治  URL:http://www.ne.jp/asahi/excel/inoue/ [Excelでお仕事!]
'***************************************************************************************************
Public Class Form1
    '===============================================================================================
    Private g_cnsButtonMax As Integer = 4                       ' ボタン配列要素上限
    Private g_tblButton() As Button                             ' ボタンの配列

    '***********************************************************************************************
    '* 処理名 :Form_Load
    '* 機能  :フォーム初期化
    '-----------------------------------------------------------------------------------------------
    '* 返り値 :(なし)
    '* 引数  :(既定)
    '***********************************************************************************************
    Private Sub Form_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        '-------------------------------------------------------------------------------------------
        ReDim g_tblButton(g_cnsButtonMax)
        ' ボタンを配列化する
        g_tblButton(0) = Button1
        g_tblButton(1) = Button2
        g_tblButton(2) = Button3
        g_tblButton(3) = Button4
        g_tblButton(4) = Button5
        '-------------------------------------------------------------------------------------------
        Dim intIx As Integer = 0                                    ' テーブルINDEX
        Dim intNo As Integer = 0                                    ' 表示
        ' Tagとイベントをセット
        Do While intIx <= g_cnsButtonMax
            intNo += 1
            ' Tagにbセット
            g_tblButton(intIx).Tag = intNo
            ' クリックイベントを実装
            AddHandler g_tblButton(intIx).Click, AddressOf Me.Button_Click
            ' 次へ
            intIx += 1
        Loop
    End Sub

    '***********************************************************************************************
    '* 処理名 :Button_Click
    '* 機能  :ボタンのクリックイベント
    '-----------------------------------------------------------------------------------------------
    '* 返り値 :(なし)
    '* 引数  :(既定)
    '***********************************************************************************************
    Private Sub Button_Click(ByVal sender As Object, ByVal e As System.EventArgs)
        '-------------------------------------------------------------------------------------------
        Dim intNo As Integer = CType(sender, Button).Tag            ' 表示
        MessageBox.Show(Me, "Button" & intNo.ToString & "がクリックされました。")
    End Sub

    '----------------------------------------<< End of Source >>------------------------------------
End Class
これはボタン5個を配置したフォームで、ボタンのクリックイベントを1つにまとめただけのサンプルですが、記述はこれだけです。
クリックされたボタンが「どれか」というのを表示名ではなく、予めTagに登録しておいた番号から表示させています。



モジュールに確保した「g_tblButton」のオブジェクトテーブルは、ボタンだとすると後からの利用シーンが考えにくいですが、 例えばテキストボックスだとすると、表示するための値をセットする処理や、項目条理チェック、ユーザーが入力した値の取り出しなどにもこのオブジェクトテーブルが利用できるというものです。

ループ内で変数宣言ができて、ループ内スコープで動作する
近年では、変数の宣言はなるべく極所で行なうべきとの思いから、ループ内でしか使用しない変数はループ内で宣言する以下のような使い方をすることがあります。 (よくやります)



[VBA]の場合

Sub TEST2()
    Const cnsUB As Integer = 2
    Dim intIx1 As Integer                   ' 上位INDEX
    Dim intCnt As Integer                   ' 連番
    Dim tblValue(cnsUB, cnsUB) As Integer   ' 2次元テーブル
    ' 上位ループ
    Do While intIx1 <= cnsUB
        Dim intIx2 As Integer               ' 下位INDEX
        ' 下位ループ
        Do While intIx2 <= cnsUB
            ' 連番を加算
            intCnt = intCnt + 1
            ' 連番を2次元テーブルにセット
            tblValue(intIx1, intIx2) = intCnt
            ' 次へ
            intIx2 = intIx2 + 1
        Loop
        ' 次へ
        intIx1 = intIx1 + 1
    Loop
End Sub
これは、3×32次元テーブルに連番を格納するという単純なサンプルです。



見ていただきたいのは、

    Dim intIx2 As Integer               ' 下位INDEX
がある場所です。上位ループの「中」で宣言しているわけです。(VBAでも宣言はできます)
上位ループ毎に宣言し直すから、当然暗黙の初期化で「ゼロに戻る」と思ってこのように記述しているのですが、これを動かすと思わぬ動作をします。 なんと、2順目の下位ループは3から始まってしまいます。



VB.NETであれば、そもそも初期化をせずに変数を参照することができないので、

    Dim intIx2 As Integer = 0           ' 下位INDEX
と記述するわけで、VBAのような問題にはなりません。以下のような記述になります。



[VB.NET]の場合

    Sub Main()
        Const cnsUB As Integer = 2
        Dim intIx1 As Integer = 0               ' 上位INDEX
        Dim intCnt As Integer = 0               ' 連番
        Dim tblValue(cnsUB, cnsUB) As Integer   ' 2次元テーブル
        ' 上位ループ
        Do While intIx1 <= cnsUB
            Dim intIx2 As Integer = 0           ' 下位INDEX
            ' 下位ループ
            Do While intIx2 <= cnsUB
                ' 連番を加算
                intCnt += 1
                ' 連番を2次元テーブルにセット
                tblValue(intIx1, intIx2) = intCnt
                ' 次へ
                intIx2 += 1
            Loop
            ' 次へ
            intIx1 += 1
        Loop
    End Sub
むしろ「下位INDEX」を「上位INDEX」と同じレベルで宣言して、「上位ループ」内の先頭でゼロクリアを忘れる方の危険性があるのかも知れません。

クラスやModule保持変数は終了まで確保される
VBAでは「保持されない」が「仕様」なのです。
クラスやModule保持変数は、一連のマクロ動作が完了した後は、そのワークブックを閉じるまで保持されているわけではありません。
これは実行時エラーの有無によるものでもありません。



次にマクロを起動した時に、モジュールレベルに「この変数が保持されているはずだ」としていきなり参照してしまうと、思わぬ結果を生んでしまいます。 但し「保持しているか」を判断するためのスイッチを同じモジュールレベルに配置しておけば、このスイッチも初期化されるので判断には使えると思います。



一方、VB.NETではモジュールレベル変数は再初期化するか終了するまで保持されます。

フォームの複数スクリーンでの表示位置制御が容易にできる
VBAのユーザーフォームの表示位置はデフォルトでは「オーナーフォームの中央」となっており、通常、単一スクリーンでExcelウィンドウしかない状態でこのユーザーフォームを表示させた時は正しく表示されると思います。
しかし、複数スクリーン環境ではExcelウィンドウを移動させた時などで、ユーザーフォームが別のスクリーンの端に表示されてしまうなどの現象が発生します。 このあたりはExcelのバージョンが上がっても何も改善されていません。



一方で、独自にユーザーフォームの表示位置を制御するのであれば、Office2007以降であれば可能となり、 当サイトではカレンダー入力用フォームで利用しています。
しかし、スクリーンごとのサイズなどが判断できておらず、そのユーザーフォームを表示した時にスクリーン内に収まっているのかが把握できていません。



VB.NETであればこれらは消化できると思います。

フォーム上でのコントロールの配置がやりやすい
この画像はVB.NETのフォームデザイナです。
フォームデザイナ
これは同じサイズのテキストボックスを均等に並べようとしているところですが、ドラッグさせていくとこのように両端に青い線が表示されて左右のズレがないことが判り、 また、上下も均等なピッチに配置されます。
VBAのユーザフォームでは「Top」や「Left」のプロパティで数値入力で調整していくところですが、 VB.NETではこのように簡単な上、コントロールを選択して四方矢印キーで微調整も行なえます。

DataGridViewというグリッドコントロールがある
VB.NETDataGridViewは非常に使い勝手の良いグリッドコントロールです。
配属一覧サンプル@
これは当サイト内のDataGridViewを利用したサンプルです。



VBAには現在、このようなものはありません。 昔は「スプレッドシートコントロール」というものがあったのですが、現在サポートされているOfficeでは動作しないようです。 しかも、上の画面のような用途に合うものではなかったようです。
おそらく「Excelにはワークシートがあるのだから、ユーザーフォームに表を表示させるような処置は必要ない」といったことなのではないでしょうか。



どうしてもExcelのユーザーフォーム上で「表」の表示を行ない、ユーザーが行を選択したことを通知するならリストボックスを使うしかありません。 リストボックスでも複数列の表示ができるので上記に近いものにはなりますが、列ごとの書式を登録するなどはできないので「簡易的処置」に止まってしまいます。

カレンダーコントロール、ProgressBar等が標準で存在する(コントロールの種類が豊富)
VB.NETであれば、カレンダーやProgressBarは標準コントロールなのですが、VBAにはこれらは存在しません。 このため、ユーザーフォーム上でこれらのものを利用するためには「似た機能を疑似的に作成する」必要があり、当サイトにも紹介しているもの(代替処置)があります。
「プログレスバーのクラス」
「カレンダー入力用フォーム」



VB.NETの方はこれ以外にもコントロールは多用にあります。

ユーザーコントロールが作成できる
VBAでは、上記で説明した「代替コントロール」もそうですが、実際にはツールボックスからドラッグさせるような「コントロール」にはなり得ていません。
最終的にはユーザーフォーム上で「それらしく」機能するように作成したというものです。
本来、ツールボックスからドラッグさせるような「コントロール」は、VBA上では作成することが原理的にできません。 VBAでは「コントロ−ル」の中にソースコードを書き込めないので仕方がないことです。



一方、VB.NETでは、VB.NETのプロジェクト内でフォーム上にドラッグさせて貼り付けられるユーザーコントロールを作成することができ、 何種類かは私も利用しています。
ユーザーコントロール側にイベント動作などのコードを実装できるため、例えば複数のフォームで利用する「ユーザー情報パネル」のようなものをユーザーコントロールとしておき、 プロパティで社員コードを渡すだけで部署・氏名・役職等を表示させるなどを行なうことができます。

日付型の取り得る値の違いと精度(精度が高い)
これは「できる」「できない」の話ではなく、VBAVB.NETとの「データ型」の違いの話です。



VBAの日付型は「8バイトの浮動小数点数型で、日付の値を日付型の整数部分で表し、時刻の値を小数部分で表します。整数部分は、日付型の開始日である1899年12月30日以降の日数を表します。」と説明されています。



VB.NETの日付型は「IEEE64ビット(8バイト)の値として格納され、西暦0001年1月1日から西暦9999年12月31日までの日付と、午前12:00:00(深夜)から午後11:59:59.9999999までの時刻を表します。」と説明されています。



この方法の違いは把握しておく必要があります。VBAの方は時刻が小数部分なので8:000.3333333333....になってしまい事実上、結局秒未満の精度がないのに対し、 VB.NETの方は、100ナノ秒単位の精度を持っているということです。



この取り得る値の違いは、VB.NETからExcelシートのセルに値転記する時に「悪さ」をすることがあります。
例えばVB.NET上で日付無しの「8:00」は実際は「西暦0001年1月1日 8:00」ですから、 Excelが「1899年12月30日以降」しか取り込めないことからエラーになります。
やるなら、この「差分」である「693593日」を加算してやる必要があります。

Variant型のVB.NETでの処遇
VBAや旧VBユーザーが、VB.NETに移行するのに弊害となっている項目に「Variant型がない」という意見があるそうです。 随分昔の記憶なので、今では解決していることかも知れませんが....
そこで、一応、「Variant型」の処遇について説明しておきます。



[VBA]の場合

Dim tblValue As Variant
tblValue = Array("AAA", "BBB", "CCC")
Array関数の例ですが、このように記述します。



[VB.NET]の場合

Dim tblValue() As String = New String() {"AAA", "BBB", "CCC"}
このように記述すれば良く「文字列型配列」と明示して宣言できます。
また、取り込む要素によってデータ型が不定となる場合は「Variant型」ではなく「Object型」を使用します。

修正プログラムの再配置に「ClickOnce」が使える
例えば「新しいプログラムをリリースしたので、最新バージョンをご利用下さい。」とアナウンスしても、各現場に配布してしまったプログラムが最新に置き換わる保証がない、 ということはこのサイト立ち上げの当初から「配布の問題」で訴えていることなのです。 特にExcelの場合は、マクロを仕込んだワークブックにマクロの仕掛けも含まれていると、マクロ部分だけを入れ替えることが困難です。
「配布の問題」では「主機能をアドインに移す」ことを提案していますが、


もっと究極なのはExcelワークブックにデータを保持させるのではなく、SQLServerなどのデータベースに格納するような方法に変更して、 これを扱うプログラムはVB.NETで作成するようにします。 このプログラムをClickOnceで配給するようにすれば、Webサーバ側のプログラムを入れ替えるだけで、 各PCにインストールされているプログラムが起動時に自動的に入れ替えられるという便利な仕組みになっています。

コメント行の先頭に「TODO:」を書くと「タスク」に一覧表示される
これは大きなプロジェクトを作る時にとても便利な機能です。VBAでそれほど大きなプロジェクトを作ることがないから「不要」と判断されているかも知れませんが。 ソースコード上に埋め込むコメントを以下のように記述すると、

' TODO: 第3引数は不要:使われていない(MdbConvert)
Visual Studioの「タスク一覧」には
タスク一覧
このように開いているソリューション内の各タスクコメントが一覧になって表示されます。
当然、この「タスク一覧」からその行をダブルクリックすれば、実ソースコードのその行にジャンプできるので 作業の「未完遂」についてこのようなコメントを残しておけば、未完了作業のところにすぐにたどり着けます。



VBAでは「TODO:」のコメントは書けますが、ひとつずつ検索で探るしか方法がありません。

デバッグログやエラーメッセージにプロシージャ名が簡単に埋め込める
細かいプロシージャを多数渡り歩くような処理を作成した場合、実行段階のエラー処理に困るようなことが発生します。 できればログやエラーメッセージにエラーが発生したプロシージャ名を埋め込みたいというようなことが起きると思います。
各プロシージャの先頭にそのプロシージャ名を定数で宣言すれば実現できるのでVBAではこのような方法を採るでしょう。

'**************************************************************************************************
' エラー発生時にエラーが起きたプロシージャ名をコンソールに表示させる例@
'**************************************************************************************************
Module Module1

    '**********************************************************************************************
    ' メイン処理
    '**********************************************************************************************
    Sub Main()
        '------------------------------------------------------------------------------------------
        Const cnsProcname As String = "Main"
        Try
            ' サブ処理@
            Call GP_Sub1()
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & cnsProcname)
        End Try
    End Sub

    '**********************************************************************************************
    ' サブ処理@
    '**********************************************************************************************
    Private Sub GP_Sub1()
        '------------------------------------------------------------------------------------------
        Const cnsProcname As String = "GP_Sub1"
        Try
            ' サブ処理A
            Call GP_Sub2()
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & cnsProcname)
        End Try
    End Sub

    '**********************************************************************************************
    ' サブ処理A
    '**********************************************************************************************
    Private Sub GP_Sub2()
        '------------------------------------------------------------------------------------------
        Const cnsProcname As String = "GP_Sub2"
        Try
            Err.Raise(1)
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & cnsProcname)
        End Try
    End Sub
End Module
ですが、各プロシージャに定数を配置しなければならないし、コピー・貼り付けなどでの「直し漏れ」などのミスもあり得ます。
そこで、VB.NETであれば、この定数に頼らず、エラー処理側で現在プロシージャ名をつかむことができます。

'**************************************************************************************************
' エラー発生時にエラーが起きたプロシージャ名をコンソールに表示させる例A
'**************************************************************************************************
Module Module2

    '**********************************************************************************************
    ' メイン処理
    '**********************************************************************************************
    Sub Main()
        '------------------------------------------------------------------------------------------
        Try
            ' サブ処理@
            Call GP_Sub1()
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & _
                              Reflection.MethodBase.GetCurrentMethod.Name)
        End Try
    End Sub

    '**********************************************************************************************
    ' サブ処理@
    '**********************************************************************************************
    Private Sub GP_Sub1()
        '------------------------------------------------------------------------------------------
        Try
            ' サブ処理A
            Call GP_Sub2()
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & _
                              Reflection.MethodBase.GetCurrentMethod.Name)
        End Try
    End Sub

    '**********************************************************************************************
    ' サブ処理A
    '**********************************************************************************************
    Private Sub GP_Sub2()
        '------------------------------------------------------------------------------------------
        Try
            Err.Raise(1)
        Catch ex As Exception
            Console.WriteLine(ex.Message & Strings.Space(1) & _
                              Reflection.MethodBase.GetCurrentMethod.Name)
        End Try
    End Sub
End Module
これなら、プロシージャ名の定数は要らないし、エラー処理側も同じ記述で現在プロシージャ名が取得できるので、 既存プロシージャからのコピー利用でも記述変更が要らないのでミスも防げます。

モジュールのインポートで「リンク参照」が使える
ソースコードの「世代管理」に関連する件です。
VBAで、たとえばいろいろな機能のマクロを作成(複数のワークブックに分散)しているとして、 おそらく共通記述になる部分を「共通モジュール」「共通クラス」にまとめるなどと言うことを考えると思います。



そうすると、同じ「共通モジュール」「共通クラス」を利用した機能ワークブックが多数作られるわけですが、 各ワークブックに分散した「共通モジュール」「共通クラス」が一元管理できるかというと、そうではありません。
同じ名前のモジュールであっても、ワークブックに持ち込まれた時点で別の「コピー」となってしまうので、 元々の「共通モジュール」「共通クラス」を変更した場合に連動して更新されるようなことはありません。



そこに着目するとVB.NETは非常に便利です。
リンクとして追加
作成済みの「共通モジュール」「共通クラス」をプロジェクトに追加させる場合にこのように「リンクとして追加」が選択できます。
これはプロジェクトの外部にあるフォルダからの追加も可能なので、複数プロジェクトから参照させる「共通モジュール」「共通クラス」をまとめて1つのフォルダに置いておくことが可能です。






リンクとして追加
「リンクとして追加」でインポートされた「共通モジュール」「共通クラス」はこのようにショートカットマークが付いたアイコンで表示されます。



「共通モジュール」「共通クラス」側に変更が入った場合は、それぞれの実行モジュールへの再ビルドは必要ですが、ソースコードのモジュールとしては一元管理できるので非常に便利です。



一方VBAでは、共通となるソースコードをワークブックの外側に置くこと自体ができません。
できるとすれば別のワークブックかアドインに共通機能だけを置いて呼び出す方法は考えられますが、 Runステートメントによる外部プロジェクト呼び出しなので、動作レスポンスに大きく影響することが考えられます。