6

我正在开发一个 Windows Metro 应用程序,并且遇到了 UI 变得无响应的问题。据我所知,原因如下:

    <ListView
...
        SelectionChanged="ItemListView_SelectionChanged"            
...

此事件在此处处理:

    async void ItemListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (this.UsingLogicalPageNavigation()) this.InvalidateVisualState();

        MyDataItem dataItem = e.AddedItems[0] as MyDataItem;
        await LoadMyPage(dataItem);
    }

    private async Task LoadMyPage(MyDataItem dataItem)
    {            
        SyndicationClient client = new SyndicationClient();
        SyndicationFeed feed = await client.RetrieveFeedAsync(new Uri(FEED_URI));                    

        string html = ConvertRSSToHtml(feed)
        myWebView.NavigateToString(html, true);            
    }

LoadMyPage需要一段时间才能完成,因为它从 Web 服务获取数据并将其加载到屏幕上。然而,看起来 UI 正在等待它:我的猜测是直到上述事件完成。

所以我的问题是:我能做些什么呢?有没有更好的事件我可以挂钩,还是有另一种方法来处理这个?我想过启动一个后台任务,但这对我来说似乎有点过头了。

编辑:

只是为了澄清这个问题的规模,我说的是最多 3 - 4 秒无响应。这绝不是一项长期的工作。

编辑:

我已经尝试了下面的一些建议,但是,该SelectionChanged函数的整个调用堆栈都在使用 async/await。我已经追踪到这个声明:

myFeed = await client.RetrieveFeedAsync(uri);

在完成之前似乎不会继续处理。

编辑:

我意识到这正在变成战争与和平,但下面是使用空白地铁应用程序和按钮复制问题:

XAML:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel>
        <Button Click="Button_Click_1" Width="200" Height="200">test</Button>
        <TextBlock x:Name="test"/>
    </StackPanel>
</Grid>

后面的代码:

    private async void Button_Click_1(object sender, RoutedEventArgs e)
    {
        SyndicationFeed feed = null;

        SyndicationClient client = new SyndicationClient();
        Uri feedUri = new Uri(myUri);

        try
        {
            feed = await client.RetrieveFeedAsync(feedUri);

            foreach (var item in feed.Items)
            {       
                test.Text += item.Summary.Text + Environment.NewLine;                    
            }
        }
        catch
        {
            test.Text += "Connection failed\n";
        }
    }
4

5 回答 5

5

试试这个...

SyndicationFeed feed = null;

SyndicationClient client = new SyndicationClient();

var feedUri = new Uri(myUri);

try {
    var task = client.RetrieveFeedAsync(feedUri).AsTask();

    task.ContinueWith((x) => {
        var result = x.Result;

        Parallel.ForEach(result.Items, item => {
            Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
            () =>
            {
                test.Text += item.Title.Text;
            });
       });     
   });
}
catch (Exception ex) { }

我通过使用 Grid 应用程序模板向应用程序添加一个按钮,在我的机器上进行了尝试。我可以在更新页面标题时来回滚动项目网格,而不会出现问题。虽然我没有很多项目,但它进展得非常快,所以很难 100% 积极。

于 2012-08-17T22:40:29.453 回答
4

由于您await在前面使用,LoadMyPage我假设它可以编译并返回一个Task. 鉴于此,我创建了一个小例子。

让我们假设LoadMyPage(and Sleep()) 看起来像这样:

public Task<string> LoadMyPage()
{
    return Task<string>.Factory.StartNew(() =>
                                                {
                                                    Sleep(3000);
                                                    return "Hello world";
                                                });
}
static void Sleep(int ms)
{
    new ManualResetEvent(false).WaitOne(ms);
}

看起来XAML像这样:

<StackPanel>
    <TextBlock x:Name="Result" />
    <ListView x:Name="MyList" SelectionChanged="ItemListView_SelectionChanged">
        <ListViewItem>Test</ListViewItem>
        <ListViewItem>Test2</ListViewItem>
    </ListView>
    <Button>Some Button</Button>
    <Button>Some Button2</Button>
