Search code examples
vb.netparallel.foreach

How to use Parallel.ForEach to loop into a DataGridViewRow


I have a code below that loops thru each row in a datagridview which contains the server and the service name and uses the ServiceController reference to check the status of each service and return the value on the cell.

        For Each dgvrow As DataGridViewRow In DataGridView1.Rows
        myController = New ServiceController With {
        .MachineName = dgvrow.Cells(0).Value,
        .ServiceName = dgvrow.Cells(1).Value
        }
        dgvrow.Cells(2).Value = myController.Status.ToString
        Next

This works but it runs sequentially and it is taking time for each thread to finish before it goes to the next row so I want to run it on parallel.

I search here and stumbled upon Parallel.ForEach but I could not find the right code/combination to make this work.

My intitial attempt was

Parallel.ForEach(dgvrow as DataGridViewRow in DatagridView1.Rows
Sub(myServer)
    myController = New ServiceController With {
        .MachineName = dgvrow.Cells(0).Value,
        .ServiceName = dgvrow.Cells(1).Value
        }
        dgvrow.Cells(2).Value = myController.Status.ToString
End Sub
)

Which is definitely wrong, not sure what to put after the ForEach part

The expected result should look like below, I expect the Service Status Column to get filled up simultaneously.

<table border=1>
  <tr>
    <th>Server Name</th>
    <th>Service Name</th> 
    <th>Service Status</th>
  </tr>
  <tr>
    <th>Server 1</th>
    <th>Service 1</th> 
    <th>Not Running</th>
  </tr>
  <tr>
    <th>Server 2</th>
    <th>Service 2</th> 
    <th>Running</th>
  </tr>
  <tr>
    <th>Server 3</th>
    <th>Service 3</th> 
    <th>Not Running</th>
  </tr>
  </table>

Solution

  • A DataGridView is a UI element - you're not allowed to access it from any thread other than the UI. You can't use Parallel.ForEach on it.

    What you can do is extract the data as a non-UI object, work in parallel on that, and then assign the results back.

    Try this:

    'UI thread
    Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
    {
        .MachineName = dgvrow.Cells(0).Value,
        .ServiceName = dgvrow.Cells(1).Value
    }).ToArray()
    
    'Parallel
    Dim serviceControllers = inputs.AsParallel().Select(Function (x) New ServiceController With _
    {
        .MachineName = x.MachineName,
        .ServiceName = x.ServiceName
    }).ToArray()
    
    'UI thread
    For Each x In serviceControllers.Zip(DataGridView1.Rows.Cast(Of DataGridViewRow), Function (sc, dgvrow) New With { sc, dgvrow })
        x.dgvrow.Cells(2).Value = x.sc.Status.ToString
    Next
    

    Use this if the rows change order during the computation.

    'UI thread
    Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
    {
        .MachineName = dgvrow.Cells(0).Value,
        .ServiceName = dgvrow.Cells(1).Value,
        .dgvrow = dgvrow
    }).ToArray()
    
    'Parallel
    Dim results = inputs.AsParallel().Select(Function (x) New With
    {
        .sc = New ServiceController With _
        {
            .MachineName = x.MachineName,
            .ServiceName = x.ServiceName
        }, _
        .dgvrow = x.dgvrow
    }).ToArray()
    
    'UI thread
    For Each x In results
        x.dgvrow.Cells(2).Value = x.sc.Status.ToString
    Next
    

    Keep in mind if rows are deleted during computation this will also fail.


    You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Forms and add using System.Reactive.Linq; - then you can do this:

    'UI thread
    Dim inputs = DataGridView1.Rows.Cast(Of DataGridViewRow).Select(Function(dgvrow) New With _
    {
        .MachineName = CType(dgvrow.Cells(0).Value, String),
        .ServiceName = CType(dgvrow.Cells(1).Value, String),
        .Row = dgvrow
    }).ToArray()
    
    'Rx query
    Dim query = _
        From x In inputs.ToObservable()
        From s In Observable.Start(Function () FetchStatus(x.MachineName, x.ServiceName))
        Select New With
        {
            x.Row,
            .Status = s
        }
    
    'Rx subscription
    Dim subscription As IDisposable = _
        query _
            .ObserveOn(DataGridView1) _
            .Subscribe(Sub (x) x.Row.Cells(2).Value = x.Status)
    

    This will update each row as quickly as the respond comes back - and it'll be processed in parallel.

    I have assumed that you have a function with this signature: Function FetchStatus(MachineName As String, ServiceName As String) As String.