學習目標
- 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 事件可以呼叫。
*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; IListitems; 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="< 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 我們再相會...
沒有留言:
張貼留言
注意:只有此網誌的成員可以留言。