天天看點

第十九章:集合視圖(十六)

重新整理内容

如您所見,如果您使用ObservableCollection作為ListView的源,則對集合的任何更改都會導緻ObservableCollection觸發CollectionChanged事件,而ListView會通過重新整理項目的顯示來響應。

有時,這種類型的重新整理必須由使用者控制的東西補充。例如,考慮一個電子郵件用戶端或RSS閱讀器。這樣的應用程式可能被配置為每15分鐘左右查找新的電子郵件或RSS檔案的更新,但是使用者可能有點不耐煩并且可能希望程式立即檢查新資料。

為此,開發了ListView支援的約定。如果ListView的IsPullToRefresh屬性設定為true,并且使用者向下滑動ListView,則ListView将通過調用綁定到其RefreshCommand屬性的ICommand對象的Execute方法來響應。 ListView還将其IsRefreshing屬性設定為true并顯示某種動畫,表明它正忙。

實際上,ListView并不忙。隻是等待通知新資料可用。您可能已經編寫了由ICommand對象的Execute方法調用的代碼來執行異步操作,例如Web通路。它必須通過将ListView的IsRefreshing屬性設定為false來通知ListView它已完成。此時,ListView顯示新資料并完成重新整理。

這聽起來有點複雜,但如果您将這個功能建構到提供資料的ViewModel中,它會變得更加容易。整個過程通過一個名為RssFeed的程式進行示範,該程式通路來自NASA的RSS源。

RssFeedViewModel類負責使用RSS提要下載下傳XML并對其進行解析。首先在設定Url屬性并且set通路器調用LoadRssFeed方法時發生:

public class RssFeedViewModel : ViewModelBase
{
    string url, title;
    IList<RssItemViewModel> items;
    bool isRefreshing = true;
    public RssFeedViewModel()
    {
        RefreshCommand = new Command(
            execute: () =>
            {
                LoadRssFeed(url);
            },
            canExecute: () =>
            {
                return !IsRefreshing;
            });
    }
    public string Url
    {
        set
        {
            if (SetProperty(ref url, value) && !String.IsNullOrEmpty(url))
            {
                LoadRssFeed(url);
            }
        }
        get
        {
            return url;
        }
    }
    public string Title
    {
        set { SetProperty(ref title, value); }
        get { return title; }
    }
    public IList<RssItemViewModel> Items
    {
        set { SetProperty(ref items, value); }
        get { return items; }
    }
    public ICommand RefreshCommand { private set; get; }
    public bool IsRefreshing 
    {   
        set { SetProperty(ref isRefreshing, value); }
        get { return isRefreshing; }
    }
    public void LoadRssFeed(string url)
    {
        WebRequest request = WebRequest.Create(url);
        request.BeginGetResponse((args) =>
        {
            // Download XML.
            Stream stream = request.EndGetResponse(args).GetResponseStream();
            StreamReader reader = new StreamReader(stream);
            string xml = reader.ReadToEnd();
            // Parse XML to extract data from RSS feed.
            XDocument doc = XDocument.Parse(xml);
            XElement rss = doc.Element(XName.Get("rss"));
            XElement channel = rss.Element(XName.Get("channel"));
            // Set Title property.
            Title = channel.Element(XName.Get("title")).Value;
            // Set Items property.
            List<RssItemViewModel> list = 
                channel.Elements(XName.Get("item")).Select((XElement element) =>
                {
                    // Instantiate RssItemViewModel for each item.
                    return new RssItemViewModel(element);
                }).ToList();
            Items = list;
            // Set IsRefreshing to false to stop the 'wait' icon.
            IsRefreshing = false;
        }, null);
    }
}           

LoadRssFeed方法使用System.Xml.Linq命名空間中的LINQ-to-XML接口來解析XML檔案,并設定類的Title屬性和Items屬性。 Items屬性是RssItemViewModel對象的集合,它定義與RSS提要中的每個項目關聯的五個屬性。 對于XML檔案中的每個item元素,LoadRssFeed方法執行個體化一個RssItemViewModel對象:

public class RssItemViewModel
{
    public RssItemViewModel(XElement element)
    {
        // Although this code might appear to be generalized, it is
        // actually based on desired elements from the particular 
        // RSS feed set in the RssFeedPage.xaml file.
        Title = element.Element(XName.Get("title")).Value;
        Description = element.Element(XName.Get("description")).Value;
        Link = element.Element(XName.Get("link")).Value;
        PubDate = element.Element(XName.Get("pubDate")).Value;
        // Sometimes there's no thumbnail, so check for its presence.
        XElement thumbnailElement = element.Element(
        XName.Get("thumbnail", "http://search.yahoo.com/mrss/"));
        if (thumbnailElement != null)
        {
            Thumbnail = thumbnailElement.Attribute(XName.Get("url")).Value;
        }
    }
    public string Title { protected set; get; }
    public string Description { protected set; get; }
    public string Link { protected set; get; }
    public string PubDate { protected set; get; }
    public string Thumbnail { protected set; get; }
}           

