14

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.

tofutim
  • 22,664
  • 20
  • 87
  • 148
  • *"..."translator" currently runs on the UI thread - but I think even on its own thread it would cause the UI to freeze."* - That makes absolutely no sense; you can't freeze the UI if the UI thread isn't doing the work. Not sure why someone voted this down though, so +1 to even the score. – Ed S. Jul 01 '11 at 01:17

4 Answers4

6

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));
    }
}
Jay
  • 56,361
  • 10
  • 99
  • 123
  • I agree that this a cleaner approach (I would normally take this route), though it doesn't answer the question, which really has to do with packaging. I would like to offer future developer-users a way to use the end result as just a converter. – tofutim Jul 01 '11 at 05:52
  • 2
    @tofutim: Why do you want to offer solution which is not recommended? Value conversion performs on a thread of its target element. The IsAsyns property act on property getters and do not affect conversion. I'm sure the best answer is not to use converter for long-time operations. – Marat Khasanov Jul 01 '11 at 06:20
  • 1
    +1. Exactly, converters are part of your UI logic so should be short-running. Square peg, round hole. – Kent Boogaart Jul 01 '11 at 06:56
5

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();
    }
}
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • @H.B. I tried this, but the UI freezes for the amount in Thread.Sleep plus my conversion. Specifically, the Converter is triggered by a change in one of the bound objects which in turn is changed by a combobox. The combobox list does not release until the conversion is converted - so it looks like the program is frozen. It's true that my translate code is on the UI thread, but this happens even if just sleep is there. – tofutim Jul 01 '11 at 01:39
  • This is an implementation of the DispatcherFrame (as you presented) for sleep(2000): http://screencast.com/t/6RvuNXriknJN – tofutim Jul 01 '11 at 01:42
  • @tofutim: Preferrably some bit of standalone code which illustrates the problem and lets me (or others) reproduce it. – H.B. Jul 01 '11 at 01:51
  • I find that if you use this approach binded to `ComboBox.SelectedItem' you must click the item twice in order to change the selection. – J Cooper Jul 01 '11 at 05:52
  • Updated. I think the problem might be that the combobox is holding itself open until the whole process of updating is complete. – tofutim Jul 01 '11 at 05:54
  • Not working, `InvalidOperationException` (Cannot perform this operation while dispatcher processing is suspended) on `Dispatcher.PushFrame(frame)` – deerchao Nov 24 '12 at 16:20
  • @deerchao: Worked for me when i wrote it, maybe the code is called when not everything is loaded yet in your case, don't know. (Wouldn't recommend this anyway...) – H.B. Nov 24 '12 at 23:57
1

It is not a good design to have heavy computations in a converter - especially if you are making functionality others should use as a good example.

I would rewrite and use MVVM with your ViewModel as converter on steroids where you can do all those things in a transparent way - easier to program, more understandable programflow, easier to understand code.

And then you could utilize Prioritybindings:

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

For your original problem I would look at when the converter is called - if it is when the binding has returned its value, you probably can't get Async to do what it does. I suspect that wpf waits for the property to return and then calls the converter - in that case it might not be possible to get your converter to not freeze gui.

An approach you could take:

  • In your converter you should start fetching your data and return for example with backgroundworker - otherwise ui will freeze.
  • In the multibinding pass a reference to something so when your data arrives you can fire propertychanged
Rune Andersen
  • 1,650
  • 12
  • 15
0

I suggest looking at the BackgroundWorker. It can perform the translation on a background thread and then raises a completed event on the UI thread.

See http://www.dotnetperls.com/backgroundworker

Richard Schneider
  • 34,944
  • 9
  • 57
  • 73
  • 1
    As the UIThread calls the convert method which expects that the converted value is returned i cannot think of a way to get this done with a BackgroundWorker, could you elaborate on this? – H.B. Jul 01 '11 at 01:23
  • @H.B. That is easy enough just do it on events or in the property setter and fire PropertyChanged when the backgroundworker finishes. That is what I would recomend too - async stuff in a converter is not a good way to do it - you can't return from your value converter before you have the value. – Rune Andersen Jul 01 '11 at 07:01