数据、视效与行为

数据

  • WPF 中的对象,我觉得大致可以分为两类,一个是前台对象,一个是后台对象,前台对象指的就是 View,后台对象指的就是 ViewModel,View 对象又分为 xaml 状态和 cs 状态,本质是一个 partial 类。
  • ViewModel 对象与 View 中的对象是相互独立的,因此需要借助一些手段来让他们关联起来

Resources

  • Resources 相当于你当前这个对象下的一个仓库,本质类似于一个 Dictionary,你在其中设置的对象本质就是你实例化了一个对象,然后将其放入了 Dictionary 中
  • 当你通过 {StaticResource} 或 {DynamicResource} 去访问时,就会自动去查找最近(父子关系下)的 Resources,如果找不到,就继续往外找,直到找到 APP 对象为止

Binding

  • 在 WPF 中,可以通过 “Binding” 机制,来实现将两个数据关联起来,这两个数据既可以是 View 与 ViewModel 之间,也可以是 View 内部的两个对象中的两个属性,并且根据被绑定对象的特点,还能够实现单向通知以及双向通知功能

  • 关联机制主要需要注意以下四点

    • 目标(Target):UI 元素的依赖属性(如 TextBox.TextButton.Content
    • 源(Source):提供数据的对象,通常就是 DataContext
    • 路径(Path):指定要绑定到源对象中的哪个属性(如 Person.Name
    • 绑定模式(Mode):决定数据流动方向(OneWay、TwoWay、OneTime 等)
  • Binding 可以通过以下几种方式选择 Source

    • DataContext:这算是一种保底机制,当没有显式指定 Source 时,Binding 会以当前对象或其视觉树外层对象的 DataContext 为目标 Path 的 Source(注意,这里表明了如果没有显式设置 Source,则默认也是去找 DataContext,而不是去找对象自己身上的属性)
    • Source:可以通过设置 Source 属性显式绑定一个对象,这个对象可以是在 Resources 中设置好的某个实例,也可以是通过 {x: Static } 直接绑定一个静态对象(静态字段、枚举值等)
    • RelativeSource:这是一种以相对关系去设置 Source 的方法,常用方式有以下三种
      • Self:以当前对象自己为 Source
      • FindAncestor:向上查找某个特定类型的对象作为 Source
      • TemplatedParent:以应用了此模版的对象为 Source
    • ElementName:显式指定某个设置了特定 Name 的对象作为 Source
    • XPath:当数据源为 XML 时,可以使用的特殊用法
    • Reference:某些情况下,当目标对象不在可视化树内时,可能需要通过其和 DataProxy 来进行绑定
    • 特殊情况:当父对象是 ListBox 之类的可以设置 ItemSource 的对象时,每个 Item 它们的 Source 默认是这个 ItemSource 中的每个对象
  • 可视化树机制:这一点就类似 PhotoShop 之类的图像编辑器中的图层机制一样,WPF 默认有个主可视化树,除此之外,像 Popup, ContextMenu, AdornerLayer 等都会形成各自的可视化树(也就类似于其他图层)

  • 绑定限制

    • 像是 RelativeSourceElementName 查找元素时,都会受到可视化树的限制,前者是因为依赖于可视化树的层级结构,当元素不在同一可视化树中时,自然无法通过层级结构查找到,而后者则是受到 NameScope 的影响,一个元素在注册 x:Name 时,本质是注册到当前可视化树的 NameScope 下,因此不同可视化树中无法访问到对方,但是 x:Reference 由于是解析 xaml 时读取,因此不受 NameScope 影响
  • 默认值与异步加载:在 Binding 中,可以通过 FallbackValue 来为目标属性规定一个源属性未绑定成功或未加载完成(可能此值是需要异步请求的值)时默认显示的值;当 FallbackValue 还无法满足需求时,可以使用 PriorityBinding 来实现优先级绑定,它会从上到下找到第一个可取值的值作为当前目标属性的值,而无视优先级更低的那些值

  • 值转换器(Converter):用于在源值和目标值之间进行类型转换,类需要实现 IValueConveter 来实现不同类型的源属性与目标属性之间的相互转化,如果需要避免重复在 Resource 中实现转换实例,可以考虑直接建立一个静态实例

  • UpdateSourceTrigger​:用于控制当数据从视图变化传输回 ViewModel 时,它的触发时机​,它有以下四个值

    • PropertyChanged:当属性发生变化时立刻变化
    • LostFocus:当控件失去焦点时才变化
    • Explicit:不自动更新,需要在代码中通过 BindingExpression.UpdateSource() 手动触发更新
    • Default:默认值,大部分可编辑控件是 LostFocus,少部分例如滑动条之类的控件默认为 PropertyChanged
  • 验证与错误处理

    • ValidationRules:通过继承一个 ValidationRule 类来实现验证方法,简单,但是只能返回一个错误
    • INotifyDataErrorInfo:通过实现 INotifyDataErrorInfo 接口,能支持复杂的验证逻辑,返回多个错误,但是较为复杂,且 Binding 时需要注意将 ValidatesOnNotifyDataErrors 设为 True
    • IDataErrorInfo:也是一个错误接口,比较老的一种实现方法,目前不用,需要将 ValidatesOnDataErrors 设为 True
    • ValidatesOnExceptions:设为 True 时支持将 setter 中抛出的异常作为错误处理
    • 可以将 NotifyOnValidationError 设为 True,此时遇到错误时,会触发 Validation.Error 事件,如果为其绑定处理逻辑,即可进行一些想要的做(如禁止按钮点击等)
  • 多绑定

    • 多绑定需要实现 IMultiValueConverter 接口的来决定如何组合多个绑定对象以及输出什么结果
  • 绑定调试:可以通过在 Binding 时设置 PresentationTraceSources.TraceLevel 以及对应等级 None, Low, Medium, High 来在输出窗口中显示绑定时的错误信息

  • BindingExpression:它是 Binding 对象在代码中的一个具象对象表示,通常通过 控件.GetBindingExpression() 拿到它,你可以通过它来实现 UpdateSource()UpdateTarget() 来推送给源或目标绑定对象,也可以通过它拿到 ResolvedSourceResolvedSourceProperty ,当然,非必要的情况下不推荐直接操作它以免打破 Mvvm 自动绑定与更新;你可以通过在代码中尝试拿到它来判断是否绑定成功

    • Binding 的本质可以说是由 BindingExpression 持有源对象和目标对象,并且订阅两者中对应的CLR/动态/依赖属性依赖属性的值变化事件,在收到其中一方的事件时,去修改另一方
    // ViewModel中的代码
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            if (_name != value)
            {
                _name = value;
                // 关键:触发事件并携带属性名
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
    
    // View 伪代码:WPF内部近似逻辑
    void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Name") // 检查是否是绑定的属性
        {
            // 重新获取值并更新UI
            var newValue = ((ViewModel)sender).Name;
            TextBlock.SetCurrentValue(TextBlock.TextProperty, newValue);
        }
    }
    
    • 关于 BindingExpression 是如何找到源对象和目标对象的属性并获取与修改它们的,首先,目标对象由于必须是依赖属性,所以可以很轻松的通过 DepandencyObject.GetValue(TargetDP)DependencyObject.SetValue(TargetDP, value) 的方式去处理,而对于 CLR 属性 和动态属性(如 dynamic),则一般会通过反射的方式拿到 gettersetter,并且为了优化考虑,还会使用 Expression Trees 或 ILGenerator 动态生成代码,将之前拿到的 gettersetter 转换为委托并缓存起来,此时再次访问时就相当于 (object instance) => ((User)instance).Name;

属性

  • WPF 中的属性大致分为三类:
    • 普通属性:既对象身上的属性,一般来说对 WPF 特有功能支持有限,例如数据绑定、样式、动画、属性继承等
    • 依赖属性:一种拓展过的属性,支持了更多的 WPF 功能,使用 DependencyProperty.Register 注册到系统内,通过指定 ownType 能够控制哪些对象能够使用此依赖属性
    • 附加属性:属于依赖属性的一种,本质也是一种拓展过的属性,与依赖属性相比,它主要是用在非 own 的其他对象上
  • 依赖属性相当于给一个对象增加一个属性,而附加属性相当于给对象增加一个标记
  • 访问依赖属性时,可以直接像访问普通属性一样通过 . 进行访问,也可以通过 GetValue()SetValue() 进行访问;访问附加属性时,需要通过 own 下的静态方法 SetXXX()GetXXX() 进行访问
  • 附加属性通常用来拓展行为,详情在 行为 一节中进行讨论

视效

  • WPF 中的视觉效果主要通过控件身上的依赖属性以及其对应值决定

Style

  • Style 是 WPF 中一种预定义的视效模板,它的本质也是一种对象
  • Style 中可以指定 TargetTpye,以便在定义时,能够设定目标 Type 上的依赖属性值,此外在不定义 x:Key 此 Style 会自动成为隐式样式,默认应用于此作用域下的所有对应类型上

模板

  • 可以通过模板来规划整个控件的显示结构,目前常见的模板有以下三种
    • ControlTemplate:彻底改变控件的视觉树结构, 让你能自定义整个控件的显示,其中有个很关键的子对象 ContentPresenter,用此对象能够规定控件中能够让外部定义的 “内容” 的所处区域,在 ControlTemplate 中,通常会使用到 TemplateBinding 来关联到 TargetType 上的依赖属性
    • DataTemplate:它的作用是指定被 Content 绑定的对象在界面中是如何显示的,相比于 ControlTemplate 只能适用于 ControlContent 及其派生类,它可以适用于任意对象;它通常应用于 ContentTemplateItemTemplate 属性;它也类似 Style,能够指定特定类型,它使用的关键字是 DataType,并且它在只使用 DataType 而不使用 x:Key 也能隐式应用于所有作用域下对应的实例对象
    • ItemsPanelTemplate:它主要用于 ItemsControl 派生类,用于规定这些集合类的子对象如何在界面上排布,应用于 ItemsPanel 属性
  • ControlTemplate 中通常使用 TemplateBinding 关联控件的属性,而 DataTemplate 则更多是用 Binding 关联数据对象上的属性
    • ControlTemplate + TemplateBinding:关联控件自身的属性(模板→控件)。
    • DataTemplate + Binding:关联数据对象的属性(模板→数据)。

posted on 2025-07-17 00:13  HutatsuiwaKaede  阅读(11)  评论(0)    收藏  举报

导航