版本:Unity 6 (6000.0)
语言:英语
在 C# 脚本中创建运行时绑定
定义绑定模式和更新触发器

为运行时绑定定义数据源

创建绑定对象时,必须定义一个数据源。数据源是包含要绑定属性的对象。可以将任何 C# 对象用作运行时绑定数据源。

要使绑定系统能够访问数据源,必须将绑定对象的 dataSource 属性定义为数据源对象。例如,如果您有一个数据源对象和一个 UI(用户界面) 允许用户与您的应用程序交互。Unity 目前支持三种 UI 系统。 更多信息
术语表 中查看
元素,如下所示

using UnityEngine;
using UnityEngine.UIElements;
using Unity.Properties;

public class DataSource
{
    public Vector3 vector3 { get; set; } 
}

var element = new VisualElement();

然后,可以将 element.dataSource 属性定义为数据源对象,如下所示

element.dataSource = new DataSource();

这使应用于元素的绑定能够访问 DataSource 对象。

要使应用于元素的绑定能够访问 DataSource 对象的 vector3 字段,请添加以下内容

element.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));

要使应用于子元素的绑定能够访问 DataSource 对象的 vector3 字段,请添加以下内容

var child = new VisualElement();
child.dataSourcePath = PropertyPath.FromName(nameof(DataSource.vector3));
element.Add(child)

属性包

UI 工具包使用 Unity.Properties 模块为两个对象之间的绑定数据创建 属性包。它根据可用的 C# 类型信息生成属性包。但是,对于某些内置的 Unity 类型,生成的属性包可能不包含预期的属性。当这些类型缺乏必要的属性时,就会发生这种情况。例如,Rect 类型具有公共属性和私有字段,这些字段没有用 [SerializeField] 属性标记,或者您在本地端定义了这些字段,这些字段在运行时无法确定。

注意:当将值类型用作数据源时,由于 VisualElement.dataSource 被定义为对象属性,因此会产生装箱成本。这意味着在将值类型分配给 dataSource 属性之前,必须对其进行装箱。装箱操作会带来内存分配和复制的开销,从而导致性能成本。对于小型数据集或偶尔使用,这种性能影响可能并不明显。但是,在性能关键型场景或处理大量数据时,装箱成本会成为问题。

要为运行时绑定以及创作或序列化目的定义数据源,请使用如下所示的常见模式

using UnityEngine;
using Unity.Properties;

public class MyBehaviour : MonoBehaviour
{
    // Serializations go through the field. 
    [SerializeField, DontCreateProperty] 
    private int m_Value;
    
    // Bindings go through the property rather than the field. 
    // This allows you to do validation, notify changes, and more.
    [CreateProperty] 
    public int value
    {
        get => m_Value;
        set => m_Value = value;
    }
    
    // This is a similar example, but for an auto-property.
    [field: SerializeField, DontCreateProperty]
    [CreateProperty]
    public float floatValue { get; set; }
}

注意:这些可绑定属性本质上具有多态性。

集成版本控制和更改跟踪

为了提高性能,可以将版本控制和更改跟踪集成到绑定数据源中。默认情况下,绑定系统会持续轮询数据源并在每次修改时更新 UI,而不知道自上次更新以来是否发生了任何实际变化。虽然这种方法对于简单的项目很方便,但当处理大量绑定时,它无法有效地扩展。

源的版本控制和更改跟踪是可选功能,需要有意识地激活。默认情况下,活动绑定对象会每帧更新一次,这可能是一个资源密集型过程。为了最大限度地减少处理开销,您可以实现两个接口来指示绑定系统何时更新与源关联的绑定

  • IDataSourceViewHashProvider 接口提供了一个视图哈希码,用于指示何时更新与源链接的所有绑定。
  • INotifyBindablePropertyChanged 接口支持每个属性的更改通知,仅触发与修改属性相关的单个绑定的更新。

您可以单独或一起实现这些接口以获得更好的控制。

注意:目前,实现任一接口的类型会在程序集标记为 [assembly: Unity.Properties.GeneratePropertyBagsForAssembly] 时自动选择加入代码生成。但是,此行为可能会发生变化。

实现 IDataSourceViewHashProvider

要为特定源提供视图哈希码,请实现 IDataSourceViewHashProvider 接口。此接口使绑定系统能够在源自上次更新以来没有发生更改的情况下跳过更新某些绑定对象。

以下示例创建了一个立即报告更改的数据源

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    public int intValue;
    public float floatValue;

    // Determines if the data source has changed. If the hash code is different, then the data source
    // has changed and the bindings are updated.
    public long  GetViewHashCode()
    {
        return HashCode.Combine(intValue, floatValue);
    }
}

IDataSourceViewHashProvider 接口还缓冲更改。当数据频繁更改但 UI 不需要立即反映每次更改时,这种缓冲功能特别有用。

要缓冲更改,请实现 IDataSourceViewHashProvider 接口,并在要通知绑定系统数据源已更改时调用 CommitChanges 方法。

默认情况下,如果数据源的版本保持不变,绑定系统不会更新绑定对象。但是,即使版本没有发生变化,如果您调用其 MarkDirty 方法或将 updateTrigger 设置为 BindingUpdateTrigger.EveryFrame,绑定对象仍可能被更新。当使用 IDataSourceViewHashProvider 缓冲更改时,请避免对源进行任何结构性更改,例如添加或删除列表中的项目,或更改子字段或子属性的类型。

以下示例创建了一个缓冲更改的数据源

using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider
{
    private long m_Version;

    public int intValue;
    
    public void CommitChanges()
    {
        ++m_Version;
    }
    
    // Required by IDataSourceViewHashProvider
    public long  GetViewHashCode()
    {
        return m_Version;
    }
}

