Deferred execution in VB.NET?
-
22-10-2019 - |
Question
Private Sub LoadData(Of T)(ByVal query As ObjectQuery(Of T),
ByRef result As IEnumerable(Of T))
If Connection.State = ConnectionState.Open Then
result = query.ToArray
Else
AddHandler Connection.StateChange,
Sub(sender As Object, e As StateChangeEventArgs)
LoadData(query, result)
End Sub
End If
End Sub
In the above code I am trying to recurse the LoadData function when the connection is unavailable, I want to defer the loading to when it becomes available.
The problem is the above code leads to a compiler error, since a ByRef
param cannot be used in lambda expressions.
Any idea of how to do this the right way?
Solution
You cannot use a ByRef
parameter in a lambda because it could be pointing to a location on the stack which no longer exists once the lambda execuetes. All you have to do is use a more "permanent" storage location. You can pass in an object with a property of IEnumerable(Of T)
that you can set in order to assign the result.
A probably better option is to pass in a delegate (Action<IEnumerable<T>>
) that accepts the result and performs whatever action the caller requires with the result. Here's an example in C#:
void LoadData<T>(ObjectQuery<T> query, Action<IEnumerable<T>> action)
{
if (Connection.State == ConnectionState.Open)
action(query.ToArray());
else
{
// create a lambda to handle the next state change
StateChangeEventHandler lambda = null;
lambda = (sender, e) =>
{
// only perform our action on transition to Open state
if (Connection.State == ConnectionState.Open)
{
// unsubscribe when we're done
Connection.StateChange -= lambda;
action(query.ToArray());
}
}
// subscribe to connection state changes
Connection.StateChange += lambda;
}
}
And you would invoke LoadData
like this:
LoadData(query, results => listBox.DataSource = results);
Note the nuances of my implementation. For example, it does not call itself within the event handler because that will cause it to resubscribe to the event if the handler is ever called with a state other than Open
. It also unsubscribes from the event once the connection opens. I'm not sure how this would translate to VB, but in C# this is a 3-step process. First you must declare a variable to hold the lambda and set its value to null. Then you create the lambda, which can now reference itself to unsubscribe. And finally you can use the lambda to subscribe to the event.
OTHER TIPS
You've got a problem in that your calling thread has no idea if the variable has been populated by the LoadData()
call
In thi case you need to do something like:
- Block execution until the load completes
- Raise an event when the load completes
- Set a publicly visible field on your loader to indicate load status
One (possible) compromise would be to return a custom object instead of an IEnumerable
The custom object could immediately attempt to load the data and keep re-trying until success. If the result set of the custom object is read before a load has occured, block the thread until the load completes, otherwise return the result set
In this scenario, you get a benefit if there's a delay between the Load occuring and the data being used - your program can continue on until it needs the data. Whether this i useful or not depends entirely on what you're using it for.
More information on blocking execution: It depends a little on how you become aware that the connection is back up but something like:
Public Sub LoadData(Of T)(ByVal query As ObjectQuery(Of T), ByRef result As IEnumerable(Of T))
While Not Connection.State = ConnectionState.Open
Threading.Thread.Sleep(100) 'Pick a decent value for this delay based on how likely it is the connection will be available quickly
End While
result = 'Use the connection to get your data
End Sub
Is there any reason you're doing this as a sub with ByRef parameters as opposed to a function? You're only "returning" one object so I don't quite see the benefit. Not that it would make a huge difference functionally but it would be more readable...