Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I invoke a method on the UI thread when using the TPL?

I am working on an MVVM app that performs several tasks in the background, using TPL. The tasks need to report progress to the UI so that a progress dialog can be updated. Since the app is MVVM, the progress dialog is bound to a view model property named Progress, which is updated by a view model method with the signature UpdateProgress(int increment). The background tasks need to call this method to report progress.

I use a method to update the property because it lets each task increment the Progress property by different amounts. So, if I have two tasks, and the first one takes four times as long as the second, the first task calls UpdateProgress(4), and the second task calls UpdateProgress(1). So, progress is at 80% when the first task completes, and at 100% when the second task completes.

My question is really pretty simple: How do I call the view model method from my background tasks? Code is below. Thanks for your help.


The tasks use Parallel.ForEach(), in code that looks like this:

private void ResequenceFiles(IEnumerable<string> fileList, ProgressDialogViewModel viewModel)
{
    // Wrap token source in a Parallel Options object
    var loopOptions = new ParallelOptions();
    loopOptions.CancellationToken = viewModel.TokenSource.Token;

    // Process images in parallel
    try
    {
        Parallel.ForEach(fileList, loopOptions, sourcePath =>
        {
            var fileName = Path.GetFileName(sourcePath);
            if (fileName == null) throw new ArgumentException("File list contains a bad file path.");
            var destPath = Path.Combine(m_ViewModel.DestFolder, fileName);
            SetImageTimeAttributes(sourcePath, destPath);

            // This statement isn't working
            viewModel.IncrementProgressCounter(1);
        });
    }
    catch (OperationCanceledException)
    {
        viewModel.ProgressMessage = "Image processing cancelled.";
    }
}

The statement viewModel.IncrementProgressCounter(1) isn't throwing an exception, but it's not getting through to the main thread. The tasks are called from MVVM ICommand objects, in code that looks like this:

public void Execute(object parameter)
{
    ...

    // Background Task #2: Resequence files
    var secondTask = firstTask.ContinueWith(t => this.ResequenceFiles(fileList, progressDialogViewModel));

    ...
}
like image 531
David Veeneman Avatar asked Dec 22 '22 09:12

David Veeneman


1 Answers

Assuming your ViewModel is constructed on the UI thread (ie: by the View, or in response to a View related event), which is the case nearly always IMO, you can add this to your constructor:

// Add to class:
TaskFactory uiFactory;

public MyViewModel()
{
    // Construct a TaskFactory that uses the UI thread's context
    uiFactory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
}

Then, when you get your event, you can use this to marshal it:

void Something()
{
    uiFactory.StartNew( () => DoSomething() );
}

Edit: I made an util class. It is static but if you want you can create an interface for it and make it nonstatic:

public static class UiDispatcher
{
    private static SynchronizationContext UiContext { get; set; }

    /// <summary>
    /// This method should be called once on the UI thread to ensure that
    /// the <see cref="UiContext" /> property is initialized.
    /// <para>In a Silverlight application, call this method in the
    /// Application_Startup event handler, after the MainPage is constructed.</para>
    /// <para>In WPF, call this method on the static App() constructor.</para>
    /// </summary>
    public static void Initialize()
    {
        if (UiContext == null)
        {
            UiContext = SynchronizationContext.Current;
        }
    }

    /// <summary>
    /// Invokes an action asynchronously on the UI thread.
    /// </summary>
    /// <param name="action">The action that must be executed.</param>
    public static void InvokeAsync(Action action)
    {
        CheckInitialization();

        UiContext.Post(x => action(), null);
    }

    /// <summary>
    /// Executes an action on the UI thread. If this method is called
    /// from the UI thread, the action is executed immendiately. If the
    /// method is called from another thread, the action will be enqueued
    /// on the UI thread's dispatcher and executed asynchronously.
    /// <para>For additional operations on the UI thread, you can get a
    /// reference to the UI thread's context thanks to the property
    /// <see cref="UiContext" /></para>.
    /// </summary>
    /// <param name="action">The action that will be executed on the UI
    /// thread.</param>
    public static void Invoke(Action action)
    {
        CheckInitialization();

        if (UiContext == SynchronizationContext.Current)
        {
            action();
        }
        else
        {
            InvokeAsync(action);
        }
    }

    private static void CheckInitialization()
    {
        if (UiContext == null) throw new InvalidOperationException("UiDispatcher is not initialized. Invoke Initialize() first.");
    }
}

Usage:

void Something()
{
    UiDispatcher.Invoke( () => DoSomething() );
}
like image 191
Pellared Avatar answered Jan 11 '23 03:01

Pellared