Asynchronous "Loading..." text in WPF - c#

I'm trying to get a TextBlock to show up on screen with the words "Loading..." with the number of dots changing every half a second to indicate that a file is currently being parsed. Unfortunately, it doesn't animate like I want it to when the file is being parsed. This is what I have currently:
private void MainWindow_MIDIBrowseClick(object sender, RoutedEventArgs e)
{
MIDIBrowseClick?.Invoke(this, e);
OpenFileDialog browseDialog = new OpenFileDialog
{
Filter = "MIDI files (*.mid)|*.mid|All files (*.*)|*.*"
};
if (browseDialog.ShowDialog() == true)
{
try
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { ShowLoadingText(tokenSource.Token); }));
MIDIParser midiParse = new MIDIParser(File.ReadAllBytes(browseDialog.FileName));
midiParse.fileName = browseDialog.SafeFileName;
midiParse.ParseFile();
NoteParser noteParse = new NoteParser(midiParse);
noteParse.ParseEvents();
tokenSource.Cancel();
DataContext = new PianoRollView(midiParse, noteParse);
}
catch (InvalidOperationException)
{
MessageBox.Show("Error parsing MIDI file!", "Error");
}
}
}
That calls the below method which animates the "Loading..." text:
private async void ShowLoadingText(CancellationToken token)
{
txtLoading.Visibility = Visibility.Visible;
try
{
while (!token.IsCancellationRequested)
{
txtLoading.Text = "Loading";
await Task.Delay(500, token);
txtLoading.Text = "Loading.";
await Task.Delay(500, token);
txtLoading.Text = "Loading..";
await Task.Delay(500, token);
txtLoading.Text = "Loading...";
await Task.Delay(500, token);
}
}
catch (TaskCanceledException)
{
txtLoading.Visibility = Visibility.Hidden;
}
}
Not sure what I'm doing wrong. Any help would be appreciated!

It seems like you try to invoke the parsing operation from the UI thread synchronously. That's why it locked UI and the text isn't animated. You should try to add an asynchronous invocation of your parsing operation. Something like this:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
Dispatcher.Invoke(DispatcherPriority.Background, new ThreadStart(delegate { ShowLoadingText(tokenSource.Token); }));
await Task.Run(() => Thread.Sleep(5000)); // your parsing operation
tokenSource.Cancel();
}
private async void ShowLoadingText(CancellationToken token)
{
txtLoading.Visibility = Visibility.Visible;
try
{
while (!token.IsCancellationRequested)
{
txtLoading.Text = "Loading";
await Task.Delay(500, token);
txtLoading.Text = "Loading.";
await Task.Delay(500, token);
txtLoading.Text = "Loading..";
await Task.Delay(500, token);
txtLoading.Text = "Loading...";
await Task.Delay(500, token);
}
}
catch (TaskCanceledException)
{
txtLoading.Visibility = Visibility.Hidden;
}
}
}
}
In the sample above you will see Loading animation with 5 sec duration. This is achieved by asynchronous invocation of Thread.Sleep(5000). In your case you should write a method which will implement parsing operation:
private void Parse()
{
MIDIParser midiParse = new MIDIParser(File.ReadAllBytes(browseDialog.FileName));
midiParse.fileName = browseDialog.SafeFileName;
midiParse.ParseFile();
NoteParser noteParse = new NoteParser(midiParse);
noteParse.ParseEvents();
}
And replace Thread.Sleep(5000) with its invocation.

Related

C#: Task cancellation not working (CancellationTokenSource)

I have some long running code that I would like to run as a Task and cancel when needed using CancellationTokenSource but cancellation doesn't seem to work as my task keeps running when tokenSource.Cancel() is called (no exception thrown).
Probably missing something obvious?
Cut down example below:
bool init = false;
private void Button1_Click(object sender, EventArgs e)
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
Task task = new Task(() =>
{
while (true)
{
token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested)
{
Console.WriteLine("Operation is going to be cancelled");
throw new Exception("Task cancelled");
}
else
{
// do some work
}
}
}, token);
if (init)
{
tokenSource.Cancel();
button1.Text = "Start again";
init = false;
} else
{
try
{
task.Start();
} catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
button1.Text = "Cancel";
init = true;
}
}
Main issue in your code is that you don't store a tokenSource. Second Button1_Click invocation cancels different token than you pass to task during first call.
Second issue is that you create over and over again new task, but your logic suggest that you want one task which should be created on first click and terminated during second click.

