WPF数据绑定允许创建从任何对象的任何属性获取信息的绑定,并且可以使用创建的绑定填充任何元素的任何属性。WPF还提供了一系列能够处理整个信息集合的列表控件,并且允许在这些控件中进行导航。
构建数据对象
数据对象是准备在界面中显示的信息包。只要由公有属性组成(不支持字段和私有属性),任何类都可供使用。此外,如果希望使用这个对象进行修改(通过双向绑定),那么属性不能是只读的。
public class Product
{
private string modelNumber;
public string ModelNumber
{
get { return modelNumber; }
set { modelNumber = value; }
}
}
<TextBlock> Model Number:</TextBlock>
<TextBox Text="{Binding Path=ModelNumber}"></TextBox>
数据经常包含可空字段,这里的空值表示缺失的或不能使用的信息。在数据类中可用 decimal? 代替 decimal 等方式来表示。绑定null值,元素不会显示任何内容。可以通过绑定表达式中设置 TargetNullValue 属性来改变WPF对null值的处理方式。Text= "{Binding Path=Description, TargetNullValue=[No Description]}".
更改通知
如果数据在别的地方被修改了,怎么通知到界面上呢?
- 可以将 Product类中每个属性都改为依赖项属性(Product类必须继承自DependecyObject类)。但一般不这么干,最合理的做法是用于元素-在窗口中具有可视化外观的类。
- 可以为每个属性引发事件。事件以 propertyNameChanged的形式命名,属性变化时引发事件。
- 可以实现 System.ComponentModel.INodifyPropertyChanged 接口,无论何时属性发生变化,都必须引发PropertyChanged事件,此时仍引发事件但不必为每个属性定义单独的事件
public class Product : INofityPropertyChanged
{
private decimal unitCost;
public decimal UnitCost
{
get { return unitCost; }
set {
unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(PropertyEventArgs e)
{
if( null != PropertyChanged)
PropertyChanged(this, e);
}
}
如果好几个数值都发生了变化,可以调用 OnPropertyChanged() 方法并传递空字符串,WPF会重新评估绑定到类的任何属性的绑定表达式。
绑定到对象集合
单值绑定比较直观,集合绑定需要智能程度更高的元素,所有派生自 ItemsControl 的类都能显示条目的完整列表。ListBox, ComboBox, ListView 和 DataGrid(以及用于显示层次化数据的 Menu 和 TreeView).
为了支持集合绑定,ItemsControl 类定义了三个重要的属性:
属性名称 | 说明 |
---|---|
ItemsSource | 集合对象 |
DisplayMemberPath | 用于为每个项创建显示文本的属性 |
ItemTemplate | 接受的数据模板用于为每个项创建可视化外观 |
对集合的类型唯一的要求是支持 IEnumerable 接口。数组,各种类型的集合都支持该接口。然而,基本的 IEnumerable 接口仅支持只读绑定。
为启用集合更改跟踪,需要使用实现了 INotifyCollectionChanged 接口的集合。WPF提供了一个使用该接口的集合:ObservableCollection.
提高大列表的性能
虚拟化
WPF列表控件提供的最重要功能是UI虚拟化,是列表仅为当前显示项创建容器对象的一种技术。
比如一个具有 50000 条记录的ListBox控件,但可见区域只包含30条记录,ListBox将只创建30个 ListBoxItem(为确保良好的滚动性能,会再增加几个ListBoxItem对象)。
UI虚拟化支持实际上没有被构建进 ListBox 或 ItemsControl 类。而是被硬编码到 VirtualizingStackPanel 容器。除增加虚拟化支持,该面板和 StackPanel 面板的功能类似。ListBox, ListView以及 DataGrid 都自动使用 VirtualizingStackPanel 面板来布局它们的子元素,不需要采用额外的步骤就能获得虚拟化的支持。
然而,Combobox 使用标准的没有虚拟化支持的 StackPanel 面板。
<ComboBox>
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
TreeView 也支持虚拟化的控件,在默认情况下关闭了该支持。
<TreeView VirtualizingStackPanel.IsVirtualizing="True">
有许多因素可能会破坏UI虚拟化:
- 在 ScrollViewer 中放置列表控件。只要把ListBox控件放入不会试图限制其尺寸的容器中,会显示所有子项。例如,将ListBox控件放到 StackPanel 面板而不是 Grid 面板中。
- 改变列表控件的模板并且没有使用 ItemsPresenter. ItemsPresenter 使用 ItemsPanelTemplate ,该模板指定了 VirtualizingStackPanel 面板。
- 不使用数据绑定。
项容器再循环
一般,当滚动支持虚拟化列表时,控件不断地创建新的项容器对象以保存新的可见项。但如果启用了项容器再循环,ListBox控件将只保持少量 ListBoxItem 对象存活,并当滚动时通过新数据加载这些 listboxItem 对象,从而重复使用它们。
<ListBox VirtualizingStackPanel.VirtualizationMode="Recycling"/>
项容器再循环提高了滚动性能,降低了内存消耗量,因为垃圾收集器不需要查找旧的项对象并释放它们。通常为了确保向后兼容,对于除 DataGrid 之外的所有控件,该特性默认是禁用的。如果是一个大列表,应当启用该特性。
缓存长度
为了确保良好的滚动,VirtualizingStackPanel会多创建一些项。这样开始滚动时就可以立即显示这些项。在以前版本将多个附加项硬编码到 VirtualizingStackPanel 中。在WPF 4.5中,可以使用 CacheLength 和 CacheLengthUnit 这两个属性进一步调整精确数量。
<!--默认-->
<ListBox VirtualizingStackPanel.CacheLength="1" VirtualizingStackPanel.CacheLengthUnit="Page"/>
<!--在可见项前后各存储100项-->
<ListBox VirtualizingStackPanel.CacheLength="100" VirtualizingStackPanel.CacheLengthUnit="Item"/>
<!--在可见项前存储100项,在后存储500项-->
<ListBox VirtualizingStackPanel.CacheLength="100" VirtualizingStackPanel.CacheLengthUnit="Item"/>
VirtualizingStackPanel 会立即显示创建的可见项集。此后,开始在优先级较低的后台线程上填充缓存,不会阻塞程序。
延迟滚动
使用延迟滚动特性,当用户在滚动条上拖动滑块时不会更新列表显示。只有释放了滑块时才刷新。
<ListBox ScrollViewer.IsDeferredScrollingEnabled="True"/>
验证
验证是指用于捕获非法数值并拒绝这些非法数值的逻辑。验证提供了两种方法用于捕获非法值:
- 可在数据对象中引发错误。通常,WPF会忽略设置属性时抛出的异常,但可以进行配置,从而显示更有帮助的可视化指示。
- 可在绑定级别上定义验证。
只有当来自目标的值正在被用于更新源时才会应用验证-即只有当使用 TwoWay模式或 OneWayToSource 模式的绑定时才应用验证。
在数据对象中进行验证
public decimal UnitCost
{
get { return unitCost; }
set
{
// 不允许输入负数
if (value < 0 )
throw new ArgumentException("UnitCost cannot be negative.");
else
{
unitCost = value;
OnPropertyChanged(new PropertyChangedEventArgs("UnitCost"));
}
}
}
因为,WPF会忽略当设置和获取属性时发生的数据绑定错误,此时用户无法知道更新已经被拒绝。实际上,非法的值仍然保留在文本框中-只是没有被应用于绑定数据对象。
- ExceptionValidationRule 是预先构建的验证规则,向WPF报告所有异常。
<TextBox>
<TextBox.Text>
<Binding Path="UnitCost">
<Binding.ValidationRules>
<ExceptionValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
当验证失败时,WPF会采取以下三个步骤:
- 在绑定的元素上,将Validation.HasError 附加属性设置为 true
- 创建包含错误细节的 ValidationError 对象,并将该对象添加到关联的Validation.Errors集合中
- 如果Binding.NotifyOnValidationError属性被设置为true,WPF在元素上引发 Validation.Error附加事件
当发生错误时,控件的可视化外观会发生变化,模板切换为由 Validation.ErrorTemplate 附加属性定义的模板。
- INofityDataErrorInfo 接口,允许构建报告错误的对象而不会抛出异常。
public class Product : INofityPropertyChanged, INofityDataErrorInfo
{
private Dictionary<string, List<string>> errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void SetErrors(string propertyName, List<string> propertyErrors)
{
errors.Remove(propertyName);
errors.Add(propertyName, PropertyErrors);
if(ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
private void ClearErrors(string propertyName)
{
errors.Remove(propertyName);
if(ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public string ModelNumber
{
get {return modelNumber;}
set
{
modelNumber = value;
bool valid = true;
foreach ( char c in modelNumber)
{
if(!Char.IsLetterOrDigit(c))
{
valid = false;
break;
}
}
if(!valid)
{
List<string> errors = new List<string>();
errors.Add("The ModelNumber can only contain letters and numbers.");
SetErrors("ModelNumber", errors);
}
else
{
ClearErrors("ModelNumber");
}
OnPropertyChanged(new PropertyChangedEventArgs("ModelNumber"));
}
}
}
为告知WPF使用 INofityDataErrorInfo接口,并通过该接口在修改属性时检查错误,ValidatesOnNotifyDataErrors属性必须为true.
自定义验证规则
自定义验证规则定义继承自 ValidationRule ,并重写 Validate() 方法。
public class PositivePriceRule : ValidationRule
{
private decimal min = 0;
private decimal max = Decimal.MaxValue;
public decimal Min
{
get{return min;}
set{min = value;}
}
public decimal Max
{
get{return max;}
set{max = value;}
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
decimal price = 0;
try
{
if(((string)value).Length > 0)
price = Decimal.Parse((string)value, NumberStyles.Any, culture);
}
catch
{
return new ValidationResult(false, "Illegal characters.");
}
if((price < Min) || (price > Max))
{
return new ValidationResult(false, "Not in the range " + Min + "to " + Max + ".");
}
else
{
return new ValidationResult(ture, null);
}
}
}
将验证规则添加到 Binding.ValidationRules 集合,和元素进行关联。
<TextBox>
<TextBox.Text>
<Binding Path="UnitCost">
<Binding.ValidationRules>
<local:PositivePriceRule Max="999.99"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
通常,会为使用同类规则的每个元素定义不同的验证规则对象。如果希望为多个绑定使用完全相同的验证规则,可将验证规则定义为资源,并在每个绑定中简单使用 StaticResource 标记扩展指向该资源。
Binding.ValidationRules 集合可包含任意数量的验证规则,WPF将按顺序检查每个验证规则。如果成功了接着会调用转换器并为源应用值。
相应验证错误
默认验证失败时,文本框周围显示红色轮廓。为提供更多信息,可以处理 Error 事件,当存储或清楚错误时引发该事件,但前提要设置 Binding.NotifyOnValidationError.
<Binding Path="UnitCost" NofityOnValidationError="True">
Error事件是冒泡策略的路由事件,可通过在父容器中关联事件处理程序为多个控件处理 Error 事件。
<Grid Name="gridProductDetails" Validation.Error="validationError">
private void validationError(object sender, ValidationErrorEventArgs e)
{
if(e.Action == ValidationErrorEventAction.Added)
MessageBox.Show(e.Error.ErrorContent.ToString());
}
验证多个值
创建验证规则,但不是应用到单个绑定表达式,而是将其附加到包含所有绑定控件的容器上(通常就是将DataContext设置为数据对象的同一容器)。当提交编辑时,WPF会使用该验证规则验证整个数据对象。
<Grid Name="gridProductDeltails" DataContext="{Binding ElementName=lstProducts, Path=SelectedItem}"
TextBox.LostFocus="txt_LostFocus">
<Grid.BindingGroup>
<BindingGroup x:Name="productBindingGroup">
<BindingGroup.ValidationRules>
<local:NoBlankProductRule/>
</BindingGroup.ValidationRules>
</BindingGroup>
</Grid.BindingGroup>
<!--多个元素-->
<TextBox Text="{Binding Path=ModelNumber}"/>
...
</Grid>
// 规则验证方法
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
BindingGroup bindingGroup = (BindingGroup)value;
// 默认情况下 product 是原始对象,没有应用任何修改
Product product = (Prodect)bindingGroup.Items[0];
// 为了得到要验证的新值,需要调用 GetValue()方法
string newModelName = (string)bindingGroup.GetValue(product, "ModelName");
if (newModelName == "")
{
return new ValidationResult(false, "A product requires a ModelName.");
}
else
{
return new ValidationResult(true, null);
}
}
// 当编辑控件失去焦点时运行事件处理程序
private void txt_LostFocus(object sender, RoutedEventArgs e)
{
productBindingGroup.CommitEdit();
}
绑定组使用事务处理编辑系统,这意味着在运行验证逻辑之前需要正式地提交编辑。最简单的做法是调用 BindingGroup.CommitEdit()方法。
数据提供者
通常通过设置元素的 DataContext 属性或列表控件的 ItemsSource 属性来提供数据源。数据提供者是另一种方式,目前WPF只提供了以下两个数据提供者:
- ObjectDataProvider,通过调用另一个类中的方法获取信息。
- XmlDataProvider,直接从XML文件获取信息
ObjectDataProvider
- 能够创建需要的对象并为构造函数传递参数
- 能够调用所创建对象中的方法,并向它传递方法参数
- 能够异步地创建数据对象
<!--一个基本的ObjectDataProvider数据提供者,创建了StoreDB类的一个实例,调用实例的GetProducts方法-->
<Window.Resources>
<ObjectDataProvider x:Key="productsProvider" ObjectType="{x:Type local:StoreDB}"
MethodName="GetProducts"/>
</Window.Resources>
<!--创建绑定,从数据提供者获取数据源-->
<ListBox Name="lstProducts" DisplayMemberPath="ModelName"
ItemsSource="{Binding Source={StaticResource productsProvider}}"/>
上面标签像是绑定到ObjectDataProvider,但其智能程度足够高,知道实际上需要绑定到从 GetProducts() 方法返回的产品列表。另外支持异步数据查询,ObjectDataProvider会在后台进程中执行工作,界面也能相应用户的操作。
<ObjectDataProvider IsAsynchronous="True"/>
XmlDataProvider
XmlDataProvider数据提供者提供了一种简捷方法,用于从单独的文件,Web站点或应用程序资源中提取XML数据。XmlDataProvider默认是异步的。
<!--如果不能确定文件名,也可以通过代码设置Source属性-->
<XmlDataProvider x:Key="productsProvider" Source="store.xml" XPath="/Prodcuts"/>
<!--绑定列表-->
<ListBox DisplayMemberPath="ModelName"
ItemsSource="{Binding Source={StaticResource products},XPath=product}"/>
<TextBox Text="{Binding XPath=ModelNumber}"/>