C# と VB.NET の質問掲示板

ASP.NET、C++/CLI、Java 何でもどうぞ

C# と VB.NET の入門サイト

Re[19]: COMオブジェクトで起動したExcelの印刷を行うと例外発生 [1]


(過去ログ 176 を表示中)

[トピック内 26 記事 (21 - 26 表示)]  << 0 | 1 >>

■101090 / inTopicNo.21)  Re[16]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
  
□投稿者/ KOZ (382回)-(2023/01/06(Fri) 00:16:29)
No101088 (ジェイド さん) に返信

ログを見ると、System.Windows.Forms.Control.CreateHandle() の中で異常終了しているので、
コンストラクタ(Sub New) の中で Close しているのではないかと思われます。
Load イベントまで来てれば、ログの出方が違うはずですが、提示された部分は本当に動いてますか?

情報を小出しにしても解決しません。
コードを削っていき、現象がおきる最低限のコードを提示してください。

引用返信 編集キー/
■101091 / inTopicNo.22)  Re[16]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
□投稿者/ 魔界の仮面弁士 (3521回)-(2023/01/06(Fri) 10:14:40)
2023/01/06(Fri) 11:47:44 編集(投稿者)

No101088 (ジェイド さん) に返信
> フォームのLoad処理で、以下のようにしておりました。
> AddHandler Timer.Tick, AddressOf Timer_Tick

System.Windows.Forms.Timer のようですね。
https://atmarkit.itmedia.co.jp/ait/articles/0511/11/news117.html

ここで、わざわざ AddHandler する意図が見えませんでした。VB から利用するのなら、コンポーネントとして
デザイン時に貼り付けたうえで、WithEvents + Handles で利用するのが一般的だと思うので。

AddHandler が悪いというわけでは無いですが、わざわざ面倒な手続きを踏む必要があるとすれば、
RemoveHandler による解除と組み合わせる必要がある場合(初回しか実行しないなど)や、
あるいは Publisher-Subscriber パターンでマルチキャストデリゲートを用いるケースぐらいかな、と。


> こちらですが、サービス処理のタイムアウト対策目的で呼び出しているようです。
System.Windows.Forms.Timer が、Windows Forms から呼ぶ際に問題になることはほとんどありません。
ただし常に呼び出し元の UI スレッドで動くため、Tick イベント内で
長い待機処理が入ると、フォーム側のメッセージ処理が詰まってしまうという
制限があります。

一方、System.Threading.Timer や System.Timers.Timer は、ワーカープロセスとして
実行されるものであるため、比較的長い待機処理でも耐えられます。
https://atmarkit.itmedia.co.jp/ait/articles/0511/11/news118.html
https://atmarkit.itmedia.co.jp/ait/articles/0511/11/news119.html
ただしこれらはマルチスレッドとなるため、Windows Forms から利用する場合は
UI 側のスレッドが管理する変数やコントロールとやり取りする際に、あらかじめ
適切な排他制御を考慮した設計が必要になるなど、実装難易度が幾分高くなります。
(マルチスレッドを考慮していないプログラムを、後から修正していくのはかなり大変です)


>>先のエラーメッセージで、CreateHandle 中の ObjectDisposedException 例外が見えているので、
>>「待機処理を Sub New で行っているのではあるまいか…」という疑念です。
>>要するに、何らかのロード処理(OnLoad メソッドのオーバーライド、または Load イベントの処理)が
>>終わっていない状況で Form が破棄されていて、それがエラーの原因になったのではないか、と。
> フォームのLoad処理は、以下の内容になっているのですが・・・

『Load 処理』ではなく、その前の『コンストラクタ部(VB でいえば Sub New のこと)』を心配しています。
例えば、新規プロジェクトの Form1 に対して、
下記 3 パターンのうち、いずれかのコンストラクタを設けたとしましょう。

 Public Sub New()
  Close()
 End Sub

 Public Sub New()
  InitializeComponent()
  Close()
 End Sub

 Public Sub New()
  Close()
  InitializeComponent()
 End Sub


