WPF_14_数据绑定

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中,可以使用 CacheLengthCacheLengthUnit 这两个属性进一步调整精确数量。

<!--默认-->
<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会忽略当设置和获取属性时发生的数据绑定错误,此时用户无法知道更新已经被拒绝。实际上,非法的值仍然保留在文本框中-只是没有被应用于绑定数据对象。

  1. 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 附加属性定义的模板。

  1. 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}"/>
我的公众号

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值