2.1-数据绑定和MVVM:数据绑定Binding
一、基本概念
1、数据绑定,链接两个对象的属性,其中一个对象作为绑定源,另一个对象作为绑定目标。两个链接的属性之间,可以实现双向、单向或单次等丰富的数据响应,一方数据发生变化,另一方做出同步响应。绑定目标几乎总是页面元素,而绑定源可能是同一页面元素,也可能是后台代码文件中某一对象。
2、数据绑定的另一个显著特征是,绑定源对绑定目标是无感的,它不需要知道哪些目标绑定了它。绑定源主要有两种类型,一是可绑定对象(BindableObject)的可绑定属性(BindableProperty),绝不多数控件都是可绑定对象,绝不多数控件属性都是可绑定属性;二是MVVM模式中的ViewModel,可以作为绑定源,通过Propertychanged事件机制实现绑定。
3、绑定设置主要在绑定目标上进行,主要完成以下两个工作:①通过设置BindingContext,或者Binding扩展标记的Source属性,设置绑定源;②通过Binding扩展标记设置绑定的Path(绑定源的链接属性)。BindingContext(绑定上下文)比较特殊,可以将绑定上下文设置在控件树的上级元素上,绑定目标会延着控件树向上找,以最先找到的上下文为准,这也叫绑定上下文继承。
二、数据绑定的五种实现方式(以同一页面元素之间的绑定为例)
以下案例实现Lable/文本控件的Text值,与Slider/滑块控件的Value值绑定。Lable是绑定目标,Slider是绑定源。提供了5种实现方式。
<!--实现方式①:BindingContext和Binding都在绑定目标上设置-->
<VerticalStackLayout>
<!--StringFormat实现格式化字符串输出。{0:F0},其中0表示变量占位符,F0表示小数位为0位的浮点值-->
<Label BindingContext="{x:Reference slider}" Text="{Binding Path=Value, StringFormat='滑块值为:{0:F0}'}" />
<Slider x:Name="slider" Maximum="360" />
</VerticalStackLayout>
<!--实现方式②:BindingContext上控制树的上级元素中设置-->
<VerticalStackLayout BindingContext="{x:Reference slider}">
<!--Path是内容属性,可以省略,即{Binding Value}-->
<Label Text="{Binding Path=Value, StringFormat='滑块值为:{0:F0}'}" />
<Slider x:Name="slider" Maximum="360" />
</VerticalStackLayout>
<!--实现方式③:直接在Binding扩展标记上设置绑定源-->
<VerticalStackLayout>
<Label Text="{Binding Source={x:Reference slider}, Path=Value, StringFormat='滑块值为:{0:F0}'}" />
<Slider x:Name="slider" Maximum="360" />
</VerticalStackLayout>
<!--实现方式④:通过后台代码设置数据绑定,使用BindingContext-->
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
label.BindingContext = slider;
label.SetBinding(Label.TextProperty,"Value",stringFormat:"滑块值为:{0:F0}");
}
}
<!--实现方式⑤:通过后台代码设置数据绑定,不使用BindingContext-->
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
label.SetBinding(Label.TextProperty,new Binding("Value",source:slider,stringFormat:"滑块值为:{0:F0}"));
}
}
三、绑定路径Path:
通过绑定路径,选择绑定源的链接属性。如果属性为复杂类型或带索引的集合类型,可以通过点运算符或索引运算符选择子属性
<!-- 绑定TimePicker的Time属性的子属性Hours -->
<VerticalStackLayout HorizontalOptions="Center">
<Label
x:Name="label"
BindingContext="{x:Reference timePicker}"
Text="{Binding Time.Hours}" />
<TimePicker x:Name="timePicker" />
</VerticalStackLayout>
<!-- 绑定源为ContentPage,Content属性为VerticalStackLayout -->
<!-- VerticalStackLayout的Children属性是一个集合,通过索引获得第label1 -->
<!-- 最后绑定到label1的Text属性的Length属性 -->
<ContentPage
x:Class="MauiApp8.MainPage"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="page">
<VerticalStackLayout HorizontalOptions="Center">
<Label x:Name="label1" Text="HelloWorld" />
<Label x:Name="label2" TText="{Binding Source={x:Reference page}, Path=Content.Children[0].Text.Length}" />
</VerticalStackLayout>
</ContentPage>
四、绑定模式BindingMode:
通过设置绑定模式,可以规定两个链接属性之间数据响应的方式,有以下几种方式:
- TwoWay:数据在源和目标之间双向移动
- OneWay:数据从源流向目标
- OneWayToSource:数据从目标流向源
- OneTime:数据从源流向目标,但仅在更改BindingContext时,响应一次
- Default:默认值,大多数绑定目标的属性是OneWay,少部分是TwoWay,如:Editor/Entry/SercherBar/EntryCell的Text属性,DatePicker的Date属性,TimePicker的Time属性,Picker的SelectedIndex和SelectedItem属性,Slilder和Stepper的Value属性,Switch的IsToggled属性,SwitchCell的On属性,MultiPage的SelectedItem属性,ListView的IsRefreshing属性,其中大部分是表单类控件。
<!--在XAML中设置-->
<VerticalStackLayout>
<Label
x:Name="label"
BindingContext="{x:Reference slider}"
Scale="{Binding Value, Mode=TwoWay}"
Text="HelloWorld!"/>
<Slider x:Name="slider" Maximum="360" />
</VerticalStackLayout>
<!--在后台代码C#中设置-->
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
label.BindingContext = slider;
label.SetBinding(Label.ScaleProperty, "Value", BindingMode.TwoWay);
}
}
五、绑定数据源的字符串格式化StringFormat
如果绑定目标的属性为字符串,且绑定模式为OneWay或TwoWay,可以使用StringFormat对数据源进行格式化。<Label Text="{Binding Value,StringFormat='The slider value is {0:F2}'}" />
。如上例,{0:F2},其中0为绑定源数据值的占位符号,F2为输出格式。需要注意的是,即使绑定模式为TwoWay,使用StringFormat后,数据仅从源流向目标。
六、绑定源设置为自身或上级RelativeSource(相对于自己的源)
(1)绑定到自身
<!--WidthRequest绑定元素自身的HeightRequest属性-->
<VerticalStackLayout>
<Slider x:Name="slider" Maximum="500" />
<BoxView
CornerRadius="20"
HeightRequest="{Binding Source={x:Reference slider}, Path=Value}"
WidthRequest="{Binding Source={RelativeSource Self}, Path=HeightRequest}"
Color="LightBlue" />
</VerticalStackLayout>
<!--ListView继承ContentPage的BindingContext-->
<!--BindingContext通过Binding,绑定到自身的DefaultViewModel属性上-->
<!--DefaultViewModel,通过后台代码定义-->
<ContentPage ...
BindingContext="{Binding Source={RelativeSource Self}, Path=DefaultViewModel}">
<StackLayout>
<ListView ItemsSource="{Binding Employees}">
...
</ListView>
</StackLayout>
</ContentPage>
(2)绑定到上级
<!--绑定到上级的指定类型元素AncestorType-->
<!--如果指定类型有多个,则指定上级元素的第几层AncestorLevel-->
<!--绑定上级使用较少,因为可以直接通过x:Reference绑定指定元素-->
<Label Text="{Binding Source={RelativeSource AncestorType={x:Type Entry}, AncestorLevel=2}, Path=Text}" />
(3)控件模板将BindingContext绑定到应用该模板的对象,在控件模板中详述
<!--相当于控件模板中的元素的属性值,在使用控件模板时传递,而不是在定义控件模板时硬编码-->
<ContentPage ...>
<ContentPage.Resources>
<ControlTemplate x:Key="CardViewControlTemplate">
<Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
BorderColor="{Binding BorderColor}"
......>
......
</Frame>
</ControlTemplate>
</ContentPage.Resources>
<StackLayout>
<controls:CardView BorderColor="DarkGray"
......
ControlTemplate="{StaticResource CardViewControlTemplate}" />
</StackLayout>
</ContentPage>
七、绑定源不存在链接属性的回退值或链接属性值为Null时的替换值
<!--Entry不存在TextAbsent属性,label1通过FallbackValue设置回退值-->
<!--页面初始化时,Entry的Text为Null,label2通过TargetNullValue设置替换值-->
<StackLayout Padding="30">
<Entry x:Name="entry" Placeholder="请输入"/>
<Label x:Name="label1" Text="{Binding Source={x:Reference entry}, Path=TextAbsent, FallbackValue='绑定源不存在TextAbsent属性'}" />
<Label x:Name="label2" Text="{Binding Source={x:Reference entry}, Path=Text, TargetNullValue='未输入值'}" />
</StackLayout>
八、通过编译绑定,使用编译时绑定校验功能,并提升绑定性能。x:Datatype
XAML的绑定在运行时解析,造成两个问题:一是无法对绑定进行编译时校验,要到运行时才能发现;二是存在性能损耗。x:Datatype的作用,就是在编译时,明确绑定上下文的类型,可以明显改善以上两个问题,称之为编译时绑定。在XAML元素属性上声明x:Datatype后,绑定作用会向下延伸到子元素。所以一般情况下,x:Datatype和BindingContext定义在同一层级的元素上,但也可以在视觉树的任一层级声明。除了和BindingContext一起使用外,x:Datatype还可作用于DataTemplate,使集合绑定也变成编译时。
<!--BindingContext和x:Datatype都定义在ContentPage元素上-->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="MauiApp7.MainPage"
x:DataType="local:HslColorViewModel">
<ContentPage.BindingContext>
<local:HslColorViewModel Color="Sienna" />
</ContentPage.BindingContext>
<StackLayout Margin="10, 0">
<Label Text="{Binding Name}" />
<Slider Value="{Binding Hue}" />
<Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}" />
<Slider Value="{Binding Saturation}" />
<Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}" />
<Slider Value="{Binding Luminosity}" />
<Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}" />
</StackLayout>
</ContentPage>
<!--在DataTemplate中使用-->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="MauiApp7.MainPage"
x:DataType="local:HslColorViewModel">
<Grid>
<ListView ItemsSource="{x:Static local:NamedColor.All}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:NamedColor">
<ViewCell>
<StackLayout Orientation="Horizontal">
<BoxView Color="{Binding Color}"/>
<Label Text="{Binding FriendlyName}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ContentPage>