これを実行すると、Close 処理によって、下記の ObjectDisposedException 例外が誘発されます。
No101071 と完全同一ではないにせよ、よく似た内容になっていますね。

  at System.Windows.Forms.Control.CreateHandle()
  at System.Windows.Forms.Form.CreateHandle()
  at System.Windows.Forms.Control.get_Handle()
  at System.Windows.Forms.Control.SetVisibleCore(Boolean value)
  at System.Windows.Forms.Form.SetVisibleCore(Boolean value)
  at System.Windows.Forms.Control.set_Visible(Boolean value)
  at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
  at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
  以下略

ただし、スタートアップを Form1 から Sub Main に変更したうえで、Main メソッドを
  Dim f As New Form1()
  If Not f.IsDisposed Then
    Application.Run(f)
  End If
のように変更していた場合には、コンストラクタ内で Close されたとしても、
上記の ObjectDisposedException 例外を免れることができます。(同一スレッドからの判定であれば)


> この作りに問題があるのでしょうか?
コンストラクタ同様、Load 処理内で Close するのも、設計としてはあまり良くないですね。

コンストラクタ内で Close するよりは幾分マシなのですが、
「開こうとしている途中で取り消す」のはイレギュラーなので、
可能であれば「そもそも開かない」か、
 (A) 開いても問題ないかどうか、事前に判断する(Shared メソッドにするなど)
または「開き終わってから閉じる」
 (B) 非表示のままロードし、Load し終わってからの処理で Close する
にすることを検討した方が安全だと思います。


で…そもそも、この Form の役目が何であるのかが見えてきません。

FormClosing やメッセージングなど、Form が必要になるケースはあるので
存在に意味がないとは言いませんが、意図が見えないということです。


たとえば現在の監視フォームがスタートアップフォームであり、事前判定が必要なら、
プロジェクトのプロパティの[アプリケーション]タブの[アプリケーション イベントの表示]で
MyApplication の Startup イベントで判断し、e.Cancel = True/False で
スタートアップ フォームのロード可否を問うことができます。(あるいは Sub New を使うとか)

スタートアップフォーム以外なのであれば、Excel 印刷終了を待機させるフォームの役割と
その呼び出し元が、該当フォームをどのように呼び出しているのかを明らかにしてください。


> Try
>  If LoadProcess(strErrMsg) = False Then
>   If Not String.IsNullOrEmpty(strErrMsg) Then ShowMessage(strErrMsg, MessageBoxIcon.Exclamation)
>   Return
>  End If
> Catch ex As Exception
>  'エラーが発生しました。
>  ShowMessage(ex.Message, MessageBoxIcon.Exclamation)
> Finally
>  Try
>   Me.Close()
>  Catch ex As Exception
>   MsgBox(ex.Message, MessageBoxIcon.Exclamation)
>  End Try
> End Try

Catch ex As Exception というのは大雑把すぎるので、例外処理の実装としては
不適切なのですが、それは今回目を瞑るとして。

(1) ex.Message では、詳細情報が取れません。画面表示のための簡易情報として使う程度ならばよいですが、
 それでは障害記録のためには不十分です。例外クラスの種類や StackTrace 等も重要な情報となりますので、
 ex.ToString() をロギングしておいた方が良いかと思います。
 (Exception を詳細に記録してくれるロガークラスを導入するのも一つの手です)


(2) 外側の Catch 句は、何のエラーを拾うためのものですか?
 String.IsNullOrEmpty が失敗する事態は考えにくいので、
 LoadProcess または ShowMessage を対象にしているものと推察しますが、
 これらのメソッドは、具体的にどのような例外を吐くことが想定されているのでしょうか。


