WinRT - 在保持UI响应的同时加载数据

时间:2012-08-09 07:30:07

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

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

在完成之前似乎没有继续处理。

编辑:

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

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

5 个答案:

答案 0 :(得分: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) { }

我在我的机器上通过使用网格应用模板向应用添加按钮来尝试它。我可以来回滚动项目网格,同时更新页面标题没有问题。虽然我没有很多项目,但速度非常快,所以很难100%肯定。

答案 1 :(得分:4)

由于您在await前面使用LoadMyPage我假设它已编译并返回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;
}

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

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

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

答案 2 :(得分:2)

后台线程肯定过度杀伤。这正是你处理这类问题的方法。

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

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

答案 3 :(得分:2)

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

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

答案 4 :(得分:2)

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

在以下代码块中:

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之后来的任何代码将自动在之前使用的同一个线程上恢复。这意味着代码

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

正在UI线程上运行!

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

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