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

定义运行时绑定的数据源

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

要使绑定系统能够访问数据源,您必须定义dataSource属性。例如,如果您有一个数据源对象和一个 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对象。

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

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

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

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

财产袋

UI Toolkit 使用Unity.Properties模块创建属性包,用于在两个对象之间绑定数据。它根据可用的 C# 类型信息生成属性包。但是,对于某些内置 Unity 类型,生成的属性包可能不包含预期的属性。当这些类型缺少必要的属性时,可能会发生这种情况。例如,Recttype 具有未归因于[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方法或将updateTriggerBindingUpdateTrigger.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委托类型。要绑定到委托字段或属性,请使用运算符而不是 or 。如果需要添加或删除委托而不是分配委托,则可能需要实现自定义绑定类型。=+=-=

接口

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

内置组件和对象

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

此限制将在将来的版本中得到解决。同时,有两种解决方法可用:

  • 要从内置基类公开字段或属性,请添加一个private属性,将其公开给绑定系统。
  • 使用内置类型(例如Transform,创建一个公开所需属性的包装器类型。

其他资源

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