2017/7/11

Xamarin.Forms 教學系列文(十九.貳 - 3)ListView - Interactivity & MVVM





學習目標
  • ListView 在 MVVM 的實際使用

與 ListView 的互動方式有好幾種~

如果用戶點擊一個項目,ListView 會觸發一個 ItemTapped 事件,如果該項目以前沒有被選擇,也會是 ItemSelected 事件,還能針對 SelectedItem 屬性做 data binding。

ListView 還有一個 ScrollTo 方法,它允許程式捲動到 ListView 指定的項目。

互動的範例可於原文書 p.571 看,本章節重點放在 MVVM 的範例~
本章程式相當長... 請耐心服用...

ListView in MVVM 

底下帶來的範例是一個比較像是生活上會遇到的案例,架構較複雜一點,但聽我細細解說...

功能很單純,取得學生們的資料後顯示於 ListView 上,

先來看結果長怎樣:


此範例使用 XML 當作 Model,內存 65 位學生的資料,架構如下:
<StudentBody xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<School>School of Fine Art</School>
<Students>
    <Student>
        <FullName>Adam Harmetz</FullName>
        <FirstName>Adam</FirstName>
        <MiddleName />
        <LastName>Harmetz</LastName>
        <Sex>Male</Sex>
        <PhotoFilename>http://xamarin.github.io/.../.../AdamHarmetz.png</PhotoFilename>
        <GradePointAverage>3.01</GradePointAverage>
    </Student>
    <Student>
        <FullName>Alan Brewer</FullName>
        <FirstName>Alan</FirstName>
        <MiddleName />
        <LastName>Brewer</LastName>
        <Sex>Male</Sex>
        <PhotoFilename>http://xamarin.github.io/.../.../AlanBrewer.png</PhotoFilename>
        <GradePointAverage>1.17</GradePointAverage>
    </Student> 
    ...
    <Student>
        <FullName>Tzipi Butnaru</FullName>
        <FirstName>Tzipi</FirstName>
        <MiddleName />
        <LastName>Butnaru</LastName>
        <Sex>Female</Sex>
        <PhotoFilename>http://xamarin.github.io/.../.../TzipiButnaru.png</PhotoFilename>
        <GradePointAverage>3.76</GradePointAverage>
    </Student>
    <Student>
        <FullName>Zrinka Makovac</FullName>
        <FirstName>Zrinka</FirstName>
        <MiddleName />
        <LastName>Makovac</LastName>
        <Sex>Female</Sex>
        <PhotoFilename>http://xamarin.github.io/.../.../ZrinkaMakovac.png</PhotoFilename>
        <GradePointAverage>2.73</GradePointAverage>
    </Student>
</Students>
</StudentBody>

建立 ViewModel

本範例要先建立三支 ViewModel,其關係從底到上層為:
  • Student - 定義學生的基本屬性
  • StudentBody - 定義這一群學生
  • SchoolViewModel - 從 Model 獲取這一群學生的資料並指派給 StudentBody

ViewModel 建立好後,XAML 綁定資料的顯示就會非常方便
非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便非常方便

Student
學生的基本屬性,名字、性別、分數
並定義了四個 Command,其中三個 Command 實作來自下一支 ViewModel - StudentBody
public class Student : ViewModelBase
{
    string fullName, firstName, middleName;
    string lastName, sex, photoFilename;
    double gradePointAverage;
    string notes;

    public Student()
    {
        ResetGpaCommand = new Command(() => GradePointAverage = 2.5m);
        MoveToTopCommand = new Command(() => StudentBody.MoveStudentToTop(this));
        MoveToBottomCommand = new Command(() => StudentBody.MoveStudentToBottom(this));
        RemoveCommand = new Command(() => StudentBody.RemoveStudent(this));
    }

    public string FullName
    {
        set { SetProperty(ref fullName, value); }
        get { return fullName; }
    }
    public string FirstName
    {
        set { SetProperty(ref firstName, value); }
        get { return firstName; }
    }
    public string MiddleName
    {
        set { SetProperty(ref middleName, value); }
        get { return middleName; }
    }
    public string LastName
    {
        set { SetProperty(ref lastName, value); }
        get { return lastName; }
    }
    public string Sex
    {
        set { SetProperty(ref sex, value); }
        get { return sex; }
    }
    public string PhotoFilename
    {
        set { SetProperty(ref photoFilename, value); }
        get { return photoFilename; }
    }
    public double GradePointAverage
    {
        set { SetProperty(ref gradePointAverage, value); }
        get { return gradePointAverage; }
    }
    // For program in Chapter 25.
    public string Notes
    {
        set { SetProperty(ref notes, value); }
        get { return notes; }
    }
    // Properties for implementing commands.
    [XmlIgnore]
    public ICommand ResetGpaCommand { private set; get; }
    [XmlIgnore]
    public ICommand MoveToTopCommand { private set; get; }

    [XmlIgnore]
    public ICommand MoveToBottomCommand { private set; get; }
    [XmlIgnore]
    public ICommand RemoveCommand { private set; get; }
    [XmlIgnore]
    public StudentBody StudentBody { set; get; }
}

StudentBody
定義學生的集合,並實作 Student 所需的 Command
public class StudentBody : ViewModelBase
{
    string school;
    ObservableCollection<Student> students = new ObservableCollection();

    public string School
    {
        set { SetProperty(ref school, value); }
        get { return school; }
    }
    public ObservableCollection<Student> Students
    {
        set { SetProperty(ref students, value); }
        get { return students; }
    }

    // 移動或移除學生的 Command 實作.
    public void MoveStudentToTop(Student student)
    {
        Students.Move(Students.IndexOf(student), 0);
    }
    public void MoveStudentToBottom(Student student)
    {
        Students.Move(Students.IndexOf(student), Students.Count - 1);
    }
    public void RemoveStudent(Student student)
    {
        Students.Remove(student);
    }
}

SchoolViewModel
最下方有定義 StudentBody 屬性
獲取 65 位學生的資料指派給 StudentBody,並隨機給予學生的分數
public class SchoolViewModel : ViewModelBase
{
    StudentBody studentBody;
    Random rand = new Random();

    public SchoolViewModel() : this(null)
    {
    }

    public SchoolViewModel(IDictionary properties)
    {
        // Avoid problems with a null or empty collection.
        StudentBody = new StudentBody();
        StudentBody.Students.Add(new Student());

        string uri = "http://xamarin.github.io/xamarin-forms-book-samples" +
        "/SchoolOfFineArt/students.xml";

        HttpWebRequest request = WebRequest.CreateHttp(uri);

        //此動作會在背景執行下載任務
        request.BeginGetResponse((arg) =>
        {
            // 反序列 XML
            Stream stream = request.EndGetResponse(arg).GetResponseStream();
            StreamReader reader = new StreamReader(stream);
            XmlSerializer xml = new XmlSerializer(typeof(StudentBody));
            StudentBody = xml.Deserialize(reader) as StudentBody;

            foreach (Student student in StudentBody.Students)
            {
                // Set StudentBody property in each Student object.
                student.StudentBody = StudentBody;

                // Load possible Notes from properties dictionary
                // (這邊是 25 章要用的).
                if (properties != null && properties.ContainsKey(student.FullName))
                {
                    student.Notes = (string)properties[student.FullName];
                }
            }
        }, null);

        // 隨機給予平均分數.
        Device.StartTimer(TimeSpan.FromSeconds(0.1),
        () =>
        {
            if (studentBody != null)
            {
                int index = rand.Next(studentBody.Students.Count);
                Student student = studentBody.Students[index];
                double factor = 1 + (rand.NextDouble() - 0.5) / 5;
                student.GradePointAverage = Math.Round(
         Math.Max(0, Math.Min(5, factor * student.GradePointAverage)), 2);
            }
            return true;
        });
    }

    // Save Notes in properties dictionary (這邊是 25 章要用的).
    public void SaveNotes(IDictionary properties)
    {
        foreach (Student student in StudentBody.Students)
        {
            properties[student.FullName] = student.Notes;
        }
    }

    public StudentBody StudentBody
    {
        protected set { SetProperty(ref studentBody, value); }
        get { return studentBody; }
    }
}


接著來看 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="StudentList.StudentListPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                  iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    
    <!-- 先從 SchoolViewModel 開始 -->
    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>
    
    <!-- StackLayout 再綁定其屬性 StudentBody -->
    <StackLayout BindingContext="{Binding StudentBody}">
        <Label Text="{Binding School}"
               FontSize="Large"
               FontAttributes="Bold"
               HorizontalTextAlignment="Center" />

        <!-- ListView 再綁定其屬性 Students -->
        <ListView ItemsSource="{Binding Students}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ImageCell ImageSource="{Binding PhotoFilename}" 
                               Text="{Binding FullName}"
                               Detail="{Binding GradePointAverage, StringFormat='G.P.A. = {0:F2}'}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>

MVVM 就是如此方便... 不熟的人給我回去看 18 章

接續上面的範例,我們要來做一個點擊並顯示單筆資料的功能。
做法很簡單,在畫面下方放一個 StackLayout 準備顯示單筆的資料,將其 BindingContext 設為 listview 的 SelectedItem 就好
*OnPlatform 寫法已更改,請參考此篇文章

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="SelectedStudentDetail.SelectedStudentDetailPage"
             SizeChanged="OnPageSizeChanged">
    
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                  iOS="0, 20, 0, 0" />
    </ContentPage.Padding>

    <Grid x:Name="mainGrid">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="0" />
        </Grid.ColumnDefinitions>

        <ListView x:Name="listView"
                Grid.Row="0"
                Grid.Column="0"
                ItemsSource="{Binding StudentBody.Students}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ImageCell ImageSource="{Binding PhotoFilename}"
                     Text="{Binding FullName}"
                     Detail="{Binding GradePointAverage,
                              StringFormat='G.P.A. = {0:F2}'}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

        <!-- BindingContex 設為 SelectedItem-->
        <StackLayout x:Name="detailLayout"
                 Grid.Row="1"
                 Grid.Column="0"
                 BindingContext="{Binding Source={x:Reference listView},
                                  Path=SelectedItem}">
            <StackLayout Orientation="Horizontal"
                   HorizontalOptions="Center"
                   Spacing="0">
                <StackLayout.Resources>
                    <ResourceDictionary>
                        <Style TargetType="Label">
                            <Setter Property="FontSize" Value="Large" />
                            <Setter Property="FontAttributes" Value="Bold" />
                        </Style>
                    </ResourceDictionary>
                </StackLayout.Resources>

                <Label Text="{Binding LastName}" />
                <Label Text="{Binding FirstName, StringFormat=', {0}'}" />
                <Label Text="{Binding MiddleName, StringFormat=' {0}'}" />
            </StackLayout>

            <Image Source="{Binding PhotoFilename}"
             VerticalOptions="FillAndExpand" />
            <Label Text="{Binding Sex, StringFormat='Sex = {0}'}"
             HorizontalOptions="Center" />
            <Label Text="{Binding GradePointAverage, StringFormat='G.P.A. = {0:F2}'}"
             HorizontalOptions="Center" />
        </StackLayout>
        
    </Grid>
</ContentPage>

執行結果:




沒有留言:

張貼留言