4. 数据绑定

对于基于XAML的应用程序来说,数据绑定是一个及其重要的概念。数据绑定把数据从.NET对象传递给UI,或从UI传递给.NET对象。简单的对象可以绑定到UI元素、对象列表和XAML元素上。在数据绑定中,目标可以是XAML元素的任意依赖属性,CLR对象的每个属性都可以是绑定源。因为XAML元素也提供了.NET属性,所以每个XAML元素也可以用作绑定源。下图显示了绑定源和绑定目标之间的连接。绑定定义了该连接。

注意:在Xamarin.Froms应用程序中,目标对象必须派生自 BindableObject。 必须使用名为 BindableProperty 的特殊属性来实现目标属性。 所有 Xamarin.Forms 控件最终都派生自 BindableObject(通过 View)。 并且其属性大多数都是 BindableProperties。 此体系结构使所有 Xamarin.Forms 控件都成为方便的绑定目标。

Binding对象支持源与目标之间的几种绑定模式。绑定可以是单向的,即从源信息指向目标,但如果用户在用户界面上修改了该信息,则源不会更新。要更新源,需要双向绑定。

下表列出了绑定模式及其要求。

注意:

UWP支持两种绑定类型:使用Binding标记扩展的传统绑定,以及使用x:Bind标记扩展的新编译绑定。请注意,绑定模式的默认值在这些绑定类型之间存在差异,因此最好总是指定绑定模式。本节主要关注新的编译绑定。

 除了绑定模式之外,数据绑定还涉及许多方面。本节详细介绍与简单的.NET对象和列表的绑定。通过更改通知,可以使用绑定对象中的更改更新UI。本节也将论述如何动态地选择数据模版。

下面从DataBindingSamples示例应用程序开始。该应用程序显示图书列表,并允许用户选择一本书,来查看图书细节。

1. 用INotifyPropertyChanged更改通知

首先创建模型。为了在属性值变化时更改信息传递给用户界面,必须实现INotifyPropertyChanged接口。为了重用此实现代码,创建实现此接口的BindableBase类。该接口定义了PropertyChanged事件处理程序,该事件在OnPropertyChanged方法中触发。方法Set用于更改属性值,并触发PropertyChanged事件。如果要设置的值与当前值没有不同,则不触发事件,且方法仅返回false。只有使用不同的值时,属性才设置为新值,并触发PropertyChanged事件。这个方法在C#中通过CallerMemberName属性来使用调用者信息。propertyName参数通过这个属性定义为可选参数,C#编译器就会通过这个参数传递属性名,所以不需要在代码中添加硬编码字符串:

    public class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChangd(string propertyName) =>
            PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(propertyName));
        public virtual bool Set<T>(ref T item,T value,[CallerMemberName]string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(item,value))
            {
                return false;
            }
            item = value;
            OnPropertyChangd(propertyName);
            return true;
        }
    }

Book类派生自基类BindableBase,并实现了属性BookId、Title、Publisher和Authors。BookId属性只是只读的;Titlte和Publisher使用来自基类的变更通知实现;Author是一个只读属性,返回作者列表(对于此示例项目,该Book类可以不用启用属性通知更改):

    class Book:BindableBase
    {
        public Book(int id,string title,string publisher,params string[] authors)
        {
            BookId = id;
            Title = title;
            Publisher = publisher;
            Authors = authors;
        }
        public int BookId { get;}
        private string _title;
        public string Title
        {
            get => _title;
            set => Set(ref _title,value);
        }
        private string _publisher;
        public string Publisher
        {
            get => _publisher;
            set => Set(ref _publisher,value);
        }
        public IEnumerable<string> Authors { get; }
        public override string ToString() => Title;
    }

2. 创建图书列表

