—- Preliminary, needs tidying —-

Users coming to .NET from other platforms are often surprised that they have to do blocking reads on a Stream or Socket to read data from the network. And when they asks how to stop blocking the UI, the normal answer is to tell the them either to move the stream/socket reading onto a background thread, or to use the APM (asynchronous programming model) with the BeginRead/EndRead methods…

Which is ok until the programmer soon asks what “cross-thread” exceptions are.  Since both of those schemes have the data-arrival code run on a different thread than the UI we then have to explain all the complexity of cross-thread marshalling, whether using Control.BeginInvoke on WinForms, or the more general SyncronizationContext use.

So why don’t we just have the data arrival signalled by event, that’s how VB did it, and automatically ensure that the event runs on the UI thread. So the user simply signs-up for the respective events and all his/her problems are solved: no blocking, and no cross-thread marshalling required.

Until such a class is provided in the FCL (framework class library) the following will have to do. 🙂
Add this into a new file in your project (converting to C# if necessary). Then subscribe to the events, set it running on the connection and all should be good.


'
' Copyright (c) 2009 Alan J. McFarlane
' Free for any use. No rights reserved.
'

Option Explicit On
Option Strict On
Imports System
Imports System.IO
Imports System.Threading

''' <summary>
''' Raises events when data is received on a <see cref="T:System.IO.Stream" />
''' </summary>
''' <remarks>
''' <para>Raises three events <see cref="E:DataReceived" />, <see cref="E:ConnectionClosed" />,
''' and <see cref="E:ErrorOccurred" />.
''' </para>
''' <para>The events are called on the correct synchronization context, e.g. on
''' the UI thread in a WinForms app.
''' </para>
''' </remarks>
''' -
''' <example>
''' <code>
''' ...
''' Dim strm As Stream = cli.GetStream() ' etc...
''' Dim efs As New EventFromStream() ' We pick-up the UI thread when initialised
''' AddHandler efs.DataReceived, AddressOf HandleDataReceived
''' AddHandler efs.ErrorOccurred, AddressOf HandleErrorOccurred
''' AddHandler efs.ConnectionClosed, AddressOf HandleConnectionClosed
''' efs.Run(strm)
''' ...
''' End Sub
'''
''' Sub HandleDataReceived(ByVal sender As Object, ByVal e as DataReceivedEventArgs)
''' Dim data() As Byte = e.Data
''' ...
''' End Sub
''' ...
''' </code>
''' </example>
Class EventFromStream : Inherits System.ComponentModel.Component
Private m_strm As Stream
Private m_syncCtx As SynchronizationContext
Private m_disposed As Integer
Private m_disposedEvent As New ManualResetEvent(False)

'----
Friend Sub New()
m_syncCtx = SynchronizationContext.Current
End Sub

Overloads Sub Dispose(ByVal disposing As Boolean)
Try
Thread.VolatileWrite(m_disposed, 1)
m_disposedEvent.Set()
' Should we close the Stream? It makes life much easier for us if
' we do but
Finally
MyBase.Dispose(disposing)
End Try
End Sub

Private ReadOnly Property IsDisposed() As Boolean
Get
Dim v As Integer = Thread.VolatileRead(m_disposed)
Dim disposed As Boolean = v <> 0
Return disposed
End Get
End Property

'----
Public Event DataReceived As EventHandler(Of DataReceivedEventArgs)

Public Event ConnectionClosed As EventHandler(Of EventArgs)

Public Event ErrorOccurred As EventHandler(Of ErrorEventArgs)

'----
Public Sub Run(ByVal strm As Stream)
If m_strm IsNot Nothing Then Throw New ArgumentException("Supports one stream at the moment")
m_strm = strm
ThreadPool.QueueUserWorkItem(AddressOf Runner)
End Sub

Private Sub Runner(ByVal state As Object)
Dim buf(1024 - 1) As Byte
While True
Dim readLen As Integer
Try
#If False Then
readLen = m_strm.Read(buf, 0, buf.Length)
#Else
' Use the async version so we can exit when disposed
Dim ar As IAsyncResult = m_strm.BeginRead(buf, 0, buf.Length, Nothing, Nothing)
Dim ah() As WaitHandle = {ar.AsyncWaitHandle, m_disposedEvent}
Dim signalled As Integer = WaitHandle.WaitAny(ah, -1)
If signalled = 0 Then
readLen = m_strm.EndRead(ar)
Else
' We leave that BeginReceive hanging in this case. :-( There's
' not much we can do about that (well we could start a thread
' sitting on EndRead...).
' It would be much nicer if we closed the stream when we were
' disposed...
Exit While
End If
#End If
Catch ex As Exception
' Probably due to the stream closing, so ignore.
If IsDisposed Then Exit While
DoError(ex)
Exit While
End Try
If readLen = 0 Then
DoConnectionClosed()
Exit While
End If
DoDataReceived(Clone(buf, 0, readLen))
End While
End Sub

'--
Private Sub DoError(ByVal ex As Exception)
m_syncCtx.Send(AddressOf DoErrorS, ex)
End Sub

Private Sub DoErrorS(ByVal state As Object)
Dim ex As Exception = CType(state, Exception)
RaiseEvent ErrorOccurred(Me, New ErrorEventArgs(ex))
End Sub

'--
Private Sub DoConnectionClosed()
m_syncCtx.Send(AddressOf DoConnectionClosedS, Nothing)
End Sub

Private Sub DoConnectionClosedS(ByVal state As Object)
RaiseEvent ConnectionClosed(Me, New EventArgs)
End Sub

'--
Private Sub DoDataReceived(ByVal data() As Byte)
m_syncCtx.Send(AddressOf DoDataReceivedS, data)
End Sub

Private Sub DoDataReceivedS(ByVal state As Object)
Dim data() As Byte = CType(state, Byte())
RaiseEvent DataReceived(Me, New DataReceivedEventArgs(data))
End Sub

'----
Private Function Clone(ByVal buf As Byte(), ByVal offset As Integer, ByVal length As Integer) As Byte()
' Create a new array of the correct size
Dim buf2() As Byte = CType(Array.CreateInstance(GetType(Byte), length), Byte())
Array.Copy(buf, offset, buf2, 0, length)
Return buf2
End Function
End Class

Class DataReceivedEventArgs : Inherits EventArgs
Private m_data() As Byte

Friend Sub New(ByVal data() As Byte)
m_data = data
End Sub

Public ReadOnly Property Data() As Byte()
Get
Return m_data
End Get
End Property
End Class