天天看點

第十九章:集合視圖(十二)ListView和MVVM

ListView和MVVM

ListView是Model-View-ViewModel架構的View部分的主要參與者之一。 每當ViewModel包含一個集合時,ListView通常會顯示這些項目。

ViewModels的集合

讓我們探讨在MVVM中使用ListView的一些資料,這些資料更接近現實生活中的例子。 這是一個關于虛構美術學院的65名虛構學生的資訊集合,包括他們過于球形的頭像。 這些圖像和包含學生姓名和位圖參考的XML檔案位于

http://xamarin.github.io/xamarin-forms-book-samples/ SchoolOfFineArt

的網站上。 該網站托管在與本書源代碼相同的GitHub存儲庫中,該網站的内容可以在該存儲庫的gh-pages分支中找到。

該站點上的Students.xml檔案包含有關學校和學生的資訊。 這是照片的縮寫URL的開頭和結尾。

<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>           

建立此檔案時,會随機生成成績點平均值。

在本書源代碼的Libraries目錄中,您将找到一個名為SchoolOfFineArt的庫項目,該項目通路此XML檔案并使用XML反序列化将其轉換為名為Student,StudentBody和SchoolViewModel的類。 盡管Student和StudentBody類的名稱中沒有單詞ViewModel,但它們無論如何都符合ViewModel的條件。

Student類派生自ViewModelBase(其副本包含在SchoolOfFineArt庫中),并定義與XML檔案中的每個Student元素關聯的七個屬性。 在将來的章節中使用第八個屬性。 該類還定義了ICommand類型的四個附加屬性和名為StudentBody的最終屬性。 最後五個屬性不是從XML反序列化設定的,因為XmlIgnore屬性訓示:

namespace SchoolOfFineArt
{
    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; }
    }
}            

ICommand類型的四個屬性在Student構造函數中設定,并與short方法相關聯,其中三個在StudentBody類中調用方法。 這些将在後面更詳細地讨論。

StudentBody課程定義School和Students屬性。 構造函數将Students屬性初始化為ObservableCollection 對象。 此外,StudentBody定義了三種從Student類調用的方法,這些方法可以從清單中删除學生或将學生移動到清單的頂部或底部:

namespace SchoolOfFineArt
{
    public class StudentBody : ViewModelBase
    {
        string school;
        ObservableCollection<Student> students = new ObservableCollection<Student>();
        public string School
        {
            set { SetProperty(ref school, value); }
            get { return school; }
        }
        public ObservableCollection<Student> Students
        {
            set { SetProperty(ref students, value); }
            get { return students; }
        }
        // Methods to implement commands to move and remove students.
        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類負責加載XML檔案并對其進行反序列化。 它包含一個名為StudentBody的屬性,它對應于XAML檔案的根标記。 此屬性設定為從XmlSerializer類的Deserialize方法擷取的StudentBody對象。

namespace SchoolOfFineArt
{
    public class SchoolViewModel : ViewModelBase
    {
        StudentBody studentBody;
        Random rand = new Random();
        public SchoolViewModel() : this(null)
        {
        }
        public SchoolViewModel(IDictionary<string, object> 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) =>
            {
                // Deserialize XML file.
                Stream stream = request.EndGetResponse(arg).GetResponseStream();
                StreamReader reader = new StreamReader(stream);
                XmlSerializer xml = new XmlSerializer(typeof(StudentBody));
                StudentBody = xml.Deserialize(reader) as StudentBody;
                // Enumerate through all the students
                foreach (Student student in StudentBody.Students)
                {
                    // Set StudentBody property in each Student object.
                   student.StudentBody = StudentBody;
                    // Load possible Notes from properties dictionary
                   // (for program in Chapter 25).
                   if (properties != null && properties.ContainsKey(student.FullName))
                    {
                        student.Notes = (string)properties[student.FullName];
                    }
                }
            }, null); 
            // Adjust GradePointAverage randomly.
            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 for program in Chapter 25.
        public void SaveNotes(IDictionary<string, object> properties)
        {
            foreach (Student student in StudentBody.Students)
            {
                properties[student.FullName] = student.Notes;
            }
        }
        public StudentBody StudentBody
        {
            protected set { SetProperty(ref studentBody, value); }
            get { return studentBody; }
        }
    }
}            

請注意,資料是異步擷取的。在此類的構造函數完成之後的某個時間,不會設定各種類的屬性。但是,INotifyPropertyChanged接口的實作應該允許使用者界面對程式啟動後某個時間擷取的資料作出反應。

BeginGetResponse的回調運作在用于在背景下載下傳資料的相同輔助執行線程中。此回調設定了一些導緻PropertyChanged事件觸發的屬性,進而導緻更新資料綁定和更改使用者界面對象。這是否意味着從第二個執行線程通路使用者界面對象?不應該使用Device.BeginInvokeOnMainThread來避免這種情況嗎?

實際上,沒有必要。通過資料綁定連結到使用者界面對象屬性的ViewModel屬性的更改不需要編組到使用者界面線程。

SchoolViewModel類還負責随機修改學生的GradePointAverage屬性,實際上模拟動态資料。因為Student實作了INotifyPropertyChanged(通過從ViewModelBase派生),我們應該能夠看到這些值在ListView顯示時動态變化。

SchoolOfFineArt庫還有一個靜态Library.Init方法,如果程式僅從XAML引用該庫,則應該調用該方法,以確定程式集正确綁定到應用程式。

您可能想要使用StudentViewModel類來了解嵌套屬性以及它們在資料綁定中的表達方式。您可以建立一個新的Xamarin.Forms項目(例如,名為Tryout),在解決方案中包含SchoolOfFineArt項目,并将Tryout的引用添加到SchoolOfFineArt庫中。然後建立一個看起來像這樣的ContentPage:

<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="Tryout.TryoutListPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>
    <Label />
</ContentPage>           

頁面的BindingContext設定為SchoolViewModel執行個體,您可以在Label的Text屬性上試驗綁定。 例如,這是一個空綁定:

<Label Text="{Binding StringFormat='{0}'}" />           

這将顯示繼承的BindingContext的完全限定類名:

SchoolOfFineArt.SchoolViewModel

SchoolViewModel類有一個名為StudentBody的屬性,是以将綁定的路徑設定為:

<Label Text="{Binding Path=StudentBody, StringFormat='{0}'}" />           

現在,您将看到StudentBody類的完全限定名稱:

SchoolOfFineArt.StudentBody

StudentBody課程有兩個屬性,名為School和Students。 試試學校的屬性:

<Label Text="{Binding Path=StudentBody.School,     
                      StringFormat='{0}'}" />           

最後,顯示一些實際資料而不僅僅是類名。 它是從XML檔案集到School屬性的字元串:

美術學院

Binding表達式中不需要StringFormat,因為該屬性的類型為string。 現在嘗試學生屬性:

<Label Text="{Binding Path=StudentBody.Students, 
                      StringFormat='{0}'}" />            

這将顯示帶有Student對象集合的ObservableCollection的完全限定類名:

System.Collections.ObjectModel.ObservableCollection'1[SchoolOfFineArt.Student]

應該可以索引此集合,如下所示:

<Label Text="{Binding Path=StudentBody.Students[0], 
                      StringFormat='{0}'}" />           

這是Student類型的對象:

SchoolOfFineArt.Student

如果在綁定時加載了整個學生集合,您應該能夠在學生集合中指定任何索引,但索引0始終是安全的。

然後,您可以通路該學生的屬性,例如:

<Label Text="{Binding Path=StudentBody.Students[0].FullName, 
                      StringFormat='{0}'}" />           

你會看到學生的名字:

Adam Harmetz

或者,嘗試GradePointAverage屬性:

<Label Text="{Binding Path=StudentBody.Students[0].GradePointAverage, 
                      StringFormat='{0}'}" />           

最初,您将看到存儲在XML檔案中的随機生成的值:

3.01

但是等一會兒,你應該看到它改變了。

你想看一張Adam Harmetz的照片嗎? 隻需将Label更改為Image,并将目标屬性更改為Source,将源路徑更改為PhotoFilename:

<Image Source="{Binding Path=StudentBody.Students[0].PhotoFilename}" />           

他是2019年的班級:

第十九章:集合視圖(十二)ListView和MVVM

通過對資料綁定路徑的了解,應該可以建構一個頁面,其中既包含顯示學校名稱的Label,也包含顯示所有學生的全名,平均成績和照片的ListView。 ListView中的每個項目都必須顯示兩段文本和一個圖像。 這是ImageCell的理想選擇,ImageCell源自TextCell并為兩個文本項添加圖像。 這是StudentList程式:

<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>
    <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}'}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackLayout>
</ContentPage>           

與實驗性XAML檔案一樣,ContentPage的BindingContext是SchoolViewModel對象。 StackLayout繼承了BindingContext,但是将自己的BindingContext設定為StudentBody屬性,這是StackLayout子節點繼承的BindingContext。 Label的Text屬性綁定到StudentBody類的School屬性,ListView的ItemsSource屬性綁定到Students集合。

這意味着ListView中每個項的BindingContext是一個Student對象,ImageCell屬性可以綁定到Student類的屬性。 結果是可滾動和可選的,盡管選擇以特定于平台的方式顯示:

第十九章:集合視圖(十二)ListView和MVVM

不幸的是,ImageCell的Windows Runtime版本與其他兩個平台上的版本略有不同。如果您不喜歡這些行的預設大小,您可能想要設定RowHeight屬性,但它在平台上的工作方式不同,唯一一緻的解決方案是切換到自定義ViewCell派生,也許就像CustomNamedColorList中的一個,但有一個Image而不是BoxView。

頁面頂部的Label與ListView共享StackLayout,以便在滾動ListView時Label保持不變。但是,您可能希望此類标題與ListView的内容一起滾動,并且您可能還想添加頁腳。 ListView具有對象類型的頁眉和頁腳屬性,您可以将其設定為字元串或任何類型的對象(在這種情況下,标題将顯示該對象的ToString方法的結果)或綁定。

這是一種方法:頁面的BindingContext像以前一樣設定為SchoolViewModel,但ListView的BindingContext設定為StudentBody屬性。這意味着ItemsSource屬性可以在綁定中引用Students集合,并且Header可以綁定到School屬性:

<ContentPage __ >
    __
    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>
    <ListView BindingContext="{Binding StudentBody}"
              ItemsSource="{Binding Students}"
              Header="{Binding School}">
        __
    </ListView>
</ContentPage>           

這會在帶有ListView内容的标題中顯示文本“美術學院”。

如果您想格式化該标題,也可以這樣做。 将ListView的HeaderTemplate屬性設定為DataTemplate,并在DataTemplate标記内定義可視樹。 該可視化樹的BindingContext是設定為Header屬性的對象(在此示例中,為具有學校名稱的字元串)。

在下面顯示的ListViewHeader程式中,Header屬性綁定到School屬性。 在HeaderTemplate中是一個僅由Label組成的可視樹。 此Label具有空綁定,是以該Label的Text屬性綁定到Header屬性的文本集:

<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="ListViewHeader.ListViewHeaderPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>
    <ContentPage.BindingContext>
        <school:SchoolViewModel />
    </ContentPage.BindingContext>
    <ListView BindingContext="{Binding StudentBody}"
              ItemsSource="{Binding Students}"
              Header="{Binding School}">
 
        <ListView.HeaderTemplate>
            <DataTemplate>
                <Label Text="{Binding}"
                       FontSize="Large"
                       FontAttributes="Bold, Italic"
                       HorizontalTextAlignment="Center" />
            </DataTemplate>
        </ListView.HeaderTemplate>
 
        <ListView.ItemTemplate>
            <DataTemplate>
                <ImageCell ImageSource="{Binding PhotoFilename}"
                           Text="{Binding FullName}"
                           Detail="{Binding GradePointAverage,
                           StringFormat='G.P.A. = {0:F2}'}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>           

标題僅顯示在Android平台上:

第十九章:集合視圖(十二)ListView和MVVM

繼續閱讀