【问题标题】:How can I run code inside a Converter on a separate thread so that the UI does not freeze?如何在单独的线程上运行转换器内的代码,以使 UI 不会冻结?
【发布时间】:2011-09-26 10:05:25
【问题描述】:

我有一个速度很慢的 WPF 转换器(计算、在线获取等)。如何异步转换以使我的 UI 不会冻结?我找到了这个,但解决方案是将转换器代码放在属性中 - http://social.msdn.microsoft.com/Forums/pl-PL/wpf/thread/50d288a2-eadc-4ed6-a9d3-6e249036cb71 - 我宁愿不这样做。

以下是演示该问题的示例。此处下拉菜单将冻结,直到睡眠结束。

namespace testAsync
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Threading;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MyNumbers = new Dictionary<string, int> { { "Uno", 1 }, { "Dos", 2 }, { "Tres", 3 } };

            this.DataContext = this;           
        }

        public Dictionary<string, int> MyNumbers
        {
            get { return (Dictionary<string, int>)GetValue(MyNumbersProperty); }
            set { SetValue(MyNumbersProperty, value); }
        }
        public static readonly DependencyProperty MyNumbersProperty =
            DependencyProperty.Register("MyNumbers", typeof(Dictionary<string, int>), typeof(MainWindow), new UIPropertyMetadata(null));


        public string MyNumber
        {
            get { return (string)GetValue(MyNumberProperty); }
            set { SetValue(MyNumberProperty, value); }
        }
        public static readonly DependencyProperty MyNumberProperty = DependencyProperty.Register(
            "MyNumber", typeof(string), typeof(MainWindow), new UIPropertyMetadata("Uno"));
    }

    public class AsyncConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            object result = null;


            if (values[0] is string && values[1] is IDictionary<string, int>)
            {
                DoAsync(
                    () =>
                        {
                                            Thread.Sleep(2000); // Simulate long task
                            var number = (string)(values[0]);
                            var numbers = (IDictionary<string, int>)(values[1]);

                            result = numbers[number];
                            result = result.ToString();
                        });
            }

            return result;
        }

        private void DoAsync(Action action)
        {
            var frame = new DispatcherFrame();
            new Thread((ThreadStart)(() =>
            {
                action();
                frame.Continue = false;
            })).Start();
            Dispatcher.PushFrame(frame);
        }

        public object[] ConvertBack(object value, Type[] targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }

和 XAML:

<Window x:Class="testAsync.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:testAsync"
        Title="MainWindow" Height="200" Width="200">
    <Window.Resources>
        <local:AsyncConverter x:Key="asyncConverter"/>
    </Window.Resources>
    <DockPanel>
        <ComboBox DockPanel.Dock="Top" SelectedItem="{Binding MyNumber, IsAsync=True}"                   
                  ItemsSource="{Binding MyNumbers.Keys, IsAsync=True}"/>
        <TextBlock DataContext="{Binding IsAsync=True}"
            FontSize="50" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock.Text>
                <MultiBinding Converter="{StaticResource asyncConverter}">
                    <Binding Path="MyNumber" IsAsync="True"/>
                    <Binding Path="MyNumbers" IsAsync="True"/>
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
    </DockPanel>
</Window>

请注意,所有绑定现在都是 IsAsync="True",但这没有帮助。

组合框将被卡住 2000 毫秒。

【问题讨论】:

  • "..."translator" 当前在 UI 线程上运行 - 但我认为即使在其自己的线程上它也会导致 UI 冻结。” - 这绝对是没有意义;如果 UI 线程不工作,则无法冻结 UI。不知道为什么有人投了反对票,所以 +1 甚至得分。

标签: c# wpf multithreading data-binding ivalueconverter


【解决方案1】:

我知道您说过您不想从属性设置器调用翻译,但我认为它比IValueConverter/IMultiValueConverter 更简洁。

最终,您希望从组合框中设置所选数字的值,然后立即返回。您想推迟更新显示/翻译的值,直到翻译过程完成。

我认为对数据建模更清楚,这样翻译后的值本身就是一个由异步进程更新的属性。

    <ComboBox SelectedItem="{Binding SelectedNumber, Mode=OneWayToSource}"                   
              ItemsSource="{Binding MyNumbers.Keys}"/>
    <TextBlock Text="{Binding MyNumberValue}" />

public partial class MainWindow : Window, INotifyPropertyChanged
{
    public MainWindow()
    {
        InitializeComponent();

        MyNumbers = new Dictionary<string, int> { { "Uno", 1 }, { "Dos", 2 }, { "Tres", 3 } };

        DataContext = this;   
    }

    public IDictionary<string, int> MyNumbers { get; set; }

