非同期ダウンロードについて

タグの編集
投稿者 パル36  (中学生) 投稿日時 2011/10/15 11:29:19
こんにちは。毎回お世話になっています。

今回は、ダウンロードを非同期化させたいと思ってプログラムしたのですが、
うまくいきません。

解決策を教えていただけないでしょうか。

↓元のコード
        Try
            Dim NumCount As Integer = 0 '連番ファイルカウント 
            lbl.Text = "状況 : ダウンロードを開始します..."

            If MsgBox( "ダウンロードを開始しますか。", MsgBoxStyle.YesNo Or MsgBoxStyle.Question, "ダウンロードを開始しようとしています。") = MsgBoxResult.Yes Then
                My.Computer.Audio.PlaySystemSound(System.Media.SystemSounds.Question)
                For Each checked As String In List1.CheckedItems
                    Dim wc As New WebClient 'インスタンス 
                    Dim SavePath As String = My.Application.Info.DirectoryPath '保存先 
                    Dim Ext As String = Path.GetExtension(checked) 'URLから拡張子だけを取得 

                        '保存ファイル名を連番にする 
                        My.Computer.Network.DownloadFile(New Uri(checked), Path.Combine(SavePath, "Image" & NumCount & Ext), """"True, 30000, True, FileIO.UICancelOption.ThrowException)
                        NumCount = NumCount + 1

                    wc.Dispose()
                Next
            Else
                lbl.Text = "状況 : ダウンロードを中止しました。"
                Exit Sub
            End If
            lbl.Text = "状況 : ダウンロードが完了しました。"

        Catch ex As Exception
            lbl.Text = ex.Message
            txtLog.Text += vbCrLf & "・エラーが発生しました。" & vbCrLf & "エラーメッセージ:" & ex.Message & "スタックトレース:" & ex.StackTrace
            TabControl.SelectTab(1)
            txtLog.Select(0, 0)
        End Try
    End Sub


New Uri(checked)は、下のコードでブラウザから画像URLを取得し、
リストボックスに表示させているアイテムです。

        List1.Items.Clear()
        List2.Items.Clear()
        Dim Body As HtmlElement = BrowserMain.Document.Body
        Dim Images As HtmlElementCollection = Body.GetElementsByTagName("img")
        For Each Img As HtmlElement In Images
            Dim src As String = Img.GetAttribute("src")
            Dim ext As String = Path.GetExtension(src)
            List1.Items.Add(src)
            If Not List2.Items.Contains(ext) Then
                List2.Items.Add(ext, True)
            End If
        Next


上記は動きますが、たくさんチェックされているとダウンロードの間、なにもできません。

なので、http://www.vbstation.net/tips/downloadfileasync.htm
を参照してやってみました。

しかし、複数チェックしてダウンロードすると

エラーメッセージ:WebClient は同時 I/O 操作をサポートしません。
スタックトレース:場所 System.Net.WebClient.ClearWebClientState()
   場所 System.Net.WebClient.DownloadFileAsync(Uri address, String fileName, Object userToken)
   場所 System.Net.WebClient.DownloadFileAsync(Uri address, String fileName)

とエラーが出てしまいます。
Disposeの位置を変えても変わりませんでした。また、インスタンスのスコープを変えて見ましたが変わりませんでした。

失敗したコードを載せます。

For Each checked As String In List1.CheckedItems
                    Dim WebDownload As New WebClient 'インスタンス 
                    Dim SavePath As String=My.Application.Info.DirectoryPath '保存先 
                    Dim Ext As String = Path.GetExtension(checked) 'URLから拡張子だけを取得 
                    Dim URL As System.Uri = New Uri(checked)

                        '保存ファイル名を連番にする 
                        _Download.DownloadFileAsync(URL, Path.Combine(SavePath, "Image" & NumCount & Ext))
                        NumCount = NumCount + 1

                    End If
                    WebDownload.Dispose()
                Next

            End If


My.Computer.Network.DownloadFileではできたのに、DownloadFileAsyncではこのようなことはできないのでしょうか。
同時ダウンロードではなく、1つが完了したら次をダウンロードしたいです。

タイマーを使って、数秒後に確認しダウンロード中だったら待機させようとしましたができませんでした。
(どうすればいいかわかりませんでした。)

自分では解決策が見つかりませんでした。
スレッドを別にしたりしないといけないのでしょうか。

よろしくお願いします。
なにかコードが不明な箇所があればいってください。(一部書き換えたので)
投稿者 YuO  (社会人) 投稿日時 2011/10/15 15:56:11
非同期ですから,DownloadFileAsyncを呼び出したら,ダウンロードが終了しなくてもそのスレッドの実行を継続します。

WebClient.DownloadFileAsyncはイベントベースの非同期I/Oなので,
ダウンロードが終了したらDownloadFileCompletedイベントが発生します。
MSDN: WebClient.DownloadFileAsync メソッド (Uri, String) (System.Net)
http://msdn.microsoft.com/ja-jp/library/ms144196.aspx

たぶん,BackgroundWorker使ってDownloadFileでダウンロードしていくのが簡単だと思います。
ただし,UIへのアクセスができないので,その部分を考えて処理する必要があります。

イベント処理するとかTask.ContinueWithとかRxとかありますが,
結局BackgorundWorkerで内部は同期で書く方法で書くのが基本になると思います。
投稿者 パル36  (中学生) 投稿日時 2011/10/17 15:29:04
YuOさん返信有難うございます。

BackgroundWorker

とても参考になります!

>ただし,UIへのアクセスができないので,その部分を考えて処理する必要があります。
そのアクセスというのはどういうものなのでしょうか。

MSDNの説明には、
「進行状況の更新の通知を受け取るには、ProgressChanged イベントを処理します。操作の完了時に通知を受け取るには、RunWorkerCompleted イベントを処理します。」
と書いてありますが、これとは違うのですか。


バックグラウンドウォーカーで探していたら、
http://msdn.microsoft.com/ja-jp/library/ms229675.aspx
「方法 : バックグラウンドでファイルをダウンロードする」がありました!


とりあえず、読んでみます!

他に何かあったらお願いします。
投稿者 YuO  (社会人) 投稿日時 2011/10/17 16:15:01
> >ただし,UIへのアクセスができないので,その部分を考えて処理する必要があります。
> そのアクセスというのはどういうものなのでしょうか。

基本的に,UIに対して「データの取得」「データの設定」といった行為は,全部できないと考えてください。
例えば,
> For Each checked As String In List1.CheckedItems
は,List1.CheckedItems.GetEnumerator()という「UIデータの取得行為」を行っているので,例外が発生します。
もちろん,MessageBoxもだめですし,ラベルへのTextの設定もだめです。

取得に関しては先に取得しておき,設定や表示に関してはReportProgressイベントやRunWorkerCompletedイベントで行います。

    Private Sub button1_Click (sender As Object, e As EventArgs) Handles button.Click
       ' ボタンのハンドラ = UIスレッド 
       button1.Enabled = False
       lbl.Text = "状況 : ダウンロードを開始します..."
       If MsgBox( "ダウンロードを開始しますか。", MsgBoxStyle.YesNo Or MsgBoxStyle.Question, "ダウンロードを開始しようとしています。") <> MsgBoxResult.Yes Then
           lbl.Text = "状況 : ダウンロードを中止しました。"
           button1.Enabled = True
           Return
       End If

       My.Computer.Audio.PlaySystemSound(System.Media.SystemSounds.Question)
       backgroundWorker1.RunWorkerAsync(List1.CheckedItems.Cast(Of String)().ToArray()) ' LINQ使ってCheckedItemsをString()に変換して渡す 
    End Sub

    Private Sub backgroundWorker1_DoWork (sender As Object, e As DoWorkEventArgs) Handles backgroundWorker1.DoWork
       ' DoWorkイベント = バックグラウンドスレッド 
        Dim checkedItems As String() = DirectCast(e.Argument, String())
        Dim numCount As Integer = 0 '連番ファイルカウント  

        Using (wc As New WebClient())
            For Each checked As String In checkedItems
                Dim savePath As String = My.Application.Info.DirectoryPath '保存先  
                Dim saveFileName = Path.Combine(savePath, "Image" & numCount & Path.GetExtension(checked))

                ' 同期ダウンロード 
                wc.DownloadFile(checked, saveFileName)
                numCount += 1
            Next
        End Using
    End sub

    Private Sub backgroundWorker1_RunWorkerCompleted (sender As Object, e As RunWorkerCompletedEventArgs) Handles BackgroundWorker.RunWorkerCompleted
       ' 終了ハンドラ = UIスレッド 
        If e.Error IsNot Nothing Then
             lbl.Text = e.Error.Message
             txtLog.Text &= vbCrLf & "・エラーが発生しました。" & vbCrLf & "エラーメッセージ:" & e.Error.Message & "スタックトレース:" & e.Error.StackTrace
             TabControl.SelectTab(1)
             txtLog.Select(0, 0)
        Else
             lbl.Text = "状況 : ダウンロードが完了しました。"
        End If
       button1.Enabled = True
    End Sub



個人的には,
Blog: マルチスレッド Windows フォームアプリケーションの開発 - とあるコンサルタントのつぶやき - Site Home - MSDN Blogs
http://blogs.msdn.com/b/nakama/archive/2009/03/30/windows.aspx
の,一連の記事を読むとよいと思っています (コードはC#で書かれていますが)。
最低でも,
Blog: Part 4. Visual Studio によるマルチスレッドアプリの開発 - とあるコンサルタントのつぶやき - Site Home - MSDN Blogs
http://blogs.msdn.com/b/nakama/archive/2009/04/09/part-4-visual-studio.aspx
にはBackgroundWorkerについて色々書いてあるので読むべきです。
# 日本マイクロソフトの赤間さんの記事です。
投稿者 パル36  (中学生) 投稿日時 2011/10/17 17:45:43
有難うございます。

>基本的に,UIに対して「データの取得」「データの設定」といった行為は,全部できないと考えてください。
>例えば,
>> For Each checked As String In List1.CheckedItems
>は,List1.CheckedItems.GetEnumerator()という「UIデータの取得行為」を行っているので,例外が発生し>ます。
>もちろん,MessageBoxもだめですし,ラベルへのTextの設定もだめです。

わかりました。ほとんど操作できないのですね。

>LINQ使ってCheckedItemsをString()に変換して渡す 
コードにLINQが使われていますが、これはどんな理由で使用しているのでしょうか。
これも、ウァーカーのせいでしょうか。
LINQについては、言葉しか知らなかったです。。。

ダイレクトキャストやユージングの例までありがとうございます。
Usingは便利そうですね。これなら、Disposeをする必要がなく感激しました!!

まだ、読んでいるだけなので実際に試してみたいと思います。


参考サイトもありがとうございます。
マイクロソフトさんが書いているので、すごいですね。


#少し理解まで時間が掛かりそうなので時間をください。
投稿者 YuO  (社会人) 投稿日時 2011/10/17 18:40:46
> コードにLINQが使われていますが、これはどんな理由で使用しているのでしょうか。
楽をするためです。

Dim checkedItems As New List(Of String)
For Each s As String In List1.CheckedItems
    checkedItems.Add(s)
Next
backgroundWorker1.RunWorkerAsync(checkedItems.ToArray())
に比べて,
backgroundWorker1.RunWorkerAsync(List1.CheckedItems.Cast(Of String)().ToArray())
だと,「慣れている人にとっては」楽に書けますし,やっていることが明確になります。
# どちらもList1のCheckedItemsの各要素をStringにキャストしたものを配列にしている。
投稿者 パル36  (中学生) 投稿日時 2011/10/30 12:37:19
返事が遅れました。

># どちらもList1のCheckedItemsの各要素をStringにキャストしたものを配列にしている。 
確かに、同じだったらわかりやすいですね。
LINQの方法を使わさせていただきます。

そういえば、Usingのときに括弧をはずしないと機能しませんでした。


今回も、無事に解決しました

ありがとうございます!