CancellationTokenSource.Cancel method not changing CancellationToken.IsCancellationRequested property

Anyone know what I'm doing wrong?
Windows 10 - VS2013
C# .NET 4.5.1
I have a button called testButton and a testButton_Click method that I'm trying to use to do work, but also be able to abort that work. Doing the work in a task is working, and the logic for aborting is being executed (the button turns back to a grey "Run Test"), but the cts.Cancel method doesn't seem to affect the token's IsCancellationRequested flag, or the task has the wrong token, or something...?
public System.Threading.Tasks.Task DoWorkAsync(CancellationToken ct = default(CancellationToken))
{
System.Threading.Tasks.Task T1 = new System.Threading.Tasks.Task(() =>
{
try
{
var capturedToken = ct;
// do work here
for (int i = 0; i < work.Length; i++)
{
ct.ThrowIfCancellationRequested();
capturedToken.ThrowIfCancellationRequested();
if (ct.IsCancellationRequested) throw new OperationCanceledException(ct);
// sleep 1 second
System.Threading.Thread.Sleep(1000);
}
}
catch
{
MessageBox.Show("Caught :) I can do stuff", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
taskRunning = false;
}
}, ct);
return T1;
}
private async void testButton_Click(object sender, EventArgs e)
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;
var task = DoWorkAsync(ct);
if (!taskRunning)
{
Cursor.Current = Cursors.WaitCursor;
testButton.BackColor = Color.Red;
testButton.Text = "ABORT";
taskRunning = true;
task.Start();
await task;
// Release cursor when setup is complete
Cursor.Current = Cursors.Default;
}
else
{
cts.Cancel();
testButton.Text = "Run Test";
testButton.BackColor = Color.Gray;
}
}

Dataflow Task.WhenAll causes A task was canceled Exception