    string _selectedNumber;
    public string SelectedNumber
    {
        get { return _selectedNumber; }
        set
        {
            _selectedNumber = value;
            Notify("SelectedNumber");
            UpdateMyNumberValue();
        }
    }

    int _myNumberValue;
    public int MyNumberValue
    {
        get { return _myNumberValue; }
        set 
        { 
            _myNumberValue = value;
            Notify("MyNumberValue");
        }
    }

    void UpdateMyNumberValue()
    {
        var key = SelectedNumber;
        if (key == null || !MyNumbers.ContainsKey(key)) return;

        new Thread(() =>
        {
            Thread.Sleep(3000);
            MyNumberValue = MyNumbers[key];
        }).Start();
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void Notify(string property)
    {
        var handler = PropertyChanged;
        if(handler != null) handler(this, new PropertyChangedEventArgs(property));
    }
}

【讨论】:

  • 我同意这是一种更清洁的方法(我通常会采用这条路线),尽管它不能回答问题,这实际上与包装有关。我想为未来的开发者用户提供一种将最终结果用作转换器的方法。
  • @tofutim:为什么要提供不推荐的解决方案?值转换在其目标元素的线程上执行。 IsAsyncs 属性作用于属性 getter,不影响转换。我确信最好的答案是不要使用转换器进行长时间操作。
  • +1。确切地说,转换器是您的 UI 逻辑的一部分,因此应该是短期运行的。方钉,圆孔。
【解决方案2】:

您可以为此使用DispatcherFrame,这是一个示例转换器:

public class AsyncConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        object result = null;
        DoAsync(() =>
        {
            Thread.Sleep(2000); // Simulate long task
            result = (int)value * 2; // Some sample conversion
        });
        return result;
    }

    private void DoAsync(Action action)
    {
        var frame = new DispatcherFrame();
        new Thread((ThreadStart)(() =>
        {
            action();
            frame.Continue = false;
        })).Start();
        Dispatcher.PushFrame(frame);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

【讨论】:

  • @H.B.我试过这个,但 UI 冻结了 Thread.Sleep 中的数量加上我的转换。具体来说,转换器由其中一个绑定对象的更改触发,该对象又由组合框更改。在转换转换之前,组合框列表不会释放 - 所以看起来程序被冻结了。我的翻译代码确实在 UI 线程上,但即使只有 sleep 也会发生这种情况。
  • 这是睡眠(2000)的 DispatcherFrame(如您所介绍的)的实现:screencast.com/t/6RvuNXriknJN
  • @tofutim:最好是一些独立的代码来说明问题并让我(或其他人)重现它。
  • 我发现如果你使用这种绑定到 `ComboBox.SelectedItem' 的方法,你必须单击该项目两次才能更改选择。
  • 已更新。我认为问题可能在于组合框在整个更新过程完成之前一直保持打开状态。
【解决方案3】:

在转换器中进行大量计算并不是一个好的设计 - 特别是如果您正在制作其他人应该使用的功能作为一个很好的例子。

我会重写并使用 MVVM 和你的 ViewModel 作为类固醇上的转换器,你可以以透明的方式做所有这些事情 - 更容易编程,更容易理解的程序流,更容易理解代码。

然后你可以使用优先绑定:

http://msdn.microsoft.com/en-us/library/system.windows.data.prioritybinding.aspx

对于您最初的问题,我会查看何时调用转换器 - 如果是在绑定返回其值时,您可能无法让 Async 完成它的工作。我怀疑 wpf 等待属性返回然后调用转换器 - 在这种情况下,可能无法让您的转换器不冻结 gui。

您可以采取的方法:

  • 在您的转换器中,您应该开始获取数据并返回,例如使用 backgroundworker - 否则 ui 将冻结。
  • 在多重绑定中传递对某事物的引用,这样当您的数据到达时,您可以触发 propertychanged

【讨论】:

    【解决方案4】:

    我建议查看BackgroundWorker。它可以在后台线程上执行翻译,然后在 UI 线程上引发完成事件。

    http://www.dotnetperls.com/backgroundworker

    【讨论】:

    • 由于 UIThread 调用转换方法,该方法期望返回转换后的值,我想不出用 BackgroundWorker 完成此操作的方法,您能详细说明一下吗?
    • @H.B.这很容易,只需在事件或属性设置器中执行此操作,并在后台工作人员完成时触发 PropertyChanged。这也是我会推荐的 - 转换器中的异步内容不是一个好方法 - 在你拥有价值之前你不能从你的价值转换器返回。
    猜你喜欢
    • 1970-01-01
    • 2015-05-06
    • 2015-11-04
    • 1970-01-01
    • 2016-04-22
    • 2014-02-22
    • 2019-10-31
    • 2015-10-23
    • 2015-10-22
    相关资源
    最近更新 更多