Use case
A GUI with 2 controls:
- An input control for text (a DNA sequence) that should be analyzed
- A result control that displays the analyzing result of the entered text
The analysis takes 2-3 seconds and should start as soon as the text in the input control is changing. The shown result should always match the actual text in the input field.
Problem
If the user types text, it changes multiple times within the processing time of the analysis. Starting multiple processing tasks parallel is not an option.
Solution
My approach for solving the problem above:
- An 'ItemProcessor' gets the input and process it in background.
- If a new item was added whereas another item is processing, the new item is 'stored' for subsequent processing.
- If another item is already stored for subsequent processing, it will be overwritten.
- The result is returned only, of no item for subsequent processing is available anymore.
Implementation:
public class LastItemProcessor<TInput, TOutput>
{
private readonly Func<TInput, Task<TOutput>> Process;
private readonly Action OnProcessingStartedAction;
private readonly Action OnProcessingFinishedAction;
private readonly Action<Task<TOutput>> OnLastItemProcessedAction;
private TInput myInput;
private bool myIsLastItemFlag = true;
private bool myIsProcessing = false;
private readonly object myLock = new object();
public LastItemProcessor(Func<TInput, Task<TOutput>> process,
Action<Task<TOutput>> onLastItemProcessedAction,
Action onProcessingStartedAction = null,
Action onProcessingFinishedAction = null)
{
process.Ensure(nameof(process)).IsNotNull();
onLastItemProcessedAction.Ensure(nameof(onLastItemProcessedAction)).IsNotNull();
Process = process;
OnProcessingStartedAction = onProcessingStartedAction;
OnProcessingFinishedAction = onProcessingFinishedAction;
OnLastItemProcessedAction = onLastItemProcessedAction;
}
public void ProcessItem(TInput input)
{
lock (myLock)
{
myInput = input;
if (myIsProcessing)
{
myIsLastItemFlag = false;
}
else
{
myIsProcessing = true;
OnProcessingStartedAction?.Invoke();
var task = ProcessNextItemAsync();
task.ContinueWith(t =>
{
OnLastItemProcessedAction(task);
OnProcessingFinishedAction?.Invoke();
});
}
}
}
private async Task<TOutput> ProcessNextItemAsync()
{
TOutput result;
try
{
result = await Process(myInput);
}
catch (Exception)
{
// ensure that the processing flag is set in case of exceptions.
myIsProcessing = false;
throw;
}
lock (myLock)
{
if (myIsLastItemFlag)
{
myIsProcessing = false;
return result;
}
myIsLastItemFlag = true;
}
return await ProcessNextItemAsync();
}
}
Usage
// Initialization (in constructor)
// Note: AnalyzingError, IsAnalyzing and AnalysisRegions are view model properties
myLastItemProcessor = new LastItemProcessor<string, AnalysisRegion[]>(
AnalyzeSequenceAsync,
t =>
{
if (t.IsFaulted)
{
AnalyzingError = "Error Message";
}
else
{
AnalysisRegions = t.Result;
AnalyzingError = null;
}
},
() => IsAnalyzing = true,
() => IsAnalyzing = false);
// ...
// Process item (in setter of Sequence property)
myLastItemProcessor.ProcessItem(sequence);
I am interested in general code improvements (e.g. naming, simplifications, bugs, ...) and alternative approaches to address that kind of problem.
However, any constructive feedback is welcome of course :)