(3) 内側の Catch 句は、何のエラーを拾っていますか?
 この Catch の仕方は、「Close メソッドが失敗した場合」は拾えますが、
 「Close したことによって引き起こされた問題」は拾えないことに注意してください。
 なお公式資料によれば、Close メソッドによって引き起こされる例外としては、
  InvalidOperationException:ハンドルが作成されている間にフォームが閉じられた場合
  ObjectDisposedException:最大化(WindowState = Maximized)状態で Activated イベント内で Clsoe を呼び出した場合
 の 2 パターンが記載されていました。ただし後者は Activated イベント内で呼ばれた場合を対象としたものですし、
 不適切なタイミングで Close を呼び出したことによって、本来の処理が失敗しまうという類の例外です。
 https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form.close?view=netframework-4.8.1
引用返信 編集キー/
■101123 / inTopicNo.23)  Re[17]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
□投稿者/ ジェイド (25回)-(2023/01/07(Sat) 19:00:55)
No101090 (KOZ さん) に返信
> ■No101088 (ジェイド さん) に返信
>
> ログを見ると、System.Windows.Forms.Control.CreateHandle() の中で異常終了しているので、
> コンストラクタ(Sub New) の中で Close しているのではないかと思われます。
> Load イベントまで来てれば、ログの出方が違うはずですが、提示された部分は本当に動いてますか?
>
> 情報を小出しにしても解決しません。
> コードを削っていき、現象がおきる最低限のコードを提示してください。
>

すみません、遅くなりました。
当フォーム内をGrepしてみたのですが、Sub New は存在していませんでした。
フォームの Load時に、frmStartUp_Load があてがわれています。

frmStartUp_Load内で、Closeしている(Loadが終了していないのにCloseしようとしている)
から問題のように感じています。

すみません、LoadProcess内で、ファイルをダウンロードしたり、ごちゃごちゃやっているところがありまして・・・
この中の一部に、KOZさんに直して頂いたコードが入っております。
コードを削るのは、なかなか難しく申し訳ございません。。。

引用返信 編集キー/
■101125 / inTopicNo.24)  Re[17]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
□投稿者/ ジェイド (26回)-(2023/01/07(Sat) 19:19:33)
No101091 (魔界の仮面弁士 さん) に返信
> 2023/01/06(Fri) 11:47:44 編集(投稿者)

色々ご指摘いただきありがとうございます。

> 『Load 処理』ではなく、その前の『コンストラクタ部(VB でいえば Sub New のこと)』を心配しています。

何度も見返しているのですが、Sub New がないのです。
(これがそもそも問題なのでしょうか・・・)

> コンストラクタ同様、Load 処理内で Close するのも、設計としてはあまり良くないですね。

当プログラムなのですが、メインプログラムからプロセス起動されるサブプログラムでして、
表に出ない仕様となっています。

プロセス起動された後は、画面上では表示されておらず、タスクマネージャ上にタスクとして
表示されているだけ、になっております。

なので、Loadの中で、Close しているのか、、、とも考えていたりします。
(すみません、前任者が作成したものを修正しているので詳しい仕様も分かっていない状況です)

> スタートアップフォーム以外なのであれば、Excel 印刷終了を待機させるフォームの役割と
> その呼び出し元が、該当フォームをどのように呼び出しているのかを明らかにしてください。

当フォームはスタートアップフォームに指定されています。
もう一つ、ダイアログフォームが存在するのですが、これは LoadProcess関数内で一度だけ表示するようになっています。

frmStartUp_Load 内で、LoadProcess関数の呼び出し以降(Close含む)を抜き出そうと考えています。

ただ、Load後にどうやって流そうかと考えているのですが、、、
Timerを使って(例えば、1秒後に LoadProcess関数を実行)やるしかないのかなと思っているのですが
何か他にいい方法がございますでしょうか。

例外対応のご指摘もありがとうございます。
こちらも見直しを検討しております。
引用返信 編集キー/
■101131 / inTopicNo.25)  Re[18]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
□投稿者/ ジェイド (27回)-(2023/01/07(Sat) 23:08:41)
皆さんに教えて頂いた内容を元に以下のような対応をしてみました。

1.Public Sub New を新設
  frmMain_Load内にあった AddHandler Timer.Tick, AddressOf Timer_Tick を移動

2.Private frmMain_Load の中身を修正
 フォームを非表示で開く