GetSampleBooks方法返回应使用Book类的构造函数显示的图书列表:

    class SampleBookService
    {
        public IEnumerable<Book> GetSampleBooks() =>
            new List<Book>
            {
                new Book(1,"Professional C# 7 and .NET Core 2","Wrox Press","Christian Nagel"),
                new Book(2,"Professional C# 6 and .NET Core 1.0","Wrox Press","Christian Nagel"),
                new Book(3,"Professional C# 6.0 and .NET 4.5.1","Wrox Press","Christian Nagel",
                    "Jay Glynn","Morgan Skinner"),
                new Book(4,"Enterprise Services with the .NET Framework","AWL","Christian Nagel")
            };
    }

现在,BooksService类提供了RefreshBooks、GetBook、AddBook方法以及属性Books。属性Books返回一个ObservableCollection<Book>对象。ObsevableCollection是一个泛型类,通过实现接口INotifyPropertyChanged来提供更改通知:

    class BooksService
    {
        private ObservableCollection<Book> _books = new ObservableCollection<Book>();
        public IEnumerable<Book> Books => _books;
        public void RefresyhBooks()
        {
            _books.Clear();
            var sampleBooksService = new SampleBooksService();
            var books = sampleBooksService.GetSampleBooks();
            foreach (var b in books)
            {
                _books.Add(b);
            }
        }
        public Book GetBook(int bookId) =>
            _books.SingleOrDefault(b=>b.BookId == bookId);
        public void AddBook(Book book) => _books.Add(book);       
    }

3. 列表绑定

现在可以显示图片列表了。可以使用任何ItemsSource派生控件指定ItemsSource属性,绑定到列表上。上面的代码片段使用ListView控件将ItemsSource绑定到Books属性上。使用标记扩展x:Bind时,指定的第一个名称是绑定的源,Mode参数确定了绑定模式。对于OneWay,当消息源发生变化时,UWP利用变更通知来更新用户界面:

    <Grid>
        <ListView ItemsSource="{x:Bind Books,Mode=OneWay}"/>
    </Grid>

在代码隐藏文件中,指定Books属性以引用BooksService的Books属性:

        private BooksService _booksService = new BooksService();
        public MainPage()
        {
            this.InitializeComponent();
        }
        public IEnumerable<Book> Books => _booksService.Books;

4. 把事件绑定到方法

如果没有在BooksService中调用RefreshBooks方法,列表将为空。使用XAML文件,会创建一个CommandBar,其中列出两个AppBarButton控件。通过AppBarButton控件,Click事件再次绑定到OnRefreshBooks和OnAddBook方法上:

        <CommandBar Grid.ColumnSpan="2">
            <AppBarButton Icon="Refresh" Label="Refresh" Click="{x:Bind OnRefreshBooks}"/>
            <AppBarButton Icon="Add" Label="Add Book" Click="{x:Bind OnAddBook}"/>
        </CommandBar>

如果方法实现中没有使用到参数或具有事件的委托类型指定的参数,则可以将事件绑定到无参的方法。在以下代码片段中,OnRefreshBooks和OnAddBook方法声明为void,没有参数:

        public void OnRefreshBooks()
        {
            _booksService.RefresyhBooks();
        }
        public void OnAddBook()
        {
            _booksService.AddBook(new Book(
            GetNextBookId(),$"Professional C# {GetNextBookId() + 3}",
            "Wrox Press"
            ));
        }
        private int GetNextBookId() => Books.Select(b => b.BookId).Max() + 1;

注意:

绑定到方法上只能使用x:Bind标记扩展,不能使用传统的Binding标记扩展。

正在运行的应用程序带有两个AppBar按钮,如下图所示。单击Refresh按钮加载图书,并显示图书标题,因为Book类的ToString方法返回标题。单击Add按钮会创建一个新的Book对象,该对象会出现在列表中,因为列表的类型是ObservableCollection。ObservableCollection通过接口INotifyCollectionChanged实现了更改通知。

5. 使用数据模版和数据模版选择器

为了创建不同的项外观,可以创建一个DataTemplate。可以使用x:key特性指定的键引用DataTemplate。使用x:DataType特性,可以在数据模版中使用已编译绑定。已编译绑定需要在编译时绑定到的类型。要绑定到Title属性,类型由Book类定义:

    <Page.Resources>
        <DataTemplate x:Key="WroxTemplate" x:DataType="local:Book">
            <Border Background="Red" Margin="4" Padding="4" BorderThickness="2" BorderBrush="DarkRed">
                <TextBlock Text="{x:Bind Title,Mode=OneWay}" Foreground="White" Width="300"/>
            </Border>
        </DataTemplate>
    </Page.Resources>

在ItemsControl中使用的数据模版可以使用ItemsControl的ItemsTemplate属性来引用。现在使用DataTemplateSelector,根据出版社的名称动态地选择DataTemplate,而不是指定DataTemplate。

BookDataTemplateSelector派生自基类DataTemplateSelector。BookDataTemplateSelector需要重写方法SelectTemplateCore并返回所选的DataTemplate。在实现BookDataTemplateSelector时,指定了两个属性WroxTemplate和DefaultTemplate。在SelectTemplateCore方法中,会接收Book对象。可以使用模式匹配与switch语句,这样,如果出版社是WroxPress,则返回WroxTemplate。在其他情况下,会返回DefaultTemplate。可以使用更多的出版社扩展switch语句:

    public class BookDataTemplateSelector:DataTemplateSelector
    {
        public DataTemplate WroxTemplate { get; set; }
        public DataTemplate DefaultTemplate { get; set; }
        protected override DataTemplate SelectTemplateCore(object item)
        {
            DataTemplate selectedTemplate = null;
            switch (item)
            {
                case Book b when b.Publisher == "Wrox Press":
                    selectedTemplate = WroxTemplate;
                    break;
                default:
                    selectedTemplate = DefaultTemplate;
                    break;
            }
            return selectedTemplate;
        }
    }

接下来,需要实例化和初始化BookDataTemplateSelector。可以在XAML代码中完成这个工作。在这里,指定属性WroxTemplate和DefaultTemplate来引用先前创建的DataTemplate模版:

    <Page.Resources>
        <DataTemplate x:Key="WroxTemplate" x:DataType="local:Book">
            <Border Background="Red" Margin="4" Padding="4" BorderThickness="2" BorderBrush="DarkRed">
                <TextBlock Text="{x:Bind Title,Mode=OneWay}" Foreground="White" Width="300"/>
            </Border>
        </DataTemplate>
        <DataTemplate x:Key="DefaultTemplate" x:DataType="local:Book">
            <Border Background="LightBlue" Margin="4" Padding="4" BorderThickness="2" BorderBrush="DarkBlue">
                <TextBlock Text="{x:Bind Title,Mode=OneWay}" Foreground="White" Width="300"/>
            </Border>
        </DataTemplate>
        <local:BookDataTemplateSelector x:Key="BookDataTemplateSelector"
                                        WroxTemplate="{StaticResource WroxTemplate}"
                                        DefaultTemplate="{StaticResource DefaultTemplate}">
        </local:BookDataTemplateSelector>
    </Page.Resources>

为了将BookDataTemplateSelector与ListView中的项一起使用,ItemTemplateSelector属性使用键和StaticResource标记扩展来引用模版:

        <ListView Grid.Row="1" ItemsSource="{Binding Books,Mode=OneWay}"
                  ItemTemplateSelector="{StaticResource BookDataTemplateSelector}"/>

在此阶段运行应用程序时,下图显示了基于Publisher的不同视图的新输出。

6. 绑定简单对象

不只是绑定列表,单本书应该显示在应用程序的右侧。已编译绑定用于绑定Book对象的BookId、Title和Publisher属性:

<UserControl
    x:Class="DataBindingSamples.ViewModels.BookUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:DataBindingSamples.ViewModels"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <Grid>
        <StackPanel>
            <TextBox Header="BookId" IsReadOnly="True"  Text="{x:Bind Book.BookId,Mode=OneWay}"/>
            <TextBox Header="Title" Text="{x:Bind Book.Title,Mode=OneWay}"/>
            <TextBox Header="Publisher" Text="{x:Bind Book.Publisher,Mode=OneWay}"/>
        </StackPanel>
    </Grid>
</UserControl>

在代码隐藏文件中,Book属性定义为一个依赖属性。当值更改时,需要更改通知来进行更新,这就是为什么要使用依赖属性的原因。也可以实现INotifyPropertyChanged,但是由于依赖属性已经可以从基类DependencyObject中获得,所以可以轻松地使用依赖属性:

        public Book Book
        {
            get => GetValue(BookProperty) as Book;
            set => SetValue(BookProperty,value);
        }
        public static readonly DependencyProperty BookProperty =
            DependencyProperty.Register("Book",typeof(Book),typeof(BookUserControl),new PropertyMetadata(null));

现在,用户控件需要显示在MainPage中,当前选中的图书应该分配给UserControl的Book属性。为此可以使用XAML代码。BookUserControl在MainPage的Grid中添加,在ListView中,SelectedItem属性绑定到Book属性。这次,TwoWay绑定模式需要在ListView中更新UserControl:

        <ListView Grid.Row="1" ItemsSource="{Binding Books,Mode=OneWay}"
                  ItemTemplateSelector="{StaticResource BookDataTemplateSelector}"
                  SelectedItem="{x:Bind CurrentBook.Book,Mode=TwoWay}"/>
        <local1:BookUserControl x:Name="CurrentBook" Grid.Row="1" Grid.Column="1" Margin="4"/>

注意:

也可以以另一种方式创建绑定——将BookUserControl绑定到ListView。这样,OneWay绑定就足够了——只需要将更新后的值从ListView获取到BookUserControl。但是在这里XAML编译器会报错,因为它不能将一个对象(来自ListView)分配给BookUserControl的强类型Book属性。可以通过创建一个值转换器(稍后讨论)来解决这个问题。SelectedItem属性不存在这个问题,因为微软改变了实现。最新的Windows 10版本不再报错,在早期版本中,还需要在该场景中使用对象到对象的转换器。

在运行应用程序时,可以看到图书,在列表中选择一本书时,详细信息显示在用户控件中,如下图所示。

 7. 值的转换

Authors还没有显示在用户控件中。原因是Authors属性是一个列表。可以在UserControl中定义一个ItemsControl来显示Authors属性。但是,仅为了显示一个以逗号分隔的作者列表,使用TextBox即可。只需要一个转换器就可以将IEnumerable<string>(Authors属性的类型)转换为字符串。

值转换器是IValueConverter接口的实现。这个接口定义了Convert和ConvertBack方法。对于双向绑定,需要实现这两个方法。使用单向绑定,Convert方法就足够了。类CollectionToStringConverter使用string.Join方法创建单个字符串,实现了Convert方法。值转换器还接收一个对象parameter,可以在使用值转换器时指定该参数。这里,将该参数用作字符串分隔符:

    public class CollectionToStringConverter:IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            IEnumerable<string> names = value as IEnumerable<string>;
            return string.Join(parameter?.ToString()??", ",names);
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }

使用UserControl,CollectionToStringConverter在资源部分实例化:

    <UserControl.Resources>
        <conveters:CollectionToStringConverter x:Key="CollectionToStringConverter"/>
    </UserControl.Resources>

现在可以使用Converter属性在 x:Bind 标记扩展中引用转换器。ConverterParameter属性指定在之前的string.Join方法中使用的字符串分隔符:

            <TextBox Header="Authors" IsReadOnly="True" Text="{x:Bind Book.Authors,Mode=OneWay,
                Converter={StaticResource CollectionToStringConverter},ConverterParameter='; '}"/>

运行该应用程序时,Authors将如下图所示。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值