Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I run code inside a Converter on a separate thread so that the UI does not freeze?

I have a WPF Converter which is slow (computations, online fetching, etc.). How can I convert asynchronously so that my UI doesn't freeze up? I found this, but the solution is to place the converter code in the property - http://social.msdn.microsoft.com/Forums/pl-PL/wpf/thread/50d288a2-eadc-4ed6-a9d3-6e249036cb71 - which I would rather not do.

Below is an example which demonstrates the issue. Here the dropdown will freeze until Sleep elapses.

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

and the 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>

Note that all Bindings are now IsAsync="True", but this doesn't help.

enter image description here

The combobox will be stuck for 2000 ms.

like image 561
tofutim Avatar asked Jul 01 '11 00:07

tofutim


People also ask

Is WPF multi threaded?

WPF supports a Single Threaded Apartment Model(STA) like the windows forms which introduces following constraints in a WPF application. The thread that creates a WPF UI element owns the elements and other threads can not interact with the UI elements directly,this is known as thread affinity.

Is WPF single threaded?

WPF supports a single-threaded apartment model that has the following rules: One thread runs in the entire application and owns all the WPF objects. WPF elements have thread affinity, in other words other threads can't interact with each other.

What is the UI thread c#?

A UI thread creates UI elements and waits and responds to events like mouse clicks and key presses. You can only access the UI elements from the UI thread. There are two types of threads: background and foreground. A UI thread is an example of a foreground thread.

What is Dispatcher object in WPF?

WPF Dispatcher is associated with the UI thread. The UI thread queues methods call inside the Dispatcher object. Whenever your changes the screen or any event executes, or call a method in the code-behind all this happen in the UI thread and UI thread queue the called method into the Dispatcher queue.


2 Answers

I know you said you don't want to invoke the translation from the property setter, but I submit that it is a cleaner approach than the IValueConverter/IMultiValueConverter.

Ultimately, you want to set the value of the selected number from the combobox, and return from that immediately. You want to defer updating the displayed/translated value until the translation process is complete.

I think it is clearer to model the data such that the translated value is itself a property that just gets updated by an asynchronous process.

    <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));
    }
}
like image 117
Jay Avatar answered Sep 21 '22 08:09

Jay


You could use a DispatcherFrame for this, here's an example converter:

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();
    }
}
like image 28
H.B. Avatar answered Sep 20 '22 08:09

H.B.