2017/7/13

Xamarin.Forms 教學系列文(十九.貳 - 4)ListView - 長按選單 (Context Menu) & 資料刷新



學習目標
  • Context Menu - 長按或滑動出現的選單
  • IsPullToRefresh - 向下滑動時刷新資料

終於來到 ListView 最終章... 本章節將補完 ListVeiw 兩個常用的功能,一個是長按選單,另一個是下滑刷新資料


長按選單 (Context Menu)
針對 Cell 長按或滑動時,會出現更多功能的功能選單 (有點饒舌)

等等的範例會加入長按選單,而選單內的功能,是延續 上一章節 Student 類別定義的四個 Command:
  • Reset GPA
  • Move to Top
  • Move to Bottom
  • Remove

XAML 寫法上,你必須將 MenuItem 加入至 ContextActions 集合內,
MenuItem 定義了五個屬性:
  • Text (type string)
  • Icon (type FileImageSource)
  • IsDestructive (type bool)
  • Command (type ICommand)
  • CommandParameter (type object)

當然有最基本的 Clicked 事件可以呼叫。

直接來看 XAML 如何寫:
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:school="clr-namespace:SchoolOfFineArt;assembly=SchoolOfFineArt"
             x:Class="CellContextMenu.CellContextMenuPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>
    
    <StackLayout BindingContext="{Binding StudentBody}">
        <Label Text="{Binding School}"
               FontSize="Large"
               FontAttributes="Bold"
               HorizontalTextAlignment="Center" />
        
        <ListView ItemsSource="{Binding Students}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ImageCell ImageSource="{Binding PhotoFilename}"
                               Text="{Binding FullName}"
                               Detail="{Binding GradePointAverage, StringFormat='G.P.A. = {0:F2}'}">
                        
                        <!-- 四個 Command -->
                        <ImageCell.ContextActions>
                            <MenuItem Text="Reset GPA"
                                      Command="{Binding ResetGpaCommand}" />
                            <MenuItem Text="Move to top"
                                      Command="{Binding MoveToTopCommand}" />
                            <MenuItem Text="Move to bottom"
                                      Command="{Binding MoveToBottomCommand}" />
                            <MenuItem Text="Remove"
                                      IsDestructive="True"
                                      Command="{Binding RemoveCommand}" />
                        </ImageCell.ContextActions>
                    </ImageCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

執行結果:


可以注意到 Remove 項目的 IsDestructive 屬性設為 True,表示此項目在 iOS 會以紅色顯示,讓使用者更清楚這個動作會刪除此項目。


資料刷新

如果你使用 ObservableCollection 當作 ItemSource,當資料有任何改變時,都會自動刷新 ListView。

但我們還是需要 手動刷新 的功能,像是讀取電子郵件或 RSS (或滑 FB...?),都會用到手動刷新。

如果將 ListView 的 IsPullToRefresh 屬性設為 true,使用者在 ListView 往下滑動時,會出現正在讀取資料的動畫,並將 IsRefreshing 設為 true
同時間,會去觸發 RefreshCommand 所綁定的方法,直到資料讀取完畢後將 IsRefreshing 設回 false,表示刷新完成。

來看一個有趣的範例:
實作擁有刷新 RSS 資源的功能 --

先建立一個 RssFeedViewModel,此 ViewModel 負責 下載 Rss 資源,並定義刷新時要綁定的 Command,
解析完的 Rss 資料將會指派給  Items,視為 ListView 的資料來源。
Items 為 RssItemViewModel 類別集合,此類別定義了與 RSS 資源相關的屬性。

先來看 RssFeedViewModel 和 RssItemViewModel:
重點在於 LoadRssFeed() 內做的事情,Webrequest 所搭配的 BeginGetResponse 是非同步方法。
public class RssFeedViewModel : ViewModelBase
{
    string url, title;
    IList 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))
            {
                //當設定 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) =>
        {
            // 非同步下載 XML
            Stream stream = request.EndGetResponse(args).GetResponseStream();
            StreamReader reader = new StreamReader(stream);
            string xml = reader.ReadToEnd();

            // 解析 RSS
            XDocument doc = XDocument.Parse(xml);
            XElement rss = doc.Element(XName.Get("rss"));
            XElement channel = rss.Element(XName.Get("channel"));

            // 設定 Title
            Title = channel.Element(XName.Get("title")).Value;

            // 設定 Items
            List< Rssitemviewmodel> list =
            channel.Elements(XName.Get("item")).Select((XElement element) =>
            {
                // 初始化為 RssItemViewModel 類別
                return new RssItemViewModel(element);
            }).ToList();

            Items = list;

            // 讀取完畢,將 IsRefreshing 設回 false
            IsRefreshing = false;
        }, null);
    }
}

RssItemViewModel
就是一些 Rss 的屬性...
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; }
}

RefreshCommand 的 execute() 會去呼叫 LoadRssFeed(),並在 canExcute() 使用 IsRefreshing 屬性來防止資料重複讀取 (當刷新時就不允許再次刷新,直到資料讀取完畢)。

可以注意的是,RssFeedViewModel 中的 Items 屬性不一定是 ObservableCollection 類別,因為一旦讀取了 Items,其集合項目就不會再改變
直到刷新資料時,會再建立一個全新的 List 物件並指派給 Items,此時就會觸發 PropertyChanged 事件。

XAML:
*OnPlatform 寫法已更改,請參考此篇文章

<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>
        <!--初始化 RssFeedViewModel 並指派 Url-->
        <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" />
            
            <!--設定 IsPullToRefreshEnabled="True"-->
            <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,拿來顯示點選的資料-->
        <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>



以上範例都建立好後,當你將手指輕輕向下滑動時... ListView 就會進入刷新狀態並呼叫 Command 去讀取資料。

畫面最下方有放置一個隱藏的 StackLayout,其中包含兩個東西,一個要用來顯示 RSS 資料的 WebView 和一顆返回的 Button

ItemSelected 事件會隱藏第一個 StackLayout,並顯示第二個 StackLayout:
public partial class RssFeedPage : ContentPage
{
    public RssFeedPage()
    {
        InitializeComponent();
    }

    void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
    {
        if (args.SelectedItem != null)
        {
            // 取消選擇
            ((ListView)sender).SelectedItem = null;

            // 創造 rssItem 物件,為了取得其 Link 給 WebView 用
            RssItemViewModel rssItem = (RssItemViewModel)args.SelectedItem;

            // For iOS 9, a NSAppTransportSecurity key was added to 

        }

        // 直接顯示網站內容
        webView.Source = rssItem.Link;

        // 隱藏第一個,顯示第二個
        rssLayout.IsVisible = false;
        webLayout.IsVisible = true;
    }

    void OnBackButtonClicked(object sender, EventArgs args)
    {
        // 隱藏第二個,顯示第一個
        webLayout.IsVisible = false;
        rssLayout.IsVisible = true;
    }
}

注意 ItemSelected 事件內,程式將 SelectedItem 屬性 設為 null 這件事 --
這是一個簡單且有效的 取消已選擇項目 方法。

當使用者返回到 ListView 時,您不希望該項目還是被選中的狀態,所以將 ItemSelected = null 取消選擇。



ListView 就告一段落啦,下一小節 TableView 我們再相會...


沒有留言:

張貼留言