Files
BABA_YAGA/Packages/app.rive.rive-unity/Runtime/DataBinding/ViewModelInstance.cs

832 lines
31 KiB
C#
Raw Normal View History

2026-05-19 17:39:03 +07:00
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using Rive.Utils;
namespace Rive
{
/// <summary>
/// Represents a runtime instance of a view model with mutable property values.
/// A ViewModelInstance contains the same properties as its source view model but maintains its own state that can change during execution.
/// </summary>
public sealed class ViewModelInstance : ViewModelInstanceProperty, IDisposable
{
private ViewModelInstanceSafeHandle m_safeHandle;
private WeakReference<File> m_riveFile;
// Strong references to subscribed properties keyed by native pointer.
// The VMI owns these so that properties survive even when the user drops their reference.
private readonly Dictionary<IntPtr, ViewModelInstancePrimitiveProperty> m_subscribedProperties = new Dictionary<IntPtr, ViewModelInstancePrimitiveProperty>();
private readonly List<WeakReference<ViewModelInstance>> m_parents = new List<WeakReference<ViewModelInstance>>(); private readonly List<ViewModelInstance> m_children = new List<ViewModelInstance>();
// caching nested view model instances by name
private readonly Dictionary<string, ViewModelInstance> m_viewModelInstances = new Dictionary<string, ViewModelInstance>();
private const char kPathSeparator = '/';
/// <summary>
/// Cache for split paths to avoid repeated string operations
/// </summary>
private static readonly ConcurrentDictionary<string, string[]> s_pathSegmentsCache = new ConcurrentDictionary<string, string[]>();
private bool m_disposed = false;
internal bool IsDisposed => m_disposed;
private string m_viewModelName = null;
internal File RiveFile
{
get
{
if (m_riveFile != null && m_riveFile.TryGetTarget(out var file))
{
return file;
}
return null;
}
}
internal ViewModelInstanceSafeHandle NativeSafeHandle => m_safeHandle;
internal string ViewModelName
{
get
{
if (m_viewModelName == null && !m_disposed)
{
m_viewModelName = Marshal.PtrToStringAnsi(getViewModelNameFromViewModelInstance(NativeSafeHandle));
}
return m_viewModelName;
}
}
private ViewModelInstance(IntPtr instanceValue, File riveFile)
{
m_safeHandle = new ViewModelInstanceSafeHandle(instanceValue);
m_riveFile = new WeakReference<File>(riveFile);
}
~ViewModelInstance()
{
Dispose(false);
}
private static string[] GetPathSegments(string path)
{
// For very frequent calls, caching the split results can improve performance
// If the user tries to get all the properties of a view model instance, this can be called a lot
// We cache the split results to avoid repeated string operations
if (!s_pathSegmentsCache.TryGetValue(path, out var segments))
{
segments = path.Split(kPathSeparator);
s_pathSegmentsCache[path] = segments;
}
return segments;
}
private T GetPropertyFromPathSegments<T>(string[] pathSegments, int index) where T : ViewModelInstanceProperty
{
if (index < pathSegments.Length - 1)
{
// We need to navigate to a nested view model instance so we can propagate callbacks
var nestedInstance = GetInternalViewModelInstance(pathSegments[index]);
if (nestedInstance != null)
{
return nestedInstance.GetPropertyFromPathSegments<T>(pathSegments, index + 1);
}
else
{
return null;
}
}
// We're at the final segment, get the property directly
return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty<T>(this, pathSegments[index]);
}
private ViewModelInstance GetViewModelInstanceFromPathSegments(string[] pathSegments, int index)
{
if (index >= pathSegments.Length)
{
return this;
}
var viewModelInstance = GetInternalViewModelInstance(pathSegments[index]);
if (viewModelInstance != null)
{
if (index == pathSegments.Length - 1)
{
return viewModelInstance;
}
else
{
return viewModelInstance.GetViewModelInstanceFromPathSegments(pathSegments, index + 1);
}
}
return null;
}
private bool HasParent(ViewModelInstance parent)
{
for (int i = 0; i < m_parents.Count; i++)
{
if (m_parents[i].TryGetTarget(out var existingParent) && existingParent == parent)
{
return true;
}
}
return false;
}
private ViewModelInstance GetInternalViewModelInstance(string name)
{
if (m_viewModelInstances.TryGetValue(name, out var instance))
{
return instance;
}
// Otherwise, create and cache it
var ptr = getViewModelInstanceViewModelProperty(NativeSafeHandle, name);
if (ptr != IntPtr.Zero)
{
if (TryGetCachedViewModelInstanceForPointer(ptr, out var cachedInstance))
{
// If we have already created this instance for this pointer, use it
m_viewModelInstances[name] = cachedInstance;
// Let's make sure the parent relationship is set
if (!cachedInstance.HasParent(this))
{
cachedInstance.AddParent(this);
}
return cachedInstance;
}
var newInstance = GetOrCreateFromPointer(ptr, RiveFile, this);
m_viewModelInstances[name] = newInstance;
return newInstance;
}
return null;
}
/// <summary>
/// Gets a nested view model instance property.
/// </summary>
/// <param name="path"> The path to the nested property. If the property is on the current instance, the path is the property name. </param>
/// <returns> The nested view model instance property. </returns>
private ViewModelInstance GetNestedViewModelInstance(string path)
{
// Fast path for simple names (no path separator)
if (!path.Contains(kPathSeparator))
{
return GetInternalViewModelInstance(path);
}
string[] pathSegments = GetPathSegments(path);
return GetViewModelInstanceFromPathSegments(pathSegments, 0);
}
/// <summary>
/// Replaces a nested view model instance property with a new instance.
/// </summary>
/// <param name="name"> The name of the property to replace.</param>
/// <param name="value"> The new view model instance to replace the property with.</param>
/// <returns> True if the view model property was replaced, false otherwise. E.g. If the view model instance provided is for a different view model, the replacement will fail.</returns>
private bool InternalReplaceViewModel(string name, ViewModelInstance value)
{
if (value == null || value.NativeSafeHandle.IsInvalid)
{
return false;
}
bool result = replaceViewModelInstanceViewModelProperty(NativeSafeHandle, name, value.NativeSafeHandle);
if (result)
{
// Clean up the old instance if it exists
if (m_viewModelInstances.TryGetValue(name, out var oldInstance))
{
oldInstance.RemoveParent(this);
// Remove from children list if present
if (m_children.Contains(oldInstance))
{
m_children.Remove(oldInstance);
}
}
m_viewModelInstances[name] = value;
value.AddParent(this);
}
return result;
}
private void ClearCallbacks()
{
foreach (var kvp in m_subscribedProperties)
{
kvp.Value.ClearDelegatesOnly();
PropertyCallbacksHub.Instance.Unregister(kvp.Key);
}
m_subscribedProperties.Clear();
}
internal void AddParent(ViewModelInstance parent)
{
// Check if parent already exists
if (HasParent(parent))
{
return;
}
m_parents.Add(new WeakReference<ViewModelInstance>(parent));
// If we have properties or children with callbacks, notify parent
if (m_subscribedProperties.Count > 0 || m_children.Count > 0)
{
parent.AddChildToCallbacks(this);
}
}
internal void RemoveParent(ViewModelInstance parent)
{
for (int i = m_parents.Count - 1; i >= 0; i--)
{
if (m_parents[i].TryGetTarget(out var existingParent) && existingParent == parent)
{
parent.RemoveChildFromCallbacks(this);
m_parents.RemoveAt(i);
return;
}
}
}
internal void AddChildToCallbacks(ViewModelInstance child)
{
if (!m_children.Contains(child))
{
m_children.Add(child);
// Propagate up to parents
for (int i = 0; i < m_parents.Count; i++)
{
var parent = m_parents[i];
if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null)
{
parentInstance.AddChildToCallbacks(this);
}
}
}
}
internal void RemoveChildFromCallbacks(ViewModelInstance child)
{
m_children.Remove(child);
// If no more children or properties need callbacks, notify parents
if (m_children.Count == 0 && m_subscribedProperties.Count == 0)
{
for (int i = 0; i < m_parents.Count; i++)
{
var parent = m_parents[i];
if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null)
{
parentInstance.RemoveChildFromCallbacks(this);
}
}
}
}
/// <summary>
/// Called by a property when the user subscribes to it for callbacks.
/// This is used to notify parents that they need to subscribe to this property for callbacks as well.
/// </summary>
internal void RegisterPropertyForCallbacks(ViewModelInstancePrimitiveProperty property)
{
if (property == null)
{
return;
}
IntPtr ptr = property.InstancePropertyPtr;
if (ptr == IntPtr.Zero)
{
return;
}
bool wasFirst = m_subscribedProperties.Count == 0;
bool added = !m_subscribedProperties.ContainsKey(ptr);
m_subscribedProperties[ptr] = property;
PropertyCallbacksHub.Instance.Register(property);
if (added && wasFirst)
{
for (int i = 0; i < m_parents.Count; i++)
{
var parent = m_parents[i];
if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null)
{
parentInstance.AddChildToCallbacks(this);
}
}
}
}
/// <summary>
/// Called by a property when it transitions from non-zero to zero subscribers.
/// </summary>
internal void UnregisterPropertyForCallbacks(ViewModelInstancePrimitiveProperty property)
{
if (property == null)
{
return;
}
IntPtr ptr = property.InstancePropertyPtr;
if (ptr != IntPtr.Zero)
{
m_subscribedProperties.Remove(ptr);
PropertyCallbacksHub.Instance.Unregister(ptr);
}
// If no more properties with callbacks and no children with callbacks, notify parents
if (m_subscribedProperties.Count == 0 && m_children.Count == 0)
{
for (int i = 0; i < m_parents.Count; i++)
{
var parent = m_parents[i];
if (parent != null && parent.TryGetTarget(out var parentInstance) && parentInstance != null)
{
parentInstance.RemoveChildFromCallbacks(this);
}
}
}
}
/// <summary>
/// Gets a cached nested view model instance for a given pointer. This is used to avoid creating multiple C# instances of the same underlying native instance.
/// </summary>
internal static bool TryGetCachedViewModelInstanceForPointer(IntPtr ptr, out ViewModelInstance instance)
{
if (ViewModelInstanceProperty.TryGetGloballyCachedVMPropertyForPointer(ptr, out var property))
{
instance = property as ViewModelInstance;
if (instance != null)
{
return true;
}
return false;
}
instance = null;
return false;
}
/// <summary>
/// Removes a cached view model instance for a given pointer.
/// /// </summary>
internal static void RemoveGloballyCachedViewModelInstanceForPointer(IntPtr ptr)
{
ViewModelInstanceProperty.RemoveCachedPropertyForPointer(ptr);
}
/// <summary>
/// Adds a cached view model instance for a given pointer. This is used to avoid creating multiple C# instances of the same underlying native instance.
/// </summary>
/// <param name="ptr"></param>
/// <param name="instance"></param>
internal static void AddCachedViewModelInstanceForPointer(IntPtr ptr, ViewModelInstance instance)
{
ViewModelInstanceProperty.AddGloballyCachedVMPropertyForPointer(ptr, instance);
}
/// <summary>
/// Gets a property of the view model instance.
/// </summary>
/// <typeparam name="T"> The type of the property to get. </typeparam>
/// <param name="path"> The path to the property. If the property is on the current instance, the path is the property name. </param>
/// <remarks> The path can be a nested path, e.g. "nestedInstance/propertyName". </remarks>
/// <returns> The property of the view model instance. </returns>
public T GetProperty<T>(string path) where T : ViewModelInstanceProperty
{
if (string.IsNullOrEmpty(path))
{
DebugLogger.Instance.LogError("Property path cannot be null or empty");
return null;
}
if (m_disposed)
{
throw new ObjectDisposedException(nameof(ViewModelInstance), "Cannot get property from a disposed ViewModelInstance.");
}
// Handle ViewModelInstance type specially since we do a few things differently for non-primitive properties
if (typeof(T) == typeof(ViewModelInstance))
{
return GetNestedViewModelInstance(path) as T;
}
// Fast path for simple property names (no nested path separator)
if (!path.Contains(kPathSeparator))
{
return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty<T>(this, path);
}
string[] pathSegments = GetPathSegments(path);
return GetPropertyFromPathSegments<T>(pathSegments, 0);
}
/// <summary>
/// Detects property value changes
/// Call this after advancing wherever you handle your per-frame logic.
/// </summary>
public void HandleCallbacks()
{
if (m_disposed)
{
return;
}
foreach (var kvp in m_subscribedProperties)
{
var prop = kvp.Value;
if (prop.HasChanged)
{
prop.RaiseChangedEvent();
}
}
foreach (var kvp in m_subscribedProperties)
{
var prop = kvp.Value;
if (prop.HasChanged)
{
prop.ClearChanges();
}
}
// Propagate to children
for (int i = 0; i < m_children.Count; i++)
{
m_children[i]?.HandleCallbacks();
}
}
/// <summary>
/// Replaces a nested view model instance property with a new instance.
/// </summary>
/// <param name="path">The path to the property to replace.</param>
/// <param name="newInstance">The new instance to replace the property with.</param>
public void SetViewModelInstance(string path, ViewModelInstance newInstance)
{
if (m_disposed)
{
throw new ObjectDisposedException(nameof(ViewModelInstance), "Cannot set view model instance on a disposed ViewModelInstance.");
}
if (string.IsNullOrEmpty(path))
{
DebugLogger.Instance.LogError("Property path cannot be null or empty");
return;
}
bool wasReplaced = false;
// Fast path for simple names (no path separator)
if (!path.Contains(kPathSeparator))
{
wasReplaced = InternalReplaceViewModel(path, newInstance);
if (!wasReplaced)
{
DebugLogger.Instance.LogError($"Failed to replace nested view model instance property at path: {path}. The property may not exist or the new instance may be of a different view model type.");
}
return;
}
string[] pathSegments = GetPathSegments(path);
ViewModelInstance currentViewModel = this;
// Navigate to the parent of the target instance (all segments except the last)
for (int i = 0; i < pathSegments.Length - 1; i++)
{
currentViewModel = currentViewModel.GetInternalViewModelInstance(pathSegments[i]);
if (currentViewModel == null)
{
DebugLogger.Instance.LogError($"View model not found at segment '{pathSegments[i]}' in path: {path}");
return;
}
}
// Now currentViewModel is the parent of our target, so lets replace the final segment
wasReplaced = currentViewModel.InternalReplaceViewModel(
pathSegments[pathSegments.Length - 1],
newInstance);
if (!wasReplaced)
{
DebugLogger.Instance.LogError($"Failed to replace nested view model instance property at path: {path}. The property may not exist or the new instance may be of a different view model type.");
}
}
#region Convenience methods
/// <summary>
/// Gets a number property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The number property, or null if the property doesn't exist or is not a number.</returns>
public ViewModelInstanceNumberProperty GetNumberProperty(string path)
{
return GetProperty<ViewModelInstanceNumberProperty>(path);
}
/// <summary>
/// Gets a boolean property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The boolean property, or null if the property doesn't exist or is not a boolean.</returns>
public ViewModelInstanceBooleanProperty GetBooleanProperty(string path)
{
return GetProperty<ViewModelInstanceBooleanProperty>(path);
}
/// <summary>
/// Gets a string property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The string property, or null if the property doesn't exist or is not a string.</returns>
public ViewModelInstanceStringProperty GetStringProperty(string path)
{
return GetProperty<ViewModelInstanceStringProperty>(path);
}
/// <summary>
/// Gets a color property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The color property, or null if the property doesn't exist or is not a color.</returns>
public ViewModelInstanceColorProperty GetColorProperty(string path)
{
return GetProperty<ViewModelInstanceColorProperty>(path);
}
/// <summary>
/// Gets an enum property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The enum property, or null if the property doesn't exist or is not an enum.</returns>
public ViewModelInstanceEnumProperty GetEnumProperty(string path)
{
return GetProperty<ViewModelInstanceEnumProperty>(path);
}
/// <summary>
/// Gets a trigger property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The trigger property, or null if the property doesn't exist or is not a trigger.</returns>
public ViewModelInstanceTriggerProperty GetTriggerProperty(string path)
{
return GetProperty<ViewModelInstanceTriggerProperty>(path);
}
/// <summary>
/// Gets an image property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The image property, or null if the property doesn't exist or is not an image.</returns>
public ViewModelInstanceImageProperty GetImageProperty(string path)
{
return GetProperty<ViewModelInstanceImageProperty>(path);
}
/// <summary>
/// Gets a list property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The list property, or null if the property doesn't exist or is not a list.</returns>
public ViewModelInstanceListProperty GetListProperty(string path)
{
return GetProperty<ViewModelInstanceListProperty>(path);
}
/// <summary>
/// Gets an artboard property of the view model instance.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The artboard property, or null if the property doesn't exist or is not an artboard.</returns>
public ViewModelInstanceArtboardProperty GetArtboardProperty(string path)
{
return GetProperty<ViewModelInstanceArtboardProperty>(path);
}
/// <summary>
/// Gets a nested view model instance property.
/// </summary>
/// <param name="path">The path to the property.</param>
/// <returns>The nested view model instance, or null if the property doesn't exist or is not a view model.</returns>
public ViewModelInstance GetViewModelInstanceProperty(string path)
{
return GetProperty<ViewModelInstance>(path);
}
#endregion
private void Dispose(bool disposing)
{
if (m_disposed)
{
return;
}
if (disposing)
{
// ClearCallbacks() is intentionally only called on the explicit Dispose() path.
// On the finalizer path, the managed objects in m_subscribedProperties may already be finalized,
// and taking the lock inside PropertyCallbacksHub.Unregister() from a finalizer thread risks deadlocks.
// The hub uses weak references, so the hub won't keep the properties alive. It will also clean up any dead properties during the next CaptureChanges() call.
ClearCallbacks();
foreach (var kvp in m_viewModelInstances)
{
var childViewModelInstance = kvp.Value;
childViewModelInstance.RemoveParent(this);
}
m_viewModelInstances.Clear();
for (int i = m_parents.Count - 1; i >= 0; i--)
{
if (m_parents[i].TryGetTarget(out var parentInstance) && parentInstance != null)
{
RemoveParent(parentInstance);
}
}
m_children.Clear();
}
if (m_safeHandle != null && !m_safeHandle.IsInvalid)
{
// Get the IntPtr for cache removal before disposing
IntPtr nativePtr = m_safeHandle.DangerousGetHandle();
m_safeHandle.Dispose();
RemoveGloballyCachedViewModelInstanceForPointer(nativePtr);
}
m_disposed = true;
if (disposing)
{
GC.SuppressFinalize(this);
}
}
public void Dispose()
{
Dispose(true);
}
/// <summary>
/// Helper method to get or create a ViewModelInstance from a native pointer.
/// This method checks if the instance already exists in the cache. If it does, it returns the existing instance so that a single C# instance is always used for the same native instance no matter which method returns it.
/// If it doesn't exist, it creates a new ViewModelInstance and adds it to the cache.
/// </summary>
/// <param name="instancePtr"> The native pointer to the ViewModelInstance.</param>
/// <param name="riveFile"> The Rive file associated with the ViewModelInstance. This is used to resolve the file context for the instance.</param>
/// <param name="parent"> The parent ViewModelInstance, if any. The parent is used to propagate callbacks to this instance. A vm instance can have multiple parents.</param>
/// <returns>The ViewModelInstance associated with the native pointer.</returns>
internal static ViewModelInstance GetOrCreateFromPointer(IntPtr instancePtr, File riveFile, ViewModelInstance parent = null)
{
if (TryGetCachedViewModelInstanceForPointer(instancePtr, out ViewModelInstance existingInstance))
{
// Unity already owns this - balance the extra ref from underlying native methods.
// If we don't do this, the native instance might stay in memory longer than intended.
ViewModelInstanceSafeHandle.unrefViewModelInstance(instancePtr);
if (parent != null)
{
existingInstance.AddParent(parent);
}
return existingInstance;
}
var newInstance = new ViewModelInstance(instancePtr, riveFile);
if (parent != null)
{
newInstance.AddParent(parent);
}
AddCachedViewModelInstanceForPointer(instancePtr, newInstance);
return newInstance;
}
#region Native Calls
[DllImport(NativeLibrary.name)]
private static extern nuint getViewModelInstancePropertyCount(ViewModelInstanceSafeHandle instanceValue);
[DllImport(NativeLibrary.name)]
private static extern IntPtr getViewModelInstancePropertyAtPath(ViewModelInstanceSafeHandle instanceValue, string path);
[DllImport(NativeLibrary.name)]
private static extern IntPtr getViewModelInstanceViewModelProperty(ViewModelInstanceSafeHandle instanceValue, string path);
[DllImport(NativeLibrary.name)]
private static extern IntPtr getViewModelNameFromViewModelInstance(ViewModelInstanceSafeHandle instanceValue);
/// <summary>
/// Replaces a nested view model instance property with a new instance.
/// </summary>
/// <param name="baseInstanceValue">The instance that contains the property to replace.</param>
/// <param name="path">The path to the property to replace.</param>
/// <param name="newInstance">The new instance to replace the property with.</param>
/// <returns>True if the view model property was replaced, false otherwise. E.g. If the view model instance provided is for a different view model, the
/// replacement will fail.</returns>
[DllImport(NativeLibrary.name)]
private static extern bool replaceViewModelInstanceViewModelProperty(
ViewModelInstanceSafeHandle baseInstanceValue,
string path,
ViewModelInstanceSafeHandle newInstance);
#endregion
}
/// <summary>
/// SafeHandle implementation for ViewModelInstance native resources
/// </summary>
internal sealed class ViewModelInstanceSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// The P/Invoke marshaller throws ArgumentNullException if a SafeHandle argument is null.
// We use this reusable invalid handle to represent IntPtr.Zero for optional parameters.
internal static readonly ViewModelInstanceSafeHandle Null = new ViewModelInstanceSafeHandle();
public ViewModelInstanceSafeHandle() : base(true)
{
}
public ViewModelInstanceSafeHandle(IntPtr handle) : base(true)
{
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
unrefViewModelInstance(handle);
return true;
}
return false;
}
[DllImport(NativeLibrary.name)]
internal static extern void unrefViewModelInstance(IntPtr instancePtr);
}
}