-1

I have a wpf app in which one of the functions is to copy files. It functions correctly but can sometimes freeze the UI, so I am trying to add async/await functions to it.

this is the code:

if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
        {
            string[] files = filePickedTextBox.Text.Split('\n');
            await Task.Run(() =>
                {
                    foreach (var file in files)
                    {
                        try
                        {
                            AddValueLoadingBar(20, true);
                            File.Copy(file, Path.Combine(dialog.SelectedPath, Path.GetFileName(file)));
                            newDirTextBox.Text = Path.Combine(dialog.SelectedPath, Path.GetFileName(file));
                        }

                        catch(Exception ex)
                        {
                            newDirTextBox.Text = ex.ToString();
                        }

                        finally
                        {
                            AddValueLoadingBar(100, true);
                        }
                    }                    
                });

I have tried adding dispatcher.invoke in several locations but always get an error that it's asking for a request from a different thread, when it needs to talk to the textbox.

I've been at this for hours and have searched many stackoverflow threads and other documents online but I cant figure out how to implement this.

Also I know my progress bar implementation sucks..but I'm not worried about that at the moment since I cant get this to run without freezing in the first place.

Any help would be great.

Thx

  • This is not a winform so it doesn't use backgroundworker. edited main post to state its wpf – user15919119 May 26 '21 at 22:56
  • Does this answer your question? [Cross-thread operation not valid: Control accessed from a thread other than the thread it was created on](https://stackoverflow.com/questions/142003/cross-thread-operation-not-valid-control-accessed-from-a-thread-other-than-the). You should be using `BeginInvoke` not `Invoke` as the latter can lead to thread _deadlock_ –  May 26 '21 at 23:21
  • 1
    @MattU - `BackgroundWorker` is no longer the way to do this kind of thing. There are a number of better options now. – Enigmativity May 26 '21 at 23:23
  • 1
    @user15919119 - Why re you using a `System.Windows.Forms` dialog box in WPF? – Enigmativity May 26 '21 at 23:39
  • I'll change that. Thx! – user15919119 May 26 '21 at 23:47

2 Answers2

1

Here is where you use the System.Windows.Threading.Dispatcher class.

You know that the dialog constructor will run on the main thread. So when the dialog is created, you record the main thread's dispatcher as a member variable.

Then, inside the Task thread, make sure you dispatch any update changes to the windows thread via this dispatcher.

using System.Windows;

class MyTestDialog
{
    readonly Dispatcher _uiThreadDispatcher = Dispatcher.CurrentDispatcher;

    public async Task DoSomethingInBackround()
    {
        await Task.Run(
             () => 
             {
                //
                // Do a bunch of stuff here
                //
                _uiThreadDispatcher.BeginInvoke(
                     new Action(
                         () => newDirTextBox.Text = "Some progress text";
                     )
                );
                // 
                // Do some more stuff
                // 
             }
        );
    }
}
Andrew Shepherd
  • 44,254
  • 30
  • 139
  • 205
1

Inside your Task.Run you cannot access or update any UI element without invoking the call back on to the UI thread. Every single one of your statements seems to be interacting with the UI. You need to extract out all of the UI work into code before pushing the hard work into a task.

Now, I'd suggest using Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Threading (for the WPF bits) and add using System.Reactive.Linq; - then you can do this:

IDisposable subscription =
    files
        .Select(f => new
        {
            source = f,
            destination = Path.Combine(dialog.SelectedPath, Path.GetFileName(f))
        })
        .ToObservable()
        .SelectMany(x => Observable.Start(() =>
        {
            File.Copy(x.source, x.destination);
            return x.destination;
        }))
        .Select((d, n) => new { destination = d, progress = 100 * (n + 1) / files.Length })
        .ObserveOnDispatcher()
        .Subscribe(
            x =>
            {
                AddValueLoadingBar(x.progress, true);
                newDirTextBox.Text = x.destination; 
            },
            ex => newDirTextBox.Text = ex.ToString(),
            () => AddValueLoadingBar(100, true));

This does everything that's needed to send the actual file copying to a background thread, yet still marshals all of the updates to the UI.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Instead of `.SelectMany` which introduces concurrency, `.Select(...).Merge(1)` is probably closer to what the OP is trying to do. In that case wrapping the `Observable.Start` in a `Observable.Defer` would be probably needed. – Theodor Zoulias May 27 '21 at 13:10
  • @TheodorZoulias - That's probably a fair alternative. – Enigmativity May 27 '21 at 13:15