I am new to Dataflow, and I follow this walkthrough How to: Cancel a Dataflow Block.
I click add button first, and then click cancel, but I got exception about "A task was canceled Exception" after clicking cancel button. I fail to find any way to resolve this error.
Any help would be appreciated.
Update:
Code for demo:
public partial class Form1 : Form
{
CancellationTokenSource cancellationTokenSource;
TransformBlock<WorkItem, WorkItem> startWork;
ActionBlock<WorkItem> completeWork;
ActionBlock<ToolStripProgressBar> incProgress;
ActionBlock<ToolStripProgressBar> decProgress;
TaskScheduler uiTaskScheduler;
public Form1()
{
InitializeComponent();
uiTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Cancel.Enabled = false;
}
private void Add_Click(object sender, EventArgs e)
{
if (!Cancel.Enabled)
{
CreatePipeline();
Cancel.Enabled = true;
}
for (int i = 0; i < 20; i++)
{
toolStripProgressBar1.Value++;
startWork.Post(new WorkItem());
}
}
private async void Cancel_Click(object sender, EventArgs e)
{
Add.Enabled = false;
Cancel.Enabled = false;
cancellationTokenSource.Cancel();
try
{
await Task.WhenAll(
completeWork.Completion,
incProgress.Completion,
decProgress.Completion);
}
catch (OperationCanceledException)
{
throw;
}
toolStripProgressBar4.Value += toolStripProgressBar1.Value;
toolStripProgressBar4.Value += toolStripProgressBar2.Value;
// Reset the progress bars that track the number of active work items.
toolStripProgressBar1.Value = 0;
toolStripProgressBar2.Value = 0;
// Enable the Add Work Items button.
Add.Enabled = true;
}
private void CreatePipeline()
{
cancellationTokenSource = new CancellationTokenSource();
startWork = new TransformBlock<WorkItem, WorkItem>(workItem =>
{
workItem.DoWork(250, cancellationTokenSource.Token);
decProgress.Post(toolStripProgressBar1);
incProgress.Post(toolStripProgressBar2);
return workItem;
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationTokenSource.Token
});
completeWork = new ActionBlock<WorkItem>(workItem =>
{
workItem.DoWork(1000, cancellationTokenSource.Token);
decProgress.Post(toolStripProgressBar2);
incProgress.Post(toolStripProgressBar3);
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationTokenSource.Token,
MaxDegreeOfParallelism = 2
});
startWork.LinkTo(completeWork);
startWork.Completion.ContinueWith(delegate { completeWork.Complete(); },cancellationTokenSource.Token);
incProgress = new ActionBlock<ToolStripProgressBar>(progress =>
{
progress.Value++;
},
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationTokenSource.Token,
TaskScheduler = uiTaskScheduler
});
decProgress = new ActionBlock<ToolStripProgressBar>(progress => progress.Value--,
new ExecutionDataflowBlockOptions
{
CancellationToken = cancellationTokenSource.Token,
TaskScheduler = uiTaskScheduler
});
}
class WorkItem
{
public void DoWork(int milliseconds, CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested == false)
{
Thread.Sleep(milliseconds);
}
}
}
}
As #SirRufo pointed out, the solution to your question is simply don't re-throw the exception after you've caught it. But to highlight some of the other techniques you can use with dataflow as discussed in the comments I put together a small sample. I've tried to keep the spirit and intent of your original code intact. To that end; the original code didn't show how the flow would complete normally, as opposed to cancelled, so I left it out here as well.
using System;
using System.Data;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using System.Windows.Forms;
namespace WindowsFormsApp1 {
public partial class Form1 : Form {
private CancellationTokenSource cancellationTokenSource;
private TransformBlock<WorkItem, WorkItem> startWork;
private ActionBlock<WorkItem> completeWork;
private IProgress<int> progressBar1Value;
private IProgress<int> progressBar2Value;
public Form1() {
InitializeComponent();
btnCancel.Enabled = false;
}
private async void btnAdd_Click(object sender, EventArgs e) {
if(!btnCancel.Enabled) {
CreatePipeline();
btnCancel.Enabled = true;
}
var data = Enumerable.Range(0, 20).Select(_ => new WorkItem());
foreach(var workItem in data) {
await startWork.SendAsync(workItem);
progressBar1.Value++;
}
}
private async void btnCancel_Click(object sender, EventArgs e) {
btnAdd.Enabled = false;
btnCancel.Enabled = false;
cancellationTokenSource.Cancel();
await completeWork.Completion.ContinueWith(tsk => this.Invoke(new Action(() => this.Text = "Flow Cancelled")),
TaskContinuationOptions.OnlyOnCanceled);
progressBar4.Value += progressBar1.Value;
progressBar4.Value += progressBar2.Value;
// Reset the progress bars that track the number of active work items.
progressBar1.Value = 0;
progressBar2.Value = 0;
// Enable the Add Work Items button.
btnAdd.Enabled = true;
}
private void CreatePipeline() {
cancellationTokenSource = new CancellationTokenSource();
progressBar1Value = new Progress<int>(_ => progressBar1.Value++);
progressBar2Value = new Progress<int>(_ => progressBar2.Value++);
startWork = new TransformBlock<WorkItem, WorkItem>(async workItem => {
await workItem.DoWork(250, cancellationTokenSource.Token);
progressBar1Value.Report(0); //Value is ignored since the progressbar value is simply incremented
progressBar2Value.Report(0); //Value is ignored since the progressbar value is simply incremented
return workItem;
},
new ExecutionDataflowBlockOptions {
CancellationToken = cancellationTokenSource.Token
});
completeWork = new ActionBlock<WorkItem>(async workItem => {
await workItem.DoWork(1000, cancellationTokenSource.Token);
progressBar1Value.Report(0); //Value is ignored since the progressbar value is simply incremented
progressBar2Value.Report(0); //Value is ignored since the progressbar value is simply incremented
},
new ExecutionDataflowBlockOptions {
CancellationToken = cancellationTokenSource.Token,
MaxDegreeOfParallelism = 2
});
startWork.LinkTo(completeWork, new DataflowLinkOptions() { PropagateCompletion = true });
}
}
public class WorkItem {
public async Task DoWork(int milliseconds, CancellationToken cancellationToken) {
if(cancellationToken.IsCancellationRequested == false) {
await Task.Delay(milliseconds);
}
}
}
}
After checking the code, I released that the tasks will be cancelled if I click Cancel.
await Task.WhenAll(
completeWork.Completion,
incProgress.Completion,
decProgress.Completion);
But, above code Task.WhenAll need all of the tasks return complete status, then the "A task was canceled Exception" throw as expected if it returned cancelled instead of completed.
For a possible way to resolve this issue, we should return Task completed if we cancelled the task, and the code below works for me.
await Task.WhenAll(
completeWork.Completion.ContinueWith(task => cancelWork(task, "completeWork"), TaskContinuationOptions.OnlyOnCanceled),
incProgress.Completion.ContinueWith(task => cancelWork(task, "incProgress"), TaskContinuationOptions.OnlyOnCanceled),
decProgress.Completion.ContinueWith(task => cancelWork(task, "decProgress"), TaskContinuationOptions.OnlyOnCanceled));
Is it reasonable?

