WPF进阶技巧和实战08-依赖属性与绑定02

将元素绑定在一起

数据绑定最简单的形式是:源对象是WPF元素而且源属性是依赖项属性。依赖项属性内置了更改通知支持,当源对象中改变依赖项属性时,会立即更新目标对象的绑定属性。

元素绑定到元素也是经常使用的,可以使元素的交互自动化,而不必编写代码。

绑定表达式

Visibility="{Binding Path=IsChecked, 
ElementName=rdKer, 
Converter={StaticResource EqualVisibleConverter}, 
ConverterParameter=1}"

此处绑定设置两个属性,ElementName属性指示源元素,Path属性指示源元素的属性。此处使用Path而不是Property,是因为Path可能指向属性的属性,也可能指向属性的索引器,用于构建多层次路径。

绑定错误

WPF不会引发异常类通知与数据绑定相关的问题,如果指定的元素不存在或者属性不存在,那么不会收到任何指示,只是不能在目标属性中显示数据。

绑定模式

名称 说明
OneWay 当源属性变化时更新目标属性
TwoWay 源属性变化时更新目标属性,目标属性变化时更新源属性
OneTime 最初根据源属性值设定目标属性值。然而,其后的所有改变都会被忽略(除非绑定被设置为一个完全不同的对象或者调用UpdateTarget()方法)。如果源属性不变化,可使用这种模式降低开销
OneWayToSource 和OneWay类似,但方向相反。当目标属性发生变化时更新源属性,但目标属性永远不会被更新
Default 此类绑定依赖于目标属性。既可以是双向的,也可以是单向的。除非明确另一种模式,否则所有绑定都是用该方法。

使用代码创建绑定

基于标记的绑定比通过代码创建的绑定常见。除非一些特殊情况:

  • 创建动态绑定:如果希望根据运行时修改绑定,或者根据环境创建不同的绑定,这时使用代码创建绑定通常更合理(也可以在窗口的Resources集合中定义可能希望使用的每个绑定,并且添加使用合适的绑定对象调用SetBinding()的代码)
  • 删除绑定:如果希望删除绑定,从而可以通过普通方式设置属性,需要借助ClearBinding()或者ClearAllBindings()方法。仅为属性应用新值是不够的-如果正在使用双向绑定,设置的值会传播到链接的对象,并且两个属性保持同步。
  • 创建自定义控件:为让他人能够更容易地修改您构建的自定义控件外观,需要将特定细节(如事件处理程序和数据绑定表达式)从标记移到代码中。

使用代码检索绑定

可使用代码检索绑定并检查其属性,而不必考虑绑定最初是用代码还是标记创建的。

  • 使用静态方法BindingOperations.GetBinding()来检索相应的Binding对象,分别传入绑定元素和具有绑定表达式的属性。可以通过Binding对象获取其各种属性
  • 通过调用BindingOperations.GetBindingExpression()方法获取更加实用的BindingExpression对象,和上面的参数相同。

绑定更新

数据源的变化会立即影响目标,但是目标到源未必立即执行、这个行为由Binding.UpdateSourceTrigger属性控制。

名称 说明
PropertyChanged 当目标属性发生变化时立即更新源
LostFocus 当目标属性发生变化并且目标丢失焦点时更新源
Explicit 除非调用BindingExpression.UpdateSource()方法,否则无法更新
Default 根据目标属性的元素确定更新行为(从技术角度看,是根据FrameworkPropertyMetadata.DefaultUpdateSourceTrigger属性决定更新行为)。大多数默认是PropertyChanged,但是TextBox.Text是LostFocus

绑定延迟

在极少数情况下,需要防止数据绑定触发操作和修改源对象,至少在某一段时间内时这样的。可能要添加短暂的延时时间,避免过分频繁地触发操作,需要使用Binding对象的Delay属性,等待数毫秒之后再提交更改。

绑定到非元素对象

在数据驱动的应用程序中,更多的操作是创建从不可见对象中提取数据的绑定表达式。唯一的要求是显示的信息必须是公有属性,不能采用私有信息或者字段。当绑定到非元素对象时,需要设置数据源,而不是使用Binding.ElementName属性

Source属性

该属性是指向源对象的引用,也就是提供数据的对象。

RelativeSource属性

这是引用,使用RelativeSource对象指向源对象。可以在当前元素的基础上构建引用,在编写控件模板以及数据模板是很方便。RelativeSource的4中模式:

名称 说明
Self 表达式绑定到同一元素的另一个属性上
FindAncestor 绑定到父元素,WPF将查找元素树直到发现期望的父元素。为了指定父元素,还必须设置AncestorType属性以提示希望查找的父元素的类型。还可以使用AncestorLevel属性略过一定数量的特定元素。比如AncestorTpye={x:Type ListBoxItem}并且AncestorLevel=3,标识希望绑定到第3个ListBoxItem(从当前节点沿着树查找),默认情况下AncestorLevel=1
PreviousData 表达式绑定到数据绑定列表中的前一个数据项,在列表项中会使用这种模式
TemplateParent 表达式绑定到应用模板的元素。只有当绑定位于控件模板或者数据模板内部时,这种模式才会工作

DataContext属性

如果没有指定Source或者RelativeSource属性数据源,WPF就从当前元素开始在元素树上向上查找,直到第一个非空 的DataContext属性。

数据绑定

WPF数据绑定允许创建从任何对象的任何属性获取信息的绑定,并且可以使用创建的绑定填充任何元素的任何属性。

使用自定义对象绑定到数据库

数据库中的提取信息,然后转换成自定义的对象中,将自定义对象作为数据源绑定到控件中进行显示某些属性。

具有null值的绑定,在数据类中,可通过简单的值类型使用可控数据类型来反应数据库中可空的字段。如果是引用类型,可以直接使用,具有null值。绑定null值的结果是可以预测的:对于数字字段,这一行为能够区分缺少数据和零值的情况。可以通过绑定表达式中设置TargetNullValue属性类改变WPF对null值的处理方式。

绑定到对象集合

绑定到单个对象是非常直观的。但是当需要绑定的对象是某些集合时,就需要使用更高级的元素。所有派生自ItemsControl的类都能够显示条目的完整列表。为了支持集合绑定,ItemsControl类定义了3个重要的属性:

名称 说明
ItemsSource 指向的集合包含将在列表中显示的所有对象
DisplayMemberPath 确定用于为每个项创建显示文本的属性
ItemTemplate 接受的数据模板用于每个项创建可视化外观。这个属性比DispalyMemberPath属性强大的多

只要是支持IEnumerable接口(数组、各种类型的集合以及许多特殊的封装了数据项组的对象都支持该接口),都可以用来填充ItemsSource属性。然而,基本的IEnumerable接口仅支持只读绑定。

提高大列表性能

  1. 虚拟化

WPF列表控件提供的最重要的功能是UI虚拟化(仅为当前显示项创建容器对象的一种技术)。例如一个具有5000条记录的ListBox控件,但是可见区域只能包含10条记录,ListBox只创建10个ListBoxItem对象,而不是全部记录都创建。

UI虚拟化支持实际上没有被构建进ListBox或者ItemsControl类,而是被硬编码到VirtualizingStackPanel容器,除了支持虚拟化外,该面板和StackPanel面板的功能类似。ListBox、ListView以及DataGrid是自动使用虚拟化面板来布局其子元素,Commbox使用标准的没有虚拟化的StackPanel面板,如果要支持虚拟化,就要明确地通过提供新的ItemsPanelTemplate来添加虚拟化支持。

<ComboBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
    <ComboBox.ItemTemplate>
        <ItemContainerTemplate>
            <VirtualizingStackPanel/>
        </ItemContainerTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

TreeView是另一个支持虚拟化的控件,但在默认情况下,她关闭了改功能。可以通过属性来启用特性。

<TreeView VirtualizingPanel.IsVirtualizing="True"/>

一般情况下,对列表控件进行绑定数据时,不会将很多的数据绑定到列表控件,会使用分页等操作进行少数数据的展示。

  1. 项容器再循环

通常当滚动支持虚拟化时,控件不断地创建新的项容器对象以保持新的可见项,如果启用了项容器再循环,ListBox控件将只保持少量ListBoxItem对象存活,当滚动时,通过新数据加载这些ListboxItem对象,从而重复使用他们。提高了滚动性能,降低了内存消耗。除DataGrid之外的所有控件,该特性默认是禁用的。

VirtualizingPanel.VirtualizationMode=”Recycling”

  1. 缓存长度

VirtualizingStackPanel创建了几个超过其显示范围的附加项。这样,在开始滚动时,就可以立即显示这些项。可以使用CacheLength和CacheLengthUnit来选择如何指定附加项的数量。

VirtualizingPanel.CacheLength=”10″ VirtualizingPanel.CacheLengthUnit=”Item”

CacheLengthUnit允许选择如何指定附加项的数量:项数、页数(其中,单页包含适应于控件可视窗口的所有项)或者像素数(如果项显示不同大小的图片,这个选择更合理)

  1. 延迟滚动

为进一步提高滚动性能,可开启延迟滚动特性。当用户在滚动条上拖动滚动滑块时不会更新列表显示,只有用户释放滚定滑块时才刷新。

ScrollViewer.IsDeferredScrollingEnabled=”True”

VirtualizingStackPanel通常使用基于项的滚动,这意味着当向下滚动少许时,下一项就显示出来。无法滚动查看项的一部分,在面板上至少会滚动一个完整项。可以通过属性覆盖该行为:

VirtualizingPanel.ScrollUnit=”Pixel”

验证

在任何数据绑定中,另一个要素是验证(捕获非法数值并拒绝这些非法数值的逻辑)。可直接在控件中构建验证(例如,通过响应文本框中的输入并拒绝非法字符),这种低级的方法限制了灵活性。验证提供了两种方法用于捕获非法值:

  • 可在数据对象中引发错误。为告知WPF发生了错误,只需要从属性设置过程中抛出异常。通常,WPF会忽略所有在设置属性时抛出的异常,但可以进行配置,从而显示更有帮助的可视化指示。另一种选择是在自定义的数据类中实现INotifyDataErrorInfo或IDataErrorInfo接口,从而可得到指示错误的功能而不会抛出异常
  • 可在绑定级别上定义验证。这种方法可获得使用相同验证的灵活性,而不必考虑使用的是哪个控件输入。更好的是,因为是在不同的类中定义验证,所以可很容易地在存储类似数据类型的多个绑定中重用验证。

只有当来自目标的值正被用于更新源时才会应用验证,也就是只有当使用TwoWay模式或者OneWayToSource模式时的绑定才应用验证。

  1. 在数据对象中进行验证
public class MainWindowViewModel : NotifyPropertyBase
{
    private decimal? _productPrice;
    public decimal? ProductPrice
    {
        get => _productPrice;
        set
        {
            if (value.HasValue && value.Value < 0)
            {
                throw new Exception("ProductPrice 不能小于0");
            }
            else
            {
                _productPrice = value;
                OnPropertyChanged("ProductPrice");
            }
        }
    }
}

上面的示例中主要验证价格不能小于0,但是不能为用户提供任何与问题相关的反馈(WPF会忽略当设置和获取属性时发生的数据绑定错误),对于这种情况,用户无法知道更新已经被拒绝。实际上,非法的值依然保留在文本框中,只是没有被应用到绑定的数据对象。需要借助于ExceptionValidationRule验证规则。

  • ExceptionValidationRule验证规则

ExceptionValidationRule是预先构建的验证规则,它向WPF报告所有异常。要使用ExceptionValidationRule验证规则,必须将他绑定到Binding.ValidationRules集合中,如下代码:

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120">
    <TextBox.Text>
        <Binding Path="ProductPrice">
            <Binding.ValidationRules>
                <ExceptionValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

这个例子中,同时使用了值转换器和验证规则。通常是在转换值之前进行验证,但是ExceptionValidationRule比较特殊。它捕获在任何位置发生的异常,包括当编辑的值不能转换成正确数据类型时发生的异常、由属性设置器抛出的异常以及由值转换器抛出的异常。
当验证失败时,System.Windows.Controls.Validation类的附加属性记录下验证错误。对于每个失败的验证规则,WPF分一下3个步骤:

  • 在绑定的元素上(TextBox控件),将Validation.HasError附加属性设置为true
  • 创建包含错误信息的ValidationError对象(作为ValidationRule.Validate()方法的返回值)并将该对象添加到关联的Validation.Errors集合中
  • 如果Binding.NotifyOnValidationError属性设置为true,WPF就在元素上引发Validation.Error附加事件

当发生错误时,绑定控件的可视化外观也会发生变化。当控件的Validation.HasError附加属性设置为true时,WPF自动将控件使用的模板切换成Validation.ErrprTemplate附加属性定义的模板。在文本框中,新模板将文本框的轮廓改成一条红色的边框。

在大多数情况下,我们都希望用某种方式来增强提示,并提供与引发问题的错误相关的信息。这样Error事件才有意义。我们可以通过代码处理Error事件或者提供自定义模板,从而提供不同的可视化指示信息。

  • INotifyDataErrorInfo接口

WPF通过接口IDataErrorInfo和INotifyDataErrorInfo,允许构建报告错误的对象而不是直接抛出异常。两个接口具有相同的目标,即用更加人性化的错误通知系统来替代未处理的异常。IDataErrorInfo是初始的错误跟踪接口,为了是WPF向下兼容。INotifyDataErrorInfo接口具有同样的功能,但界面更加丰富。

public class MainWindowViewModel : NotifyPropertyBase, INotifyDataErrorInfo
{
    private decimal? _productPrice;
    public decimal? ProductPrice
        {
            get => _productPrice;
            set
            {
                if (value.HasValue && value.Value < 0)
                {
                    //throw new Exception("ProductPrice 不能小于0");
                    SetError("ProductPrice", new List<string> { "ProductPrice 不能小于0" });
                }
                else
                {
                    ClearErrors("ProductPrice");
                }

                _productPrice = value;
                OnPropertyChanged("ProductPrice");
            }
        }

    private Dictionary<string, List<string>> DictError = new Dictionary<string, List<string>>();

    private void SetError(string propertyName, List<string> propertyErrors)
        {
            DictError.Remove(propertyName);

            DictError.Add(propertyName, propertyErrors);

            if (ErrorsChanged != null)
            {
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

    private void ClearErrors(string propertyName)
        {
            DictError.Remove(propertyName);

            if (ErrorsChanged != null)
            {
                ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }

    /// <summary>
    /// 用于指示数据对象是否包含错误信息
    /// </summary>
    public bool HasErrors => DictError.Keys.Count > 0;

    /// <summary>
    /// 在添加和删除错误时发生
    /// </summary>
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    /// <summary>
    /// 提供完整的错误信息内容
    /// </summary>
    /// <param name="propertyName"></param>
    /// <returns></returns>
    public IEnumerable GetErrors(string propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            return DictError.Values;
        }

        if (DictError.ContainsKey(propertyName))
        {
            return DictError[propertyName];
        }
        return null;
    }
}

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Margin="200,200,0,0">
    <TextBox.Text>
        <Binding Path="ProductPrice" ValidatesOnNotifyDataErrors="True" NotifyOnValidationError="True"/>
    </TextBox.Text>
</TextBox>

通过上述两种形式实现异常通知,但是他们之间有本质的区别:触发异常时,不会在数据对象中更新属性。但是使用INotifyDataErrorInfo接口时,允许使用非法值,但是会标记出来,数据对象会被更新,可以使用通知或者事件告知用户。

  1. 自定义验证规则

应用自定义规则的方法和应用自定义转化器的方法类似。继承自ValidationRule,并重写Validate()方法。

public class CustomValidation : ValidationRule
{
    private decimal? _minValue = 0;
    private decimal? _maxValue = int.MaxValue;

    public decimal? MinValue
    {
        get => _minValue;
        set => _minValue = value;
    }



    public decimal? MaxValue
    {
        get => _maxValue;
        set => _maxValue = value;
    }





    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        decimal price = 0;

        try
        {
            string strValue = value.ToString();
            if (!string.IsNullOrEmpty(strValue))
            {
                price = decimal.Parse(strValue, cultureInfo);
            }
        }
        catch (Exception)
        {

            return new ValidationResult(false, "非法数据");
        }

        if (price < _minValue || price > _maxValue)
        {
            return new ValidationResult(false, "非法数据");
        }

        return new ValidationResult(true, null);
    }
}

<TextBox HorizontalAlignment="Left" VerticalAlignment="Top" Width="120" Margin="200,300,0,0">
    <TextBox.Text>
        <Binding Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99"/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

通常会使用同类规则的每个元素定义不同的验证规则对象。如果确实希望为多个绑定使用相同的验证规则,可将验证规则定义为资源,并在每个绑定中简单的使用静态标记指向该资源。
Binding.ValidationRules集合可包含任意数量的验证规则。将值提交到源时,WPF将按照顺序检查每个验证规则,如果所有规则验证都成功了,WPF接着会调用转化器为源应用值。

  1. 响应验证错误

在上述例子中,有关用户接收到错误的唯一指示是在违反规则的文本框周围的红色轮廓。为了提供更多信息,可以处理Error事件,当存储或者清楚错误时会引发该事件,但是前提是首先确保已经将 NotifyOnValidationError=”True”。

Error事件使用冒泡策略的路由事件,所以可通过在父容器中关联事件处理程序来为多个控件来处理Error事件。

<TextBox Width="120" Margin="200,300,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Validation.Error="TextBox_Error">
    <TextBox.Text>
        <Binding NotifyOnValidationError="True" Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

private void TextBox_Error(object sender, ValidationErrorEventArgs e)
{
    if(e.Action == ValidationErrorEventAction.Added)
    {
        MessageBox.Show(e.Error.ErrorContent.ToString());
    }
}

ValidationErrorEventArgs.Error属性提供了一个ValidationError的对象,包含引起问题的异常(Exception),违反的验证规则(ValidationRule),关联的绑定对象(BindingInError)以及ValidationRule对象返回的任何自定义消息(ErrorContent)。

  1. 获取错误列表

在某些情况下,可能需要获取当前窗口所有的错误信息,需要做的就是遍历元素树,获取每个元素的属性。

  1. 显示不同的错误指示符号

可以通过定义自己的错误模板,以适当的方式来标识错误信息。错位模板是装饰层,始终位于窗口内容之上的绘图层。

<TextBox Width="120" Margin="200,300,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Validation.Error="TextBox_Error">
    <TextBox.Text>
        <Binding NotifyOnValidationError="True" Path="ProductPrice">
            <Binding.ValidationRules>
                <local:CustomValidation MaxValue="99.99" />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
    <Validation.ErrorTemplate>
        <ControlTemplate>
            <TextBlock Text="***" Foreground="Red"/>
        </ControlTemplate>
    </Validation.ErrorTemplate>
</TextBox>