RssFeedViewModel的構造函數還将其RefreshCommand屬性設定為等于具有Execute方法的Command對象,該方法也調用LoadRssFeed,該方法通過将類的IsRefreshing屬性設定為false來完成。為避免Web通路重疊,隻有IsRefreshing為false時,RefreshCommand的CanExecute方法才傳回true。

請注意,RssFeedViewModel中的Items屬性不必是ObservableCollection,因為一旦建立了Items集合,集合中的項就不會更改。當LoadRssFeed方法擷取新資料時,它會建立一個全新的List對象,并将其設定為Items屬性,進而觸發PropertyChanged事件。

下面顯示的RssFeedPage類執行個體化RssFeedViewModel并配置設定Url屬性。此對象成為StackLayout的BindingContext,其中包含用于顯示Title屬性和ListView的Label。 ListView的ItemsSource,RefreshCommand和IsRefreshing屬性都綁定到RssFeedViewModel中的屬性:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:RssFeed"
             x:Class="RssFeed.RssFeedPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="10, 20, 10, 0"
                    Android="10, 0"
                    WinPhone="10, 0" />
    </ContentPage.Padding>
 
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:RssFeedViewModel x:Key="rssFeed"
                    Url="http://earthobservatory.nasa.gov/Feeds/rss/eo_iotd.rss" />
        </ResourceDictionary>
    </ContentPage.Resources>
    <Grid>
        <StackLayout x:Name="rssLayout"
                     BindingContext="{StaticResource rssFeed}">
            <Label Text="{Binding Title}"
                   FontAttributes="Bold"
                   HorizontalTextAlignment="Center" />
            <ListView x:Name="listView"
                      ItemsSource="{Binding Items}"
                      ItemSelected="OnListViewItemSelected"
                      IsPullToRefreshEnabled="True"
                      RefreshCommand="{Binding RefreshCommand}"
                      IsRefreshing="{Binding IsRefreshing}">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ImageCell Text="{Binding Title}"
                                   Detail="{Binding PubDate}"
                                   ImageSource="{Binding Thumbnail}" />
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
        <StackLayout x:Name="webLayout"
                     IsVisible="False">
            <WebView x:Name="webView" 
                     VerticalOptions="FillAndExpand" />
            <Button Text="&lt; Back to List"
                    HorizontalOptions="Center"
                    Clicked="OnBackButtonClicked" />
        </StackLayout>
    </Grid>
</ContentPage>           

這些項目非常适合ImageCell,但可能不适用于Windows 10移動裝置:

第十九章:集合視圖(十六)

當您在此清單中向下滑動手指時,ListView将通過調用RefreshCommand對象的Execute方法并顯示訓示其忙碌的動畫進入重新整理模式。 當RssFeedViewModel将IsRefreshing屬性設定回false時,ListView将顯示新資料。 (這不是在Windows運作時平台上實作的。)

此外,該頁面包含另一個StackLayout,它位于XAML檔案的底部,其IsVisible屬性設定為false。 帶有ListView的第一個StackLayout和第二個隐藏的StackLayout共享一個單元格網格,是以它們基本上都占據整個頁面。

當使用者選擇ListView中的項時,代碼隐藏檔案中的ItemSelected事件處理程式使用ListView隐藏StackLayout并使第二個StackLayout可見:

public partial class RssFeedPage : ContentPage
{
    public RssFeedPage()
    {
        InitializeComponent();
    }
    void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
    {
        if (args.SelectedItem != null)
        {
            // Deselect item.
            ((ListView)sender).SelectedItem = null;
            // Set WebView source to RSS item
            RssItemViewModel rssItem = (RssItemViewModel)args.SelectedItem;
            // For iOS 9, a NSAppTransportSecurity key was added to
            // Info.plist to allow accesses to EarthObservatory.nasa.gov sites.
            webView.Source = rssItem.Link;
            // Hide and make visible.
            rssLayout.IsVisible = false;
            webLayout.IsVisible = true;
        }
    }
    void OnBackButtonClicked(object sender, EventArgs args)
    {
        // Hide and make visible.
        webLayout.IsVisible = false;
        rssLayout.IsVisible = true;
    }
}           

第二個StackLayout包含一個WebView,用于顯示RSS feed項引用的項和一個傳回ListView的按鈕:

第十九章:集合視圖(十六)

注意ItemSelected事件處理程式如何将ListView的SelectedItem屬性設定為null,進而有效地取消選擇該項。 (但是,所選項仍可在事件參數的SelectedItem屬性中使用。)這是将ListView用于導航目的時的常用技術。 當使用者傳回ListView時,您不希望仍然選擇該項。 當然,将ListView的SelectedItem屬性設定為null會導緻對ItemSelected事件處理程式的另一次調用,但如果處理程式在SelectedItem為null時忽略大小寫,則第二次調用應該不是問題。

更複雜的程式将導航到第二頁或使用MasterDetailPage的細節部分來顯示項目。 這些技術将在以後的章節中展示。

繼續閱讀