WPF的Image控件导致用户界面冻结

5

我希望在我的WPF应用程序中显示用户的Gravatar。这是我绑定Image-Control的方法:

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

GravatarConverter会返回给定电子邮件的URL。不幸的是,当加载第一张图片时,这会完全阻塞我的UI。请注意,我正在使用“IsAsync=True”。 经过一些研究,我发现可以在应用程序启动时在单独的线程上调用FindServicePoint来解决此问题:

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

但是当FindServicePoint没有完成而应用程序已经下载图像时,这种方法不起作用。请有人解释一下,为什么WPF应用程序需要这个FindServicePoint,为什么会阻塞UI以及如何避免阻塞?
谢谢。
更新:事实证明,当我在Internet选项的“连接”中取消选中“自动检测设置”后,我的问题消失了。
我使用了这个非常简单的WPF应用程序来重现这个问题,只需在文本框中插入图像的URL并单击按钮即可。启用“自动检测设置”后,第一次加载图像时应用程序会冻结几秒钟。禁用此选项后立即加载。
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 ){}            
        }
    }   
}
1个回答

2

阻塞UI发生是因为 IsAsync=True 只在绑定过程中以异步方式运行。在您的情况下,转换过程中有一个长时间运行的操作。要解决此问题,您应该创建一个呈现异步结果的转换器,如下所示(基于这个答案):

创建任务完成通知器:

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;

}

创建实现MarkupExtension的异步转换器:

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

}

在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>

更新2 看起来图像控件本身会异步加载图像。你是对的,第一次加载需要很长时间。你可以使用类似以下代码的方式:

    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) { } 

但它的性能不如你的版本。

非常感谢。不幸的是,这仍然冻结我的用户界面。看起来无论我是否异步执行此操作,问题似乎都在于ServicePointManager.FindServicePoint调用,该调用在每个应用程序中第一次下载远程图像时执行一次。 - zpete
在我的情况下,一切都可以正常运行而不会冻结。您在哪里调用ServicePointManager.FindServicePoint?您能提供一些代码吗?还要注意我设置了IsAsync=True。 - Deffiss
1
好的,我找到了这个“bug”——但是这真的很奇怪:它与Internet Explorer中的互联网设置有关。我在“设置”->“连接”->“局域网设置”中启用了“自动检测设置”。当我停用它时,一切都可以正常加载,但当我启用它时,它就会冻结。所以这似乎是一个操作系统问题? - zpete
这真的很奇怪。我不认为问题在操作系统中。你是否像我在示例中展示的那样,在Task.Run中执行了ServicePointManager.FindServicePoint方法?如果你提供有问题的代码,我帮助你的机会会增加。 - Deffiss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接