Version: 1.7
语言 : 中文
在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工具包使用unity.properties要创建的模块属性包用于绑定两个对象之间的数据。它根据可用的C#类型信息生成属性袋。但是,对于某些内置的统一类型,生成的特性袋可能不包含预期的属性。当这些类型缺乏必要的属性时,可能会发生这种情况。例如,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接口提供了一个 view hash code 来指示何时更新链接到源的所有绑定。
  • 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

要通知有关特定属性更改的绑定系统,请实现InotifyBindable Propertychanged界面。实现此接口时,绑定系统仅在沿属性路径检测到更改时仅更新相关的绑定。例如,如果对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数据集中到一个位置,简化数据访问并降低整个应用程序的复杂性。

  • 保持原始数据的清洁度和效率,从而消除了您类型上的额外仪器的需求并确保数据完整性。

了解限制

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

静态类型

您不能将静态类型用作数据源。您必须为系统功能创建一个类型的实例。

方法

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

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

接口

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

内置组件和对象

C#中的属性袋生成过程主要旨在与用户定义类型一起使用。结果,目前对Unity的内置组件和对象的支持有限。这是由于各种因素,包括在本机代码中定义的内置类型的字段,发动机的明确序列化处理或缺少[SerializeField]属性。但是,来自用户定义的组件和脚本对象的字段和属性按预期工作。

此限制将在以后的版本中解决。同时,有两种解决方法:

  • 要从内置基类公开字段或属性,请添加一个private您自己的班级中的财产将其暴露于绑定系统。

  • 使用内置类型的字段或属性,例如Transform,创建一种揭示所需属性的包装类型。

其他资源

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