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

定义用于运行时绑定的数据源

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

要使绑定系统能够访问数据源,必须将绑定对象的 dataSource 属性定义为数据源对象。例如,如果您有一个数据源对象和一个如下所示的__ UI__(即用户界面,User Interface)让用户能够与您的应用程序进行交互。Unity 目前支持三种 UI 系统。更多信息
See in Glossary
元素:

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# 脚本中创建运行时绑定
定义绑定模式和更新触发器