</StackPanel>

然后我们可以让SelectionChanged事件处理程序看起来像这样:

private async void ItemListView_SelectionChanged(object sender,
                                                 SelectionChangedEventArgs e)
{
    MyList.IsEnabled = false;
    var result = await LoadMyPage();

    Result.Text = result;

    MyList.IsEnabled = true;
}

返回的TaskLoadMyPage并行运行,这意味着当该任务运行时,UI不应冻结。现在从Task您使用的await. 这将创建一个延续块。

因此,在此示例中,当您选择某些内容时,ListView在整个加载时间内禁用,然后在完成后重新启用Task。您可以通过按下按钮来验证 UI 没有冻结,以查看它是否仍然响应。

如果LoadMyPage与 UI 交互,您需要稍微重新安排它,让它返回 aViewModel或您想要的结果,然后在 UI 线程上再次将所有内容放在一起。

于 2012-08-09T07:47:21.683 回答
2

后台线程绝对不是矫枉过正。这正是您处理此类问题的方式。

不要在 UI 线程上执行冗长的任务,否则你会占用 UI 并导致它变得无响应。在后台线程上运行这些,然后让该线程在完成时引发一个可由主 UI 线程处理的事件。

在 UI 线程上显示某种进度指示器也很有用。用户喜欢知道正在发生的事情。这会让他们放心,应用程序没有损坏或冻结,他们愿意再等一会儿。这就是为什么所有的网络浏览器都有某种“悸动”或其他加载指示器的原因。

于 2012-08-09T07:34:03.320 回答
2

最可能的问题是LoadMyPage同步做某事。请记住,async不要在后台线程上运行您的代码;默认情况下,它的所有实际代码都将在 UI 线程上运行(请参阅async/await 常见问题解答或我的async/await 介绍)。因此,如果您阻塞异步方法,它仍然会阻塞调用线程。

看看LoadMyPage。它是await用来调用网络服务的吗?是否在将数据放入 UI 之前对数据进行昂贵的处理?它是否会压倒 UI(许多 Windows 控件在处理数千个元素时存在可伸缩性问题)?

于 2012-08-09T10:26:59.427 回答
2

查看您的简化代码示例,我相信您的问题是等待线之外的所有内容。

在以下代码块中:

private async void Button_Click_1(object sender, RoutedEventArgs e)
    {
        SyndicationFeed feed = null;

        SyndicationClient client = new SyndicationClient();
        Uri feedUri = new Uri(myUri);

        try
        {
            feed = await client.RetrieveFeedAsync(feedUri);

            foreach (var item in feed.Items)
            {       
                test.Text += item.Summary.Text + Environment.NewLine;                    
            }
        }
        catch
        {
            test.Text += "Connection failed\n";
        }
    }

在后台线程上执行的唯一行是行

feed = await client.RetrieveFeedAsync(feedUri);

该块中的所有其他代码行都在 UI 线程上执行。

仅仅因为您的按钮单击处理程序被标记为异步并不意味着其中的代码不会在 UI 线程上运行。事实上,事件处理程序在 UI 线程上启动。因此,创建 SyndicationClient 和设置 Uri 发生在 UI 线程上。

许多开发人员没有意识到,await 之后的任何代码都将自动在await之前使用的同一线程上恢复。这意味着代码

            foreach (var item in feed.Items)
            {       
                test.Text += item.Summary.Text + Environment.NewLine;                    
            }

在 UI 线程上运行!

这很方便,因为您不必执行 Dispatcher.Invoke 来更新test.Text,但这也意味着您在循环遍历项目和连接字符串的整个过程中都在阻塞 UI 线程。

在您的(尽管是简化的)示例中,在后台线程上执行此工作的最简单方法是在 SyndicationClient 上使用另一种方法,称为RetrieveFeedAsStringAsync;然后 SyndicationClient 可以将字符串的下载、循环和连接都作为它自己任务的一部分。该任务完成后,将在 UI 线程上运行的唯一代码行是将文本分配给 TextBox。

于 2012-08-14T16:00:07.747 回答