【问题标题】:WinRT - Loading data while keeping the UI responsiveWinRT - 在保持 UI 响应的同时加载数据
【发布时间】:2012-08-09 07:30:06
【问题描述】:

我正在开发一个 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";
        }
    }

【问题讨论】:

  • “无响应”是什么意思?如果您在页面上放置一个按钮并尝试将鼠标悬停,您会看到鼠标悬停的行为吗?你可以与页面交互吗?您是否尝试在 LoadPage 期间转换页面?如果 RetrieveFeedAsync 方法需要一段时间才能完成,它不会锁定 UI,但看起来好像什么都没有发生。
  • 屏幕上的控件没有响应。鼠标移动,但应用的 UI 暂时不可用。
  • 返回的 Feed 中有多少商品?
  • 在上面的例子中,有 50 个......但是如果我完全删除 foreach 循环,我会得到相同的行为
  • 我在任何地方都看不到 LoadMyPage 的代码...请包括在内。理想情况下,请将其简化为一个简短但完整的程序来演示问题。

标签: c# .net windows-8 microsoft-metro windows-runtime


【解决方案1】:

试试这个...

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% 积极。

【讨论】:

  • 这行得通(我离开了我们的 Parallel ForEach) - 谢谢。我的印象是 await 有效地做了同样的事情 - 那么这有什么 await 没有呢?
  • 使用 await 时,编译器会自动将异步调用完成后的代码编组回 UI 线程。在后台使用任务来完成此操作,但编译器的魔法将后台处理的机会移回主线程。在我的代码中,我们明确控制任务并使用它继续在自己的线程上工作,而不是返回 UI 线程。根据您循环的数据量以及您希望 UI 更新的方式,您可能希望坚持使用 Parallel.ForEach
  • 你在那里泄漏了异常,不是吗?该任务可以快速启动,并且延续不在您有异常处理的同一个同步上下文中。
【解决方案2】:

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

让我们假设LoadMyPage(和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;
}

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

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

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

【讨论】:

  • 不幸的是我不能这样做,因为我使用的是异步方法RetrieveFeedAsync
  • -1:问题是关于异步的,而不是关于长时间运行的项目,你不能在 WinRT 中睡觉。
  • @Henrik,阅读我的答案并再次检查代码示例。我正在使用ManualResetEvent(false).WaitOne(ms) 模拟Sleep。我看不出我的答案与async 无关,您能澄清一下吗?
  • @Henrik,不是吗? Task 异步运行,在 ItemListView_SelectionChanged 中是 awaited
  • 但是重置事件上的 WaitOne 不是异步操作。我认为您混淆了两个概念;并发和异步。您假设有一个线程池运行您可以阻止线程的任务,然后您没有异步执行它。也许我对你的回答的看法是,这个例子是“睡眠”,而不是解决 GUI 应该响应的问题。当您在 GUI 同步上下文中等待时,您正在创建 OP 想要摆脱的相同的无响应行为。而是在完成后执行 SynchronizationContext.Post 到 GUI。
【解决方案3】:

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

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

在 UI 线程上显示某种进度指示器也很有用。用户喜欢知道正在发生的事情。这将使他们放心,该应用程序没有损坏或冻结,并且他们愿意等待更长的时间。这就是为什么所有的网络浏览器都有某种“跳动”或其他加载指示器的原因。

【讨论】:

    【解决方案4】:

    最可能的问题是LoadMyPage 正在同步执行某些操作。请记住,async 不会在后台线程上运行您的代码。默认情况下,它的所有实际 代码 都将在 UI 线程上运行(请参阅 async/await FAQ 或我的 async/await intro)。所以,如果你阻塞在一个异步方法中,它仍然会阻塞调用线程。

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

    【讨论】:

    • 查看最新编辑 - 它所做的最昂贵的事情是调用网络。它需要 1 到 5 秒,但在 UI 存在时会停止。
    • 试试这个测试:在调用RetrieveFeedAsync之前将LoadMyPage更改为调用await Task.Run(() =&gt; {}).ConfigureAwait(false)。会发生什么?
    • 没什么区别
    【解决方案5】:

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

    在以下代码块中:

    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。

    【讨论】:

    • 但是如果我完全删除 foreach() 循环,我会得到相同的行为。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-21
    • 2013-03-11
    • 2016-10-27
    • 1970-01-01
    • 2011-05-31
    • 1970-01-01
    相关资源
    最近更新 更多