Is there a difference with Task.Run a void method and Task method returning null?

Consider a form with 2 buttons and a richtextbox:
public partial class MainForm : Form
{
CancellationTokenSource cts;
CancellationToken token;
public MainForm()
{
InitializeComponent();
}
private void MainForm_Load(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
token = cts.Token;
var task = Task.Run(() => WriteSomeLines(), token);
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
cts.Dispose();
}
private void btnStart_Click(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
token = cts.Token;
var task = Task.Run(() => WriteSomeLines(), token);
}
private void btnCancel_Click(object sender, EventArgs e)
{
try
{
cts.Cancel();
cts.Dispose();
}
catch (ObjectDisposedException exc)
{
MessageBox.Show(exc.GetType().Name);
//object disposed
}
}
public void WriteSomeLines()
{
if (ControlInvokeRequired(rtbLoops, () => rtbLoops.Text += "Starting new loop \r\n")) ;
else rtbLoops.Text += "Starting new loop \r\n";
for (int i = 0; i < 30; i++)
{
try
{
if (ControlInvokeRequired(rtbLoops, () => { rtbLoops.AppendText("New line " + i + "\r\n"); rtbLoops.ScrollToCaret(); })) ;
else rtbLoops.AppendText("New line " + i + "\r\n");
Thread.Sleep(250);
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException ae)
{
MessageBox.Show(ae.GetType().Name);
return;
}
}
return;
}
public bool ControlInvokeRequired(Control c, Action a)
{
if (c.InvokeRequired)
c.Invoke(new MethodInvoker(delegate { a(); }));
else
return false;
return true;
}
}
Is there a difference if WriteSomeLines() is returning void and I use return inside, or if WriteSomeLines() returns Task and I do return null there? I read that I cannot use await with void returning methods, but inserting
await task;
after task declaration (in the code above) compiles perfectly fine, and runs with no issues.
Edit:
private async void btnStart_Click(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
token = cts.Token;
var task = Task.Run(() => WriteSomeLines(), token);
await task;
rtbLoops.Text += "Task complete";
}
This compiles with no issues if WriteSomeLines() returns void.
Also, slightly unrealted, am I disposing CancellationTokenSource correctly here?
Second Edit:
So is this the correct approach:
private async void btnStart_Click(object sender, EventArgs e)
{
cts.Dispose();
cts = new CancellationTokenSource();
token = cts.Token;
var task = Task.Run(() => WriteSomeLines(), token);
bool result = await task;
if(result == true) rtbLoops.Text += "Task complete \r\n";
}
and
public async Task<bool> WriteSomeLines()
{
if (ControlInvokeRequired(rtbLoops, () => rtbLoops.Text += "Starting new loop \r\n")) ;
else rtbLoops.Text += "Starting new loop \r\n";
for (int i = 0; i < 30; i++)
{
try
{
if (ControlInvokeRequired(rtbLoops, () => { rtbLoops.AppendText("New line " + i + "\r\n"); rtbLoops.ScrollToCaret(); })) ;
else rtbLoops.AppendText("New line " + i + "\r\n");
await Task.Delay(250);
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException ae)
{
MessageBox.Show(ae.GetType().Name);
return false;
}
}
return true;
You should never return a null task; that should cause a runtime NullReferenceException error.
You can use await within an async void method, but you cannot use await to consume an async void method (because you cannot await void).
I recommend that you review my async intro blog post; it should help you get a better understanding of async and await.
am I disposing CancellationTokenSource correctly here?
Your start button needs to cancel/dispose the old cts when it creates a new one.

error while adding progress bar to a method

i have been working on a windows store project using c#
i have a method called
void TranscodeProgress(IAsyncActionWithProgress<double> asyncInfo, double percent)
{
pg1.Value=percent;
}
when i try to add a progress bar to this it gives me an error
The application called an interface that was marshalled for a different thread. (Exception from HRESULT: 0x8001010E (RPC_E_WRONG_THREAD))
please help me to correct this error
thanks
this is my entire code
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
Windows.Storage.StorageFile source;
Windows.Storage.StorageFile destination;
var openPicker = new Windows.Storage.Pickers.FileOpenPicker();
openPicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
openPicker.FileTypeFilter.Add(".mp4");
openPicker.FileTypeFilter.Add(".wmv");
source = await openPicker.PickSingleFileAsync();
var savePicker = new Windows.Storage.Pickers.FileSavePicker();
savePicker.SuggestedStartLocation =
Windows.Storage.Pickers.PickerLocationId.VideosLibrary;
savePicker.DefaultFileExtension = ".wmv";
savePicker.SuggestedFileName = "New Video";
savePicker.FileTypeChoices.Add("MPEG4", new string[] { ".wmv" });
destination = await savePicker.PickSaveFileAsync();
// Method to perform the transcoding.
TranscodeFile(source, destination);
}
async void TranscodeFile(StorageFile srcFile, StorageFile destFile)
{
MediaEncodingProfile profile =
MediaEncodingProfile.CreateWmv(VideoEncodingQuality.HD1080p);
MediaTranscoder transcoder = new MediaTranscoder();
PrepareTranscodeResult prepareOp = await
transcoder.PrepareFileTranscodeAsync(srcFile, destFile, profile);
if (prepareOp.CanTranscode)
{
var transcodeOp = prepareOp.TranscodeAsync();
transcodeOp.Progress +=
new AsyncActionProgressHandler<double>(TranscodeProgress);
// p1.Value = double.Parse(transcodeOp.Progress.ToString());
// txtProgress.Text = transcodeOp.Progress.ToString();
transcodeOp.Completed +=
new AsyncActionWithProgressCompletedHandler<double>(TranscodeComplete);
}
else
{
switch (prepareOp.FailureReason)
{
case TranscodeFailureReason.CodecNotFound:
MessageDialog md=new MessageDialog("Codec not found.");
await md.ShowAsync();
break;
case TranscodeFailureReason.InvalidProfile:
MessageDialog md1 = new MessageDialog("Invalid profile.");
await md1.ShowAsync();
break;
default:
MessageDialog md2 = new MessageDialog("Unknown failure.");
await md2.ShowAsync();
break;
}
}
//txtDisplay.Text = a;
}
void TranscodeProgress(IAsyncActionWithProgress<double> asyncInfo, double percent)
{
}
void TranscodeComplete(IAsyncActionWithProgress<double> asyncInfo, AsyncStatus status)
{
asyncInfo.GetResults();
if (asyncInfo.Status == AsyncStatus.Completed)
{
// Display or handle complete info.
}
else if (asyncInfo.Status == AsyncStatus.Canceled)
{
// Display or handle cancel info.
}
else
{
// Display or handle error info.
}
}
You should:
Avoid async void.
Use the TAP naming pattern (make your Task-returning methods end in "Async").
Use AsTask to do complex interop between TAP and WinRT asynchronous operations.
Something like this:
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
...
await TranscodeFileAsync(source, destination);
}
async Task TranscodeFileAsync(StorageFile srcFile, StorageFile destFile)
{
MediaEncodingProfile profile =
MediaEncodingProfile.CreateWmv(VideoEncodingQuality.HD1080p);
MediaTranscoder transcoder = new MediaTranscoder();
PrepareTranscodeResult prepareOp = await
transcoder.PrepareFileTranscodeAsync(srcFile, destFile, profile);
if (prepareOp.CanTranscode)
{
var progress = new Progress<double>(percent => { pg1.Value = percent; });
var result = await prepareOp.TranscodeAsync().AsTask(progress);
// Display result.
}
else
{
...
}
}
You are trying to access UI component from non UI Thread.
use:
void TranscodeProgress(IAsyncActionWithProgress<double> asyncInfo, double percent)
{
if(InvokeRequired)
{
Invoke(new MethodInvoker() => TranscodeProgress(asyncInfo, percent));
return;
}
pg1.Value=percent;
}
You cannot access UI components from non UI threads, Calling Invoke with a delegate passes the function call to thread that owns the component and than that thread call the passed delegate.

Categories

Resources