3.Private frmMain_Shown を新設
 元々、frmMain_Load内に書いてあった内容を移動(※一部修正してます)

これで、実行した結果、修正前と動きが変わらずかつ、イベントビューアにもエラーがあがらなくなったことを確認しました。

これにて解決にさせていただきたいと思います。
ありがとうございました。

-------------------------------------------------------------------------------------------------------------------
    Public Sub New()

        ' この呼び出しはデザイナーで必要です。
        InitializeComponent()

        AddHandler Timer.Tick, AddressOf Timer_Tick

        ' InitializeComponent() 呼び出しの後で初期化を追加します。

    End Sub



    Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

        Me.Visible = False
        Me.Show()

    End Sub



    Private Sub frmMain_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown

        Dim strErrMsg As String = ""
        Dim curSave As Cursor

        '保存
        curSave = Cursor.Current

        '砂時計
        Me.Cursor = Cursors.WaitCursor

        Try
            If LoadProcess(strErrMsg) = False Then
                If Not String.IsNullOrEmpty(strErrMsg) Then
                    ShowMessage(strErrMsg, MessageBoxIcon.Exclamation)
                End If
            End If

        Catch ex As Exception
            'エラーが発生しました。
            ShowMessage(ex.ToString, MessageBoxIcon.Exclamation)
        Finally

            '保存していたカーソルを戻す
            Me.Cursor = curSave
            Me.Close()

        End Try

    End Sub


解決済み
引用返信 編集キー/
■101142 / inTopicNo.26)  Re[19]: COMオブジェクトで起動したExcelの印刷を行うと例外発生
□投稿者/ 魔界の仮面弁士 (3529回)-(2023/01/09(Mon) 12:55:54)
No101131 (ジェイド さん) に返信
> 皆さんに教えて頂いた内容を元に以下のような対応をしてみました。
> これにて解決にさせていただきたいと思います。
解決済みチェックは付けたままにしておきますが、幾つか気になった点をひとりごちてみるなど。
(主問題は解決済みのようなので、この投稿は無視して頂いていも構いません)


■No101125 (ジェイド さん) に返信
> ただ、Load後にどうやって流そうかと考えているのですが、、、

そもそも、表示させたくないフォームに判断処理を書いていることが、
根本的な要因であるような気もしなくもなかったり。

それはさておき、イベント処理後に何かしたい場合には、
「Application の Idle イベント」を使うという手もあります。
https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.application.idle?view=netframework-3.5

Protected Overrides Sub OnLoad(e As EventArgs)
 MyBase.OnLoad(e)
 
 'Load されてアイドリング状態になった時に、AutoClose が呼び出されるようにする
 AddHandler Application.Idle, AddressOf AutoClose
End Sub

Private Sub AutoClose(sender As Object, e As EventArgs)
 '一度しか処理したくないので、初回実行時に解除する
 RemoveHandler Application.Idle, AddressOf AutoClose
 
 '一度だけ実行させたい処理
 Me.Close()
End Sub


> 当プログラムなのですが、メインプログラムからプロセス起動されるサブプログラムでして、
> 表に出ない仕様となっています。
表示されていないという事は、終了ボタンとかも無いという事ですよね…。
それを終了する手段は、サブプログラム自身が自殺することだけだったりするのかな?
(あるいはタスクマネージャーからの強制終了とか)


常時非表示なのであれば、サブプログラム側は Form は一切使わずに、
Sub Main() 起動の実装で十分なのでは…とも思っていたのですが、
あえて Form を残した実装にしているのは、前任者(あるいはさらにその前の設計者)に、
何かしらの意図があったのかもしれないのが悩ましいところ。
コードを見ていない第三者側で判断できることでは無いですけれど。

たとえば、メインプログラム側から終了コマンドを受信できるような仕組みが用意されている場合、
非表示フォームを使った方がやりとりしやすいですし、Windows のシャットダウン通知を
捉えたい場合も、Form の FormClosing のイベント引数を用いて判断できるといったメリットがあるかも。

