I am trying to create my first TPL DataFlow service in a Windows Form application using .Net 4.5.
In overview, the application loads and parses some data from a .csv text file and, after some processing/interrogation generates a SQL database record (using an Entity Framework data access layer). Once all the records have been loaded, the application generates a results text file by iterating through the generated database records. Here is the workflow...
loadListFile: iterates through text file and posts to...
+ createStateRecord: generates the database records
+ setStateRecordStatus: updates the UI for each record
WhenAll records have been created...
generateReport: iterates through the database and posts to
+ writeReport: writes a summary record to a text file
+ setReportStatus: updates the UI to show report progress
I appreciate that I could structure the relationship between these two tasks as part of the workflow, but for now I want to keep them separate
The problem I am having is that the application/TPL workflow appears to work correctly on the first pass, but if the UI is reset and the process is run a second time, the writeReport.Completion method is never called - even though the writeReport.Complete() method is called.
Here is the full method (which is invoked from a button 'click' even handler on the form).
private async void ProcessData()
{
/********************************************************************/
/** Create State records */
/********************************************************************/
var createStateRecord = new TransformBlock
<CheckData, CheckResult>(checkData =>
{
return DalServices.CreateStateRecord(checkData);
}, new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token,
MaxDegreeOfParallelism = 10
});
var setStateRecordStatus = new ActionBlock <CheckResult>(
result =>
{
Interlocked.Increment(ref _noRecordsProcessed);
if (!result.Success)
Interlocked.Increment(ref _noRecordsFailed);
pbarLoad.Minimum = 0;
pbarLoad.Maximum = _noRecordsInFile;
pbarLoad.Value = _noRecordsProcessed;
}, new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token,
TaskScheduler = TaskScheduler
.FromCurrentSynchronizationContext()
});
var loadListFile = new ActionBlock<string>(
listFilePath =>
{
using (var reader =
new DataProviderService(_listFileInfo.FullName))
{
_noRecordsProcessed = 0;
_noRecordsInFile = reader.NoRows;
foreach (var item in reader)
{
createStateRecord.Post(new CheckData(item));
}
createStateRecord.Complete();
}
},
new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token
});
/*
* Link StateRecord tasks
*/
createStateRecord.LinkTo(setStateRecordStatus);
createStateRecord.Completion.ContinueWith(t =>
{
if (t.IsFaulted)
{
((IDataflowBlock) setStateRecordStatus)
.Fault(t.Exception);
return;
}
setStateRecordStatus.Complete();
}, _cancellationToken.Token);
/********************************************************************/
/** Reporting tasks */
/********************************************************************/
var writeReport = new TransformBlock<ReportData, State>(
reportData =>
{
using (
var writer = new StreamWriter(
reportData.ResultsFilePath, true))
{
writer.WriteLine(reportData.State.ToString());
}
return reportData.State;
},
new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token,
MaxDegreeOfParallelism = 1
});
var setReportStatus = new ActionBlock<State>(
result =>
{
Interlocked.Increment(ref _noRecordsReported);
pbarReport.Minimum = 0;
pbarReport.Maximum = _noRecordsInFile;
pbarReport.Value = _noRecordsReported;
},
new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token,
TaskScheduler = TaskScheduler
.FromCurrentSynchronizationContext()
});
var generateReport = new ActionBlock<string>(
listFilePath =>
{
using (var stateService = DalService.GetStateService())
{
var resultsFilePath = listFilePath + ".results";
foreach (var state in stateService.GetStates())
{
writeReport.Post(
new ReportData
{
State = state,
ResultsFilePath = resultsFilePath
});
}
// This alwaus gets called
writeReport.Complete();
}
},
new ExecutionDataflowBlockOptions
{
CancellationToken = _cancellationToken.Token
});
/*
* Link Reporting tasks
*/
writeReport.LinkTo(setReportStatus);
writeReport.Completion.ContinueWith(t =>
{
// This only get called on first run!
if (t.IsFaulted)
{
((IDataflowBlock) setStateRecordStatus).Fault(
t.Exception);
return;
}
setReportStatus.Complete();
}, _cancellationToken.Token);
/********************************************************************/
/** Run the tasks */
/********************************************************************/
try
{
loadListFile.Post(_listFileInfo.FullName);
loadListFile.Complete();
await Task.WhenAll(createStateRecord.Completion);
generateReport.Post(_listFileInfo.FullName);
generateReport.Complete();
await Task.WhenAll(writeReport.Completion);
}
catch (TaskCanceledException)
{
MessageBox.Show(
"Job cancelled by user.", "Job Cancelled",
MessageBoxButtons.OK);
SetUiAfterProcessing("Job cancelled by user.");
}
catch (AggregateException aex)
{
MessageBox.Show(
aex.ListExceptions(), "Task Errors", MessageBoxButtons.OK);
SetUiAfterProcessing("Task processing error - job cancelled.");
}
catch (Exception ex)
{
MessageBox.Show(
ex.ToString(), "Unhandled Exception", MessageBoxButtons.OK);
SetUiAfterProcessing("Application error - job cancelled.");
}
finally
{
SetUiAfterProcessing("Job complete.");
}
}
I have tried refactoring the methods and simplifying the internal actions of each Block, but I am still no closer to identifying what I am doing wrong - any help would be much appreciated. Thank you.