第十九章:集合视图(七)

模板和单元格
ListView的目的是显示数据。在现实世界中,数据无处不在,我们不得不编写计算机程序来处理这些数据。然而,在诸如本书的编程教程中,数据更难获得。因此,让我们发明一些数据来更深入地探索ListView,如果数据证明是有用的,那就更好了!
如您所知,Xamarin.Forms Color结构支持的颜色基于HTML 4.01标准中定义的16种颜色。另一种流行的颜色集合在层叠样式表(CSS)3.0标准中定义。该集合包含147种命名颜色(其中七种是变体拼写的重复颜色),这些颜色最初是从X11窗口系统中的颜色名称派生的,但转换为驼峰情况。
Xamarin.FormsBook.Toolkit库中包含的NamedColor类使您的Xamarin.Forms程序可以访问这147种颜色。 NamedColor的大部分是147个类型为Color的公共静态只读字段的定义。在课程结尾处的缩略列表中只显示了少数几个:

public class NamedColor
{
    // Instance members.
    private NamedColor()
    {
    }
    public string Name { private set; get; }
    public string FriendlyName { private set; get; }
    public Color Color { private set; get; }
    public string RgbDisplay { private set; get; }
    // Static members.
    static NamedColor()
    {
        List<NamedColor> all = new List<NamedColor>();
        StringBuilder stringBuilder = new StringBuilder();
        // Loop through the public static fields of type Color.
        foreach (FieldInfo fieldInfo in typeof(NamedColor).GetRuntimeFields ())
        {
            if (fieldInfo.IsPublic &&
                fieldInfo.IsStatic &&
                fieldInfo.FieldType == typeof (Color))
            {
                // Convert the name to a friendly name.
                string name = fieldInfo.Name;
                stringBuilder.Clear();
                int index = 0;
                foreach (char ch in name)
                {
                    if (index != 0 && Char.IsUpper(ch))
                    {
                        stringBuilder.Append(' ');
                    }
                    stringBuilder.Append(ch);
                    index++;
                }
                // Instantiate a NamedColor object.
                Color color = (Color)fieldInfo.GetValue(null);
                NamedColor namedColor = new NamedColor
                {
                    Name = name,
                    FriendlyName = stringBuilder.ToString(),
                    Color = color,
                    RgbDisplay = String.Format("{0:X2}-{1:X2}-{2:X2}",
                                               (int)(255 * color.R),
                                               (int)(255 * color.G),
                                               (int)(255 * color.B))
                };
                // Add it to the collection.
                all.Add(namedColor);

            }
        }
        all.TrimExcess();
        All = all;
    }
    public static IList<NamedColor> All { private set; get; }
    // Color names and definitions from http://www.w3.org/TR/css3-color/
    // (but with color names converted to camel case).
    public static readonly Color AliceBlue = Color.FromRgb(240, 248, 255);
    public static readonly Color AntiqueWhite = Color.FromRgb(250, 235, 215);
    public static readonly Color Aqua = Color.FromRgb(0, 255, 255);
    __
    public static readonly Color WhiteSmoke = Color.FromRgb(245, 245, 245);
    public static readonly Color Yellow = Color.FromRgb(255, 255, 0);
    public static readonly Color YellowGreen = Color.FromRgb(154, 205, 50);
}

如果您的应用程序引用了Xamarin.FormsBook.Toolkit和Xamarin.FormsBook.Toolkit命名空间的using指令,则可以像使用Color结构中的静态字段一样使用这些字段。 例如:

BoxView boxView = new BoxView
{
    Color = NamedColor.Chocolate
};

您也可以在XAML中使用它们而不会有太多困难。 如果您有Xamarin.FormsBook.Toolkit程序集的XML名称空间声明,则可以在x:Static标记扩展中引用NamedColor:

<BoxView Color="{x:Static toolkit:NamedColor.CornflowerBlue}" />

但并非所有:在其静态构造函数中,NamedColor使用反射创建147个NamedColor类实例,它存储在一个可从静态All属性公开获得的列表中。 NamedColor类的每个实例都有一个Name属性,一个Color类型的Color属性,一个FriendlyName属性,除了插入一些空格外,它与Name相同,还有一个格式化十六进制颜色值的RgbDisplay属性。
NamedColor类不是从BindableObject派生的,也不实现INotifyPropertyChanged。无论如何,您可以将此类用作绑定源。这是因为在实例化每个NamedColor对象后,这些属性保持不变。只有稍后更改了这些属性,该类才需要实现INotifyPropertyChanged作为成功的绑定源。
NamedColor.All属性被定义为IList 类型,因此我们可以将它设置为ListView的ItemsSource属性。 NaiveNamedColorList程序证明了这一点:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit=
                 "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
             x:Class="NaiveNamedColorList.NaiveNamedColorListPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
             iOS="10, 20, 10, 0"
             Android="10, 0"
             WinPhone="10, 0" />
    </ContentPage.Padding>
    <ListView ItemsSource="{x:Static toolkit:NamedColor.All}" />
 