ただ No10191 でも述べたように、Load 中に Close する実装というのは少々乱暴かな…と思わなくもない。
「Form を開く前に開くかどうかを先に判断し、開いても良い場合だけ Form を呼び出す」か、
「Close するのは、非表示フォームのロードが終わった後にする」方が望ましいかと。
https://atmarkit.itmedia.co.jp/bbs/phpBB/viewtopic.php?topic=33492&forum=7


COM オブジェクトを操作する場合、メッセージループが必要になることもありますが
その場合は ApplicationContext を用いることもできるかと。
https://smdn.jp/programming/netfx/fcl/System.Windows.Forms.ApplicationContext/

ApplicationContext の代わりに非表示フォームを使っても実装できるでしょうけれども、
いずれにせよ、一つのイベントが長時間実行されるようなことが無いような作りにすべきでしょうね。

たとえば Process の終了待ち処理なら、 WaitForExit メソッドは使わずに
Process の Exited イベント(と SynchronizingObject プロパティ)を使うか、
終了待ちを他のワーカースレッドに行わせるなど。
https://dobon.net/vb/dotnet/process/openfile.html



>>『Load 処理』ではなく、その前の『コンストラクタ部(VB でいえば Sub New のこと)』を心配しています。
> 何度も見返しているのですが、Sub New がないのです。
> (これがそもそも問題なのでしょうか・・・)

VB2019 の場合、特に明示しない限りは Form に Sub New() が書かれないのが普通なので、
無いなら無いで問題無いですね。

VB でコンストラクタを明示的に用意する場合は、コードウィンドウ上部のドロップダウンリストで、
クラス一覧から [Form1]を選択し([Form1 イベント]ではない)、
右上からは [New]を選択(バージョンによっては [新規作成]を選択)します。


また、クラスにコンストラクタの宣言が一切無い場合、そのクラスにはコンパイル時に
「引数無しの public なコンストラクタ」が自動生成される仕様です。

そして VB2005 以降で新規 Form を追加した場合は、フォーム名.designer.vb 側のコードに対して
 <Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
または
 <Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
という属性が付与されるようになっています。これが付いていると、自動生成されたコンストラクタ内に、
下記のような「InitializeComponent() メソッドの呼び出し」が自動挿入される仕様です。

 Public Sub New()
  'デザイン時の設定(フォームの初期サイズやコントロールの配置など)を呼び出す
  InitializeComponent()
 End Sub



これは Form 継承クラスに限った話ではなく、任意の自作クラスにおいても当てはまります。
コンストラクタが未定義で、かつ DesignerGenerated 属性が付与されたクラスの場合、
「InitializeComponent という名前(大文字小文字は区別しない)で、引数の無いメソッド」
を呼び出すためのコンストラクタが自動生成されます。
※ InitializeComponent が定義されていない場合は、空のコンストラクタが生成されます。

Public Partial Class Form1
  <Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _
  Class Example
    Private Sub InitializeComponent()
      MsgBox("自動生成")
    End Sub
  End Class
  Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim a As New Example() 'メッセージボックスが表示される
  End Sub
End Class

※Microsoft の .NET コンパイラー "Roslyn" における InitializeComponent の自動挿入処理
https://sourceroslyn.io/#Microsoft.CodeAnalysis.VisualBasic.EditorFeatures/Utilities/NamedTypeSymbolExtensions.vb

※Mono の VB.NET コンパイラーにおける InitializeComponent の自動挿入処理
https://github.com/mono/mono-basic/blob/bdb5276f7d85100e8e9ddd7e5ba2360a792644a9/vbnc/vbnc/source/TypeDeclarations/ClassDeclaration.vb#L145-L157


ちなみに C# のコンパイラにはこの仕組みがありません。 C# で Form を追加した場合には、
VB とは違って、最初からコンストラクタが記述されたファイルが追加されるようになっています。

また、C# で Form のコンストラクタをすべて消した場合、「public な引数無しコンストラクタ」が
追加されるまでは同じなのですが、InitializeComponent を呼び出す処理は自動追加されません。



■No101131 (ジェイド さん) に返信
> Public Sub New()
>  ' この呼び出しはデザイナーで必要です。
>  InitializeComponent()
>  AddHandler Timer.Tick, AddressOf Timer_Tick
>  ' InitializeComponent() 呼び出しの後で初期化を追加します。
> End Sub

今回の場合、わざわざ引数無しコンストラクタを追加する必要は無かったような。

それはさておき、Timer_Tick の定義が次のうちのどちらだったのかが気になっていたり。
(1) 『Private Sub Timer_Tick(sender As Object, e As EventArgs) Handles Timer.Tick』
(2) 『Private Sub Timer_Tick(sender As Object, e As EventArgs)』

通常は前者のはずで、その場合はそもそも AddHandler を使うべきでは無いんですよね。
イベントが多重呼び出しになってしまう可能性が高まるので。

また、「Friend WithEvents Timer As Timer」のような宣言になっている場合、
AddHandler を使わずに (1) だけで記述することをお奨めしています。逆に、(2) でコーディングするなら
その変数の定義は WithEvents にしない方が良いでしょう(混乱のもとなので)。


> Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
>  Me.Visible = False
>  Me.Show()
> End Sub
個人的には、これもあまり良い回避策とは思えなかったり…。

「Hide() / Show()」か「Visible = False / Visible = True」かという対比性はさておき、
「非表示化」→「別処理」→「表示化」ならまだしも、
「非表示化」→「表示化」というコードが、コメント説明すらなく並んでいると、
後で保守する後任の担当者が、意図を読み取れずに削除してしまう恐れがありそうで。

そもそも Shown で常に Close させるのなら、表示する意味がないですし、
そもそも Visible = False が要るのか?  という話になって、後から読む人が混乱するんじゃないかと。

「非表示が前提」のフォームなのに、一瞬とはいえ、一時的に表示させることが許容されているのかどうか、という疑念も。
エラーメッセージの表示や、選択ダイアログを表示するといった目的がある場合は良いでしょうけれども、
即時終了を目的として表示するというのは、処理として不自然では無いだろうかと。
たとえばそれによって、フォーカスの遷移が起きるなどといった問題が起きるかも知れないわけで。


> If LoadProcess(strErrMsg) = False Then
>  If Not String.IsNullOrEmpty(strErrMsg) Then ShowMessage(strErrMsg, MessageBoxIcon.Exclamation)
>  Return
> End If
ここも微妙に違和感が。

たとえば上記の処理なら、
 Dim strErrMsg As String = LoadProcess()
 If Not String.IsNullOrEmpty(strErrMsg) Then
  ShowMessage(strErrMsg, MessageBoxIcon.Exclamation)
  Return
 End If
となるよう設計の方が素直に思えます。

殆どの場合、「ByRef な引数」は使用しない方が良いので、情報を戻り値で返すことを検討すべきかな、と。

出力引数が必要になるのは、 P/Invoke の時か Try何某 系のメソッドぐらいのものでしょう。
(VB6 からマイグレーションしたコードだと、ByRef だらけになってしまうのですが)

下記は .NET のデザインガイドにおける記述ですが、パラメーター設計においては
 「AVOID using out or ref parameters.」
 「out または ref パラメーターは使用しないようにしてください。」
と明記されています。なお、 C# における out や ref とは、Visual Basic における ByRef キーワードに相当します。
https://learn.microsoft.com/ja-jp/dotnet/standard/design-guidelines/parameter-design


ところで、この LoadProcess の処理って、どのぐらいの時間続くものなのでしょうかね。

数十ミリ秒程度であれば良いですが、数秒以上かかるのであれば、非同期パターンで実装して、
その処理を別スレッドで行わせることを検討した方が良いかと思います。
https://learn.microsoft.com/ja-jp/dotnet/standard/asynchronous-programming-patterns/

解決済み
引用返信 編集キー/

<前の20件
トピック内ページ移動 / << 0 | 1 >>

このトピックに書きこむ

過去ログには書き込み不可

管理者用

- Child Tree -