Question

Can I update a progress of a loop using the yield keyword?

foreach(something obj in somelist)
{
    yield return obj;
}

I know I can do this, but how do I use the values to update a progress bar ?

Thanks.

Was it helpful?

Solution

Based on your comment that you are

trying to update the progress of transfer of data in a wcf service to a windows form

Here is the solution that I have used on a recent project. The sample code is geared towards downloading a file (i.e., pdf, jpg, etc), but can be applied to any streamable data.

The first important note, your WCF binding choice will determine whether or not this is even possible. If you are using basicHttpBinding or netTcpBinding, then the approach should work, however if you are using WsHttpBinding, you are out of luck.

A sample binding may look something like:

  <basicHttpBinding>
    <binding name="ResourceServiceBasicHttpBinding" maxReceivedMessageSize="20971520" sendTimeout="00:05:00" receiveTimeout="00:05:00" transferMode="Streamed" messageEncoding="Mtom">
      <security mode="None" />
    </binding>
  </basicHttpBinding>

The important thing to note is the transfer mode "Streamed".

To report progress on the transfer of data between a WCF service and a WinForm you need to ensure that your WCF service is using MessageContracts over DataContracts. The reason is that a MessageContract allows you to have a single stream as the response body. For instance, you may have a request/response message contract as follows:

The request...

[MessageContract]
public class DownloadFileRequest
{
  [MessageHeader(MustUnderstand = true)]
  public Guid FileId { get; set; }
}

The response...

[MessageContract]
public class DownloadFileResponse : IDisposable
{
  [MessageHeader(MustUnderstand = true)]
  public Int32 FileSize { get; set; }

  [MessageHeader(MustUnderstand = true)]
  public String FileName { get; set; }

  [MessageBodyMember(Order = 1)]
  public Stream FileByteStream { get; set; }

  public void Dispose()
  {
    if (FileByteStream != null)
      FileByteStream.Dispose();
  }
}

Now this is all well and good, but you need to have a way to report that download (or upload) progress back to the client. Unfortunately, .NET doesn't provide a nice way out of the box to do this... as mentioned by others, you are looking for events... specifically, the implementation of an "ObservableStream". Again, here is the implementation that I like to use....

The event args to report a read or write operation to a subscriber...

public class ObservableStreamEventArgs : EventArgs
{
  private readonly Int64 _length;
  private readonly Int64 _position;
  private readonly Int32 _numberOfBytes;

  public Int64 StreamLength { get { return _length; } }
  public Int64 StreamPosition { get { return _position; } }
  public Int32 NumberOfBytes { get { return _numberOfBytes; } }

  public ObservableStreamEventArgs(Int32 numberOfBytes, Int64 position, Int64 length)
  {
    _numberOfBytes = numberOfBytes;
    _position = position;
    _length = length;
  }
}

And of course the stream implementation itself...

public class ObservableStream : Stream
{
  private readonly Stream _baseStream;
  private Int64 _streamLength;

  public event EventHandler<ObservableStreamEventArgs> BytesRead;
  public event EventHandler<ObservableStreamEventArgs> BytesWritten;

  public ObservableStream(Stream stream)
  {
    Verify.NotNull(stream);

    _baseStream = stream;
    _streamLength = _baseStream.Length;
  }

  public ObservableStream(Stream stream, Int64 length)
  {
    Verify.NotNull(stream);

    _baseStream = stream;
    _streamLength = length;
  }

  public override bool CanRead      
  {        
    get { return _baseStream.CanRead; }
  }

  public override bool CanSeek
  {
    get { return _baseStream.CanSeek; }
  }

  public override bool CanWrite
  {
    get { return _baseStream.CanWrite; }
  }

  public override void Flush()
  {
    _baseStream.Flush();
  }

  public override Int64 Length
  {
      get { return _streamLength; }
    }

    public override Int64 Position
    {
      get { return _baseStream.Position; }
      set { _baseStream.Position = value; }
    }

    public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
    {
      Int32 bytesRead = _baseStream.Read(buffer, offset, count);

      var listeners = BytesRead;
      if (listeners != null)
        listeners.Invoke(this, new ObservableStreamEventArgs(bytesRead, Position, Length));

      return bytesRead;
    }

    public override Int64 Seek(Int64 offset, SeekOrigin origin)
    {
      return _baseStream.Seek(offset, origin);
    }

    public override void SetLength(Int64 value)
    {
      _streamLength = value;
    }

    public override void Write(Byte[] buffer, Int32 offset, Int32 count)
    {
      _baseStream.Write(buffer, offset, count);

      var listeners = BytesWritten;
      if (listeners != null)
        listeners.Invoke(this, new ObservableStreamEventArgs(count, Position, Length));
    }

    public override void Close()
    {
      _baseStream.Close();

      base.Close();
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
        _baseStream.Dispose();

      base.Dispose(disposing);
    }
  }

Finally, you have to consume the data in your windows client...

Simple call your service method as you normally would (ServiceChannelFactory is a wrapper around a ChannelFactory implementation... don't worry about that).

 DownloadFileResponse response;
 using (var channel = _serviceChannelFactory.CreateReportServiceChannel())
   response channel.Service.GenerateReport(request)

and then hookup to your observable stream to update the ui (obviously must be working on a background thread, otherwise you will just block the ui)

 using (var downloadStream = response.FileByteStream)
 using (var observableStream = new ObservableStream(downloadStream, response.FileSize))
 using (var fileStream = new FileStream(tempFile, FileMode.OpenOrCreate, FileAccess.Write))
 {
   observableStream.BytesRead += (sender, e) => _view.UpdateProgress(Convert.ToInt32(e.StreamPosition * 100 / e.StreamLength));

   do
   {
     bytesRead = observableStream.Read(buffer, 0, 4096);
     fileStream.Write(buffer, 0, bytesRead);
   } while (bytesRead > 0);
 }

EDIT

Oh, the service method itself is pretty simple... forgot to include it; all you have to do is return your stream:

  return new DownloadFileResponse
           {
             FileName = report.FileName,
             FileSize = report.Data.Length,
             FileByteStream = new MemoryStream(report.Data, false)
           };

OTHER TIPS

As commented by user digEmAll, it is more likely that you want to use an event for this.

But if you want a solution that updates progress on yield return, you will probably want to get the number of elements before iterating through your collection. Then, for each element you get returned, you can update your progressbar by one.

Perhaps it would be useful if you elaborated your question a bit - what exactly are you trying to accomplish.

Why not update the progress bar from inside the loop?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top