使用 TPL 时如何在 UI 线程上调用方法?

发布于 2024-12-03 00:10:32 字数 1825 浏览 1 评论 0原文

我正在开发一个 MVVM 应用程序,它使用 TPL 在后台执行多项任务。任务需要向 UI 报告进度,以便可以更新进度对话框。由于应用程序是 MVVM,因此进度对话框绑定到名为 Progress 的视图模型属性,该属性由具有签名 UpdateProgress(intincrement) 的视图模型方法进行更新。后台任务需要调用该方法来报告进度。

我使用一种方法来更新属性,因为它允许每个任务以不同的量增加 Progress 属性。因此,如果我有两个任务,并且第一个任务花费的时间是第二个任务的四倍,则第一个任务调用 UpdateProgress(4),第二个任务调用 UpdateProgress(1)< /代码>。因此,当第一个任务完成时,进度为 80%,当第二个任务完成时,进度为 100%。

我的问题非常简单:如何从后台任务中调用视图模型方法?代码如下。感谢您的帮助。


这些任务使用 Parallel.ForEach(),代码如下所示:

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.";
    }
}

语句 viewModel.IncrementProgressCounter(1) 不会引发异常,但无法通过到主线程。这些任务是从 MVVM ICommand 对象调用的,代码如下所示:

public void Execute(object parameter)
{
    ...

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

    ...
}

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));

    ...
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

时光倒影 2024-12-10 00:10:32

假设您的 ViewModel 是在 UI 线程上构建的(即:由 View 构建,或响应 View 相关事件),在我看来,情况几乎总是如此,您可以将其添加到构造函数中:

// Add to class:
TaskFactory uiFactory;

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

然后,当您获取事件时,您可以使用它来编组它:

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

编辑:
我做了一个 util 类。它是静态的,但如果您愿意,可以为其创建一个接口并使其成为非静态:

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.");
    }
}

用法:

void Something()
{
    UiDispatcher.Invoke( () => DoSomething() );
}

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() );
}
╰◇生如夏花灿烂 2024-12-10 00:10:32

要将方法调用编组到主 UI 线程,您可以使用 Dispatcher 的 InvokeMethod 方法。如果您使用像 Carliburn 这样的 MVVM 框架,它对 Dispatcher 具有抽象,因此您可以使用 Execute.OnUIThread(Action) 执行几乎相同的操作。

请查看这篇 Microsoft 文章,了解如何使用 Dispatcher。

To marshall method calls to the main UI thread you can use Dispatcher's InvokeMethod method. If you use MVVM Frameworks like Carliburn it has abstractions over Dispatcher so you can do almost the same thing using Execute.OnUIThread(Action).

Check this Microsoft article on how to use Dispatcher.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文