Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF's Imagecontrol freezes the UI

I want to display a Users Gravatar in my WPF-Application. This is how I bind the Image-Control:

<Image Source="{Binding Path=Email, Converter={StaticResource GravatarConverter},IsAsync=True}">

Where GravatarConverter returns the URL for the given Email. Unfortunately this is blocking my UI completely when loading the first Image. Please note that I am using "IsAsync=True". After some research I found out that I can hack around this issue when calling FindServicePoint in a separate thread on applications startup:

        Task.Factory.StartNew( () => ServicePointManager.FindServicePoint( "http://www.gravatar.com", WebRequest.DefaultWebProxy ) );

But this is not working when FindServicePoint isn't finished while my Application is already downloading an image. Can someone please explain why a WPF-App needs this FindServicePoint at all, why this is blocking the UI and how to avoid the blocking?

Thanks

Update: As it turns out my problem disappeared after I unchecked "Automatic detect settings" in Internet Explorers "Internet Options"->"Connections"->"LAN Settings".

I used this VERY simple WPF-Application to reproduce the problem just by inserting an url for an image in the textbox and click the button. With "Automatic detect settings" enabled the app freezes for some seconds the first time an image gets loaded. With this option disabled its loading immediately.

MainWindow.xaml

<Window x:Class="WpfGravatarFreezeTest.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBox Grid.Column="0" Grid.Row="0" HorizontalAlignment="Stretch" x:Name="tbEmail" />
    <Button Grid.Column="0" Grid.Row="0" Click="buttonLoad_OnClick" HorizontalAlignment="Right">Set Source</Button>
    <Image x:Name="img" Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2" />
</Grid>        

MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Media.Imaging;

namespace WpfGravatarFreezeTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void buttonLoad_OnClick( object sender, RoutedEventArgs e )
        {
            try { this.img.Source = new BitmapImage(new Uri(this.tbEmail.Text)); }
            catch( Exception ){}            
        }
    }   
}
like image 476
user1130329 Avatar asked Nov 02 '22 12:11

user1130329


1 Answers

Blocking UI happans because IsAsync=True runs in async manner only binding process. In your case you have a long running operation during converting process. To solve this you should create converter that presents the result asynchronously like this (based on this answer):

Create task complition notifier:

public sealed class TaskCompletionNotifier<TResult> : INotifyPropertyChanged
{
    public TaskCompletionNotifier(Task<TResult> task)
    {
        Task = task;
        if (task.IsCompleted) return;
        task.ContinueWith(t =>
        {
            var temp = PropertyChanged;
            if (temp != null)
            {
                temp(this, new PropertyChangedEventArgs("Result"));
            }
        }); 
    }

    // Gets the task being watched. This property never changes and is never <c>null</c>.
    public Task<TResult> Task { get; private set; }

    // Gets the result of the task. Returns the default value of TResult if the task has not completed successfully.
    public TResult Result { get { return (Task.Status == TaskStatus.RanToCompletion) ? Task.Result : default(TResult); } }

    public event PropertyChangedEventHandler PropertyChanged;

}

Create async converter implementing MarkupExtention:

public class ImageConverter: MarkupExtension, IValueConverter
{

    public ImageConverter()
    {
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return new BitmapImage();
        var task = Task.Run(() =>
        {
            Thread.Sleep(5000); // Perform your long running operation and request here
            return value.ToString();
        });

        return new TaskCompletionNotifier<string>(task);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }

}

Use it in Xaml:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <TextBox x:Name="uri" Grid.Row="0" Text="{Binding ImageUri, ElementName=main}"/>
    <Image Grid.Row="1" DataContext="{Binding Text, ElementName=uri, Converter={local:ImageConverter}}" Source="{Binding Path=Result, IsAsync=True}"/>

</Grid>

Update 2 Seems like Image control load images asynchronously itself. You are right first load take a lot of time. You may use code like this:

    try
    {
        var uri = Uri.Text;
        var client = new WebClient();
        var stream = await client.OpenReadTaskAsync(uri);
        var source = new BitmapImage();
        source.BeginInit();
        source.StreamSource = stream;
        source.EndInit();
        Img.Source = source;


    }
    catch (Exception) { } 

But its performance is't better than your variant.

like image 98
Deffiss Avatar answered Nov 09 '22 23:11

Deffiss