</ContentPage>

因为该程序仅从XAML文件访问NamedColor类,所以程序从其App构造函数调用Toolkit.Init。
您会发现可以滚动此列表并选择项目,但项目本身可能有点令人失望,因为您将看到147个完全限定的类名列表:
第十九章:集合视图(七)
这可能看起来令人失望,但是在您未来的涉及ListView的现实编程工作中,当您看到类似这样的显示时,您可能会欢呼,因为这意味着您已成功将ItemsSource设置为有效集合。 对象就在那里。 你只需要更好地展示它们。
此特定ListView显示NamedColor的完全限定类名,因为NamedColor没有定义自己的ToString方法,并且ToString的默认实现显示类名。 一个简单的解决方案是向NamedColor添加ToString方法:

public override string ToString()
{
    return FriendlyName;
}

现在,ListView显示所有颜色的友好名称。很简单。
但是,在实际编程中,您可能无法向数据类添加代码,因为您可能无法访问源代码。因此,让我们寻求独立于数据实际实现的解决方案。
ListView派生自ItemsView,除了定义ItemsSource属性外,ItemsView还定义了一个名为ItemTemplate的属性,类型为DataTemplate。 DataTemplate对象为您(程序员)提供了以任何您想要的方式显示ListView项目的能力。
与ListView结合使用时,DataTemplate引用Cell类来呈现项目。 Cell类派生自Element,从中获取对父/子关系的支持。但与View不同,Cell不是从VisualElement派生的。 Cell更像是对视觉元素树的描述,而不是视觉元素本身。
这是类层次结构,显示了从Cell派生的五个类:

Object
    BindableObject
        Element
            Cell
                TextCell — two Label views
                    ImageCell — derives from TextCell and adds an Image view
                EntryCell — an Entry view with a Label
                SwitchCell — a Switch with a Label
                ViewCell — any View (likely with children)

Cell类型的描述仅是概念性的:出于性能原因,Cell的实际组成在每个平台中定义。
当您开始探索这些Cell类并考虑将它们与ListView结合使用时,您可能会质疑其中几个的相关性。 但它们并非仅适用于ListView。 正如您将在本章后面看到的那样,Cell类在TableView中也扮演着重要角色,它们以不同的方式使用。
对ListView最适用的Cell派生可能是TextCell,ImageCell和强大的ViewCell,它允许您为项目定义自己的视觉效果。
让我们首先看一下TextCell,它定义了可绑定属性支持的六个属性:
-Text string类型

  • TextColor Color类型
  • Detail string类型
  • DetailColor Color类型
  • Command ICommand类型
  • CommandParameter Object类型

TextCell包含两个Label视图,您可以将它们设置为两种不同的字符串和颜色。 字体特征以与平台相关的方式固定。
TextCellListCode程序不包含XAML。 相反,它演示了如何在代码中使用TextCell来显示所有NamedColor对象的属性:

public class TextCellListCodePage : ContentPage
{
    public TextCellListCodePage()
    {
        // Define the DataTemplate.
        DataTemplate dataTemplate = new DataTemplate(typeof(TextCell));
        dataTemplate.SetBinding(TextCell.TextProperty, "FriendlyName");
        dataTemplate.SetBinding(TextCell.DetailProperty, 
            new Binding(path: "RgbDisplay", stringFormat: "RGB = {0}"));
        // Build the page.
        Padding = new Thickness(10, Device.OnPlatform(20, 0, 0), 10, 0);
        Content = new ListView
        {
            ItemsSource = NamedColor.All,
            ItemTemplate = dataTemplate
        };
    }
}

在ListView中使用Cell的第一步是创建一个DataTemplate类型的对象:

DataTemplate dataTemplate = new DataTemplate(typeof(TextCell));

请注意,构造函数的参数不是TextCell的实例,而是TextCell的类型。
第二步是在DataTemplate对象上调用SetBinding方法,但请注意这些SetBinding调用实际上如何定位TextCell的可绑定属性:

dataTemplate.SetBinding(TextCell.TextProperty, "FriendlyName");
dataTemplate.SetBinding(TextCell.DetailProperty, 
    new Binding(path: "RgbDisplay", stringFormat: "RGB = {0}"));

这些SetBinding调用与您可能在TextCell对象上设置的绑定相同,但在这些调用时,没有TextCell实例可以设置绑定!
如果您愿意,还可以通过调用DataTemplate类的SetValue方法将TextCell的某些属性设置为常量值:

dataTemplate.SetValue(TextCell.TextColorProperty, Color.Blue);
dataTemplate.SetValue(TextCell.DetailColorProperty, Color.Red);

这些SetValue调用类似于您可能对可视元素进行的调用,而不是直接设置属性。
您应该非常熟悉SetBinding和SetValue方法,因为它们由BindableObject定义,并由Xamarin.Forms中的许多类继承。 但是,DataTemplate不是从BindableObject派生而是定义自己的SetBinding和SetValue方法。 这些方法的目的不是绑定或设置DataTemplate实例的属性。 因为DataTemplate不是从BindableObject派生的,所以它没有自己的可绑定属性。 相反,DataTemplate只是将这些设置保存在两个内部字典中,这些字典可通过DataTemplate定义的两个属性(名为Bindings和Values)公开访问。
使用带ListView的Cell的第三步是将DataTemplate对象设置为ListView的ItemTemplate属性:

Content = new ListView
{
    ItemsSource = NamedColor.All,
    ItemTemplate = dataTemplate
};

这是发生了什么(概念上无论如何):
当ListView需要显示特定项(在本例中为NamedColor对象)时,它会实例化传递给DataTemplate构造函数的类型,在本例中为TextCell。 然后,已在DataTemplate上设置的任何绑定或值都将传输到此TextCell。 每个TextCell的BindingContext设置为正在显示的特定项目,在这种情况下是特定的NamedColor对象,这就是ListView中的每个项目如何显示特定NamedColor对象的属性。 每个TextCell都是一个具有相同数据绑定的可视树,但具有唯一的BindingContext设置。 这是结果:
第十九章:集合视图(七)
通常,ListView不会一次创建所有可视树。 出于性能目的,它将仅在用户将新项目滚动到视图中时根据需要创建它们。 如果为ListView定义的ItemAppearing和ItemDisappearing事件安装处理程序,您可以对此有所了解。 你会发现这些事件并没有完全跟踪视觉效果 - 项目在滚动到视图之前被报告为出现,并且在它们滚出视图后被报告为消失 - 但是这些练习仍然是有益的。
您还可以了解使用Func对象的DataTemplate的替代构造函数所发生的事情:

DataTemplate dataTemplate = new DataTemplate(() =>
{
    return new TextCell();
});

仅当项目需要TextCell对象时才调用Func对象,尽管这些调用实际上是在项目滚动到视图之前进行的。
您可能希望包含实际计算正在创建的TextCell实例数的代码,并在Visual Studio或Xamarin Studio的“输出”窗口中显示结果:

int count = 0;
DataTemplate dataTemplate = new DataTemplate(() =>
    {
        System.Diagnostics.Debug.WriteLine("Text Cell Number " + (++count));
        return new TextCell();
    });

向下滚动到底部时,您将发现为ListView中的147个项目创建了最多147个TextCell对象。 TextCell对象被缓存,但不会在项目滚入和滚出视图时重复使用。但是,在较低级别 - 特别是涉及特定于平台的TextCellRenderer对象以及由这些渲染器创建的基础平台特定视觉效果 - 视觉效果将被重用。
如果您需要在无法使用数据绑定设置的单元格对象上设置某些属性,则使用Func参数的此替代DataTemplate构造函数可能很方便。也许你已经创建了一个ViewCell衍生物,它在构造函数中需要一个参数。但是,通常使用带有Type参数的构造函数或在XAML中定义数据模板。
在XAML中,绑定语法稍微扭曲了用于为ListView项生成可视树的实际机制,但同时语法在概念上更清晰,在视觉上更优雅。这是来自TextCellListXaml程序的XAML文件,它在功能上与TextCellListCode程序相同:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit=
                 "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
             x:Class="TextCellListXaml.TextCellListXamlPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="10, 20, 10, 0"
                    Android="10, 0"
                    WinPhone="10, 0" />
    </ContentPage.Padding>
    <ListView ItemsSource="{x:Static toolkit:NamedColor.All}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding FriendlyName}"
                          Detail="{Binding RgbDisplay, StringFormat='RGB = {0}'}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

在XAML中,将DataTemplate设置为ListView的ItemTemplate属性,并将TextCell定义为DataTemplate的子级。 然后只需在TextCell属性上设置数据绑定,就像TextCell是一个普通的可视元素一样。 这些绑定不需要Source设置,因为ListView已在每个项目上设置了BindingContext。
定义自己的自定义单元格时,您会更加欣赏这种语法。