实现 INotifyBindablePropertyChanged

要将特定属性更改通知绑定系统,请实现 INotifyBindablePropertyChanged 接口。当实现此接口时,绑定系统仅在沿属性路径检测到更改时更新相关绑定。例如,如果 MyAwesomeObject 属性发出更改信号,绑定系统将更新与具有 MyAwesomeObject 前缀的数据源路径相关的所有绑定。与源关联的其他绑定对象不受影响。

这种方法支持对 UI 进行高效的更新,因为绑定系统执行最少的操作。

以下示例创建了一个按属性通知更改的数据源

using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : INotifyBindablePropertyChanged
{
    private int m_Value;
    
    // Required by INotifyBindablePropertyChanged
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;

    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;

            m_Value = value;
            Notify();
        }
    }

    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

注意:当您实现 INotifyBindablePropertyChanged 接口时,绑定系统在收到更改通知时不会执行检查。如果未报告更改,则表示绑定系统不会更新与该属性相关的绑定。因此,请确保仅在必要时报告更改。

实现 IDataSourceViewHashProviderINotifyBindablePropertyChanged

要实现最佳的绑定性能,请实现 IDataSourceViewHashProviderINotifyBindablePropertyChanged 接口。绑定系统跟踪更改的属性,直到视图的哈希码更改。此时,它会有效地仅更新与已更改属性相关的受影响绑定。

这需要额外的样板代码,但提供了最大的灵活性和性能优势。

以下示例创建了一个实现这两个接口的数据源。数据源在发生更改时通知绑定系统。但是,它不会立即更新绑定,而是在调用 Publish() 方法之前保存更新。这种方法在处理高度不稳定的数据时特别有用,因为每帧更新 UI 会产生性能成本。

using System;
using System.Runtime.CompilerServices;
using Unity.Properties;
using UnityEngine.UIElements;

public class DataSource : IDataSourceViewHashProvider, INotifyBindablePropertyChanged
{
    private long m_ViewVersion;
    private int m_Value;
    private int m_OtherValue;
    public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
    [CreateProperty]
    public int value
    {
        get => m_Value;
        set
        {
            if (m_Value == value)
                return;
            m_Value = value;
            Notify();
        }
    }
    [CreateProperty]
    public int otherValue
    {
        get => m_OtherValue;
        set
        {
            if (m_OtherValue == value)
                return;
            m_OtherValue = value;
            Notify();
        }
    }
    public void Publish()
    {
        ++m_ViewVersion;
    }
    public long GetViewHashCode()
    {
        return m_ViewVersion;
    }
    void Notify([CallerMemberName] string property = "")
    {
        propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
    }
}

最佳实践

遵循以下提示和最佳实践以优化性能

  • 对可绑定属性使用 C# 属性:定义可绑定属性时,使用 C# 属性而不是字段。这提供了灵活地包含验证、通知或任何自定义行为,从而产生更健壮和更易于维护的代码。

  • 避免在 C# 属性中进行大量计算:如果属性需要大量的处理,请仅在必要时执行计算,并为后续绑定使用缓存值。

  • 避免不必要的通知:在值没有实际变化时,请注意不要通知更改。如果值保持不变,则无需发送通知。

  • 实现版本控制和更改跟踪:在数据源中使用版本控制。为了获得最佳性能,请同时使用版本控制和更改跟踪。

  • 将数据源用作数据和 UI 之间的缓冲区:只要可能,请将数据源实现为数据和 UI 之间的中间体,而不是直接使用数据。这种方法提供了以下几个优点

  • 提供对数据流的更好控制,并便于跟踪来自 UI 的更改。它允许您管理数据更新的时间和方式。

  • 将所有 UI 数据集中在一个位置,简化数据访问并降低整个应用程序的复杂性。

  • 维护原始数据的整洁和效率,无需对您的类型进行额外的检测,并确保数据完整性。

了解局限性

以下部分概述了运行时绑定数据源的已知局限性。

静态类型

无法将静态类型用作数据源。必须创建类型的实例才能使系统正常工作。

方法

为类型生成的属性包仅考虑字段和属性。因此,您无法绑定到方法或内置事件。

但是,可以绑定到委托,例如 ActionFunc 委托类型。要绑定到委托字段或属性,请使用 = 运算符而不是 +=-=。如果您需要添加或删除委托而不是分配它们,则可能需要实现自定义绑定类型。

接口

静态类型 部分所述,必须创建数据源的对象实例。虽然绑定系统可以与接口一起使用,但实现具有用 [CreateProperty] 标记的属性的接口的类型不会自动为其生成可绑定属性。对于每种类型,必须分别标记其字段和属性,使其可绑定。此限制将在将来的版本中解决。

内置组件和对象

C# 中的属性包生成过程主要设计用于与用户定义的类型一起使用。因此,目前对 Unity 的内置组件和对象的支持有限。这是由于各种因素造成的,包括内置类型的字段在本地代码中定义、引擎对显式序列化处理或缺少 [SerializeField] 属性。但是,来自用户定义组件和可脚本化对象的字段和属性按预期工作。

此限制将在将来的版本中解决。在此期间,可以使用两种解决方法

  • 要公开来自内置基类的字段或属性,请在您自己的类中添加一个 private 属性以将其公开给绑定系统。
  • 要使用来自内置类型(例如 Transform)的字段或属性,请创建一个包装类型,该类型公开所需的属性。

其他资源

在 C# 脚本中创建运行时绑定
定义绑定模式和更新触发器