WebClient をサブクラス化して、その動作をより詳細に制御できるようにしました (タイムアウトを変更し、1MB の進行ごとに起動するイベント ハンドラーを提供します)。XML ソースから継続的に (1 分間に 1 回) ポーリング データを実行し、ほとんどの場合正常に動作します。ただし、時々、理解できない ObjectDisposedException が発生します。関連する部分と思われる部分を最初にコメント付きで投稿し、次に参照用にソース コード全体を投稿します。
DownloadProgressChanged イベント ハンドラーと破棄
ObjectDisposedException は、このハンドラーの最後の行で発生します。変数はコメントに示されている状態にあります。
void MyWebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
// cancelDueToError is false, isDisposed is true
// isDisposed must have been false when this event handler fired
if (cancelDueToError || isDisposed) return;
long additionalBytesReceived = e.BytesReceived - PreviousBytesReceived;
PreviousBytesReceived = e.BytesReceived;
BytesNotNotified += additionalBytesReceived;
if (BytesNotNotified > ONE_MB)
{
OnNotifyMegabyteIncrement(e.BytesReceived);
BytesNotNotified = 0; // This is 0 at the point of the Exception, so this handler fired.
}
firstByteReceived = true;
//
// !!!!! EXCEPTION HERE
//
// ObjectDisposedException here. Clearly the class has been disposed already (isDisposed is true).
// But why is the DownloadProgressChanged event firing after MyWebClient is disposed?
abortTimer.Change(TimeBetweenProgressChanges, System.Threading.Timeout.Infinite);
}
// Somehow IDisposable.Dispose() is being called in the middle of the
// MyWebClient_DownloadProgressChanged event handler. isDisposed is false
// at the beginning of the event handler (else it would have returned immediately)
// but true when I get the ObjectDisposedException
void IDisposable.Dispose()
{
isDisposed = true;
if (asyncWait != null) asyncWait.Dispose();
if (abortTimer != null) abortTimer.Dispose();
base.Dispose();
}
使用法
using (MyWebClient webClient = new MyWebClient())
{
webClient.NotifyMegabyteIncrement += new MyWebClient.PerMbHandler(webClient_NotifyMegabyteIncrement);
bool downloadOK = webClient.DownloadFileWithEvents(url, outputPath);
// Do stuff with downloaded file if downloadOK
}
明らかに、MyWebClient_DownloadProgressChanged イベント ハンドラーの途中で MyWebClient が破棄されることがあります。ただし、ダウンロードが完了するまでオブジェクトを破棄しないでください。これはどのように起こりますか?
完全なソース コード
public class MyWebClient : WebClient, IDisposable
{
public int Timeout { get; set; }
public int TimeUntilFirstByte { get; set; }
public int TimeBetweenProgressChanges { get; set; }
public long PreviousBytesReceived { get; private set; }
public long BytesNotNotified { get; private set; }
public string Error { get; private set; }
public bool HasError { get { return Error != null; } }
private bool firstByteReceived = false;
private bool success = true;
private bool cancelDueToError = false;
private EventWaitHandle asyncWait = new ManualResetEvent(false);
private Timer abortTimer = null;
private bool isDisposed = false;
const long ONE_MB = 1024 * 1024;
public delegate void PerMbHandler(long totalMb);
public event PerMbHandler NotifyMegabyteIncrement;
public MyWebClient(int timeout = 60000, int timeUntilFirstByte = 30000, int timeBetweenProgressChanges = 15000)
{
this.Timeout = timeout;
this.TimeUntilFirstByte = timeUntilFirstByte;
this.TimeBetweenProgressChanges = timeBetweenProgressChanges;
this.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(MyWebClient_DownloadFileCompleted);
this.DownloadProgressChanged += new DownloadProgressChangedEventHandler(MyWebClient_DownloadProgressChanged);
abortTimer = new Timer(AbortDownload, null, TimeUntilFirstByte, System.Threading.Timeout.Infinite);
}
protected void OnNotifyMegabyteIncrement(long totalMb)
{
if (NotifyMegabyteIncrement != null) NotifyMegabyteIncrement(totalMb);
}
void AbortDownload(object state)
{
cancelDueToError = true;
this.CancelAsync();
success = false;
Error = firstByteReceived ? "Download aborted due to >" + TimeBetweenProgressChanges + "ms between progress change updates." : "No data was received in " + TimeUntilFirstByte + "ms";
asyncWait.Set();
}
void MyWebClient_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
if (cancelDueToError || isDisposed) return;
long additionalBytesReceived = e.BytesReceived - PreviousBytesReceived;
PreviousBytesReceived = e.BytesReceived;
BytesNotNotified += additionalBytesReceived;
if (BytesNotNotified > ONE_MB)
{
OnNotifyMegabyteIncrement(e.BytesReceived);
BytesNotNotified = 0;
}
firstByteReceived = true;
abortTimer.Change(TimeBetweenProgressChanges, System.Threading.Timeout.Infinite);
}
public bool DownloadFileWithEvents(string url, string outputPath)
{
asyncWait.Reset();
Uri uri = new Uri(url);
this.DownloadFileAsync(uri, outputPath);
asyncWait.WaitOne();
return success;
}
void MyWebClient_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
{
if (cancelDueToError || isDisposed) return;
asyncWait.Set();
}
protected override WebRequest GetWebRequest(Uri address)
{
var result = base.GetWebRequest(address);
result.Timeout = this.Timeout;
return result;
}
void IDisposable.Dispose()
{
isDisposed = true;
if (asyncWait != null) asyncWait.Dispose();
if (abortTimer != null) abortTimer.Dispose();
base.Dispose();
}
}