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.
The combobox will be stuck for 2000 ms.
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.
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.
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.
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.
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));
}
}
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With