using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
using Rive.Utils;
namespace Rive
{
///
/// 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.
///
public sealed class ViewModelInstance : ViewModelInstanceProperty, IDisposable
{
private ViewModelInstanceSafeHandle m_safeHandle;
private WeakReference 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 m_subscribedProperties = new Dictionary();
private readonly List> m_parents = new List>(); private readonly List m_children = new List();
// caching nested view model instances by name
private readonly Dictionary m_viewModelInstances = new Dictionary();
private const char kPathSeparator = '/';
///
/// Cache for split paths to avoid repeated string operations
///
private static readonly ConcurrentDictionary s_pathSegmentsCache = new ConcurrentDictionary();
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(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(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(pathSegments, index + 1);
}
else
{
return null;
}
}
// We're at the final segment, get the property directly
return ViewModelInstancePropertyHandlersFactory.GetPrimitiveProperty(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;
}
///
/// Gets a nested view model instance property.
///
/// The path to the nested property. If the property is on the current instance, the path is the property name.
/// The nested view model instance property.
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);
}
///
/// Replaces a nested view model instance property with a new instance.
///
/// The name of the property to replace.
/// The new view model instance to replace the property with.
/// 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.
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(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);
}
}
}
}
///
/// 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.
///
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);
}
}
}
}
///
/// Called by a property when it transitions from non-zero to zero subscribers.
///
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);
}
}
}
}
///
/// 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.
///
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;
}
///
/// Removes a cached view model instance for a given pointer.
/// ///
internal static void RemoveGloballyCachedViewModelInstanceForPointer(IntPtr ptr)
{
ViewModelInstanceProperty.RemoveCachedPropertyForPointer(ptr);
}
///
/// 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.
///
///
///
internal static void AddCachedViewModelInstanceForPointer(IntPtr ptr, ViewModelInstance instance)
{
ViewModelInstanceProperty.AddGloballyCachedVMPropertyForPointer(ptr, instance);
}
///
/// Gets a property of the view model instance.
///
/// The type of the property to get.
/// The path to the property. If the property is on the current instance, the path is the property name.
/// The path can be a nested path, e.g. "nestedInstance/propertyName".
/// The property of the view model instance.
public T GetProperty(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(this, path);
}
string[] pathSegments = GetPathSegments(path);
return GetPropertyFromPathSegments(pathSegments, 0);
}
///
/// Detects property value changes
/// Call this after advancing wherever you handle your per-frame logic.
///
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();
}
}
///
/// Replaces a nested view model instance property with a new instance.
///
/// The path to the property to replace.
/// The new instance to replace the property with.
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
///
/// Gets a number property of the view model instance.
///
/// The path to the property.
/// The number property, or null if the property doesn't exist or is not a number.
public ViewModelInstanceNumberProperty GetNumberProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a boolean property of the view model instance.
///
/// The path to the property.
/// The boolean property, or null if the property doesn't exist or is not a boolean.
public ViewModelInstanceBooleanProperty GetBooleanProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a string property of the view model instance.
///
/// The path to the property.
/// The string property, or null if the property doesn't exist or is not a string.
public ViewModelInstanceStringProperty GetStringProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a color property of the view model instance.
///
/// The path to the property.
/// The color property, or null if the property doesn't exist or is not a color.
public ViewModelInstanceColorProperty GetColorProperty(string path)
{
return GetProperty(path);
}
///
/// Gets an enum property of the view model instance.
///
/// The path to the property.
/// The enum property, or null if the property doesn't exist or is not an enum.
public ViewModelInstanceEnumProperty GetEnumProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a trigger property of the view model instance.
///
/// The path to the property.
/// The trigger property, or null if the property doesn't exist or is not a trigger.
public ViewModelInstanceTriggerProperty GetTriggerProperty(string path)
{
return GetProperty(path);
}
///
/// Gets an image property of the view model instance.
///
/// The path to the property.
/// The image property, or null if the property doesn't exist or is not an image.
public ViewModelInstanceImageProperty GetImageProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a list property of the view model instance.
///
/// The path to the property.
/// The list property, or null if the property doesn't exist or is not a list.
public ViewModelInstanceListProperty GetListProperty(string path)
{
return GetProperty(path);
}
///
/// Gets an artboard property of the view model instance.
///
/// The path to the property.
/// The artboard property, or null if the property doesn't exist or is not an artboard.
public ViewModelInstanceArtboardProperty GetArtboardProperty(string path)
{
return GetProperty(path);
}
///
/// Gets a nested view model instance property.
///
/// The path to the property.
/// The nested view model instance, or null if the property doesn't exist or is not a view model.
public ViewModelInstance GetViewModelInstanceProperty(string path)
{
return GetProperty(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);
}
///
/// 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.
///
/// The native pointer to the ViewModelInstance.
/// The Rive file associated with the ViewModelInstance. This is used to resolve the file context for the instance.
/// The parent ViewModelInstance, if any. The parent is used to propagate callbacks to this instance. A vm instance can have multiple parents.
/// The ViewModelInstance associated with the native pointer.
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);
///
/// Replaces a nested view model instance property with a new instance.
///
/// The instance that contains the property to replace.
/// The path to the property to replace.
/// The new instance to replace the property with.
/// 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.
[DllImport(NativeLibrary.name)]
private static extern bool replaceViewModelInstanceViewModelProperty(
ViewModelInstanceSafeHandle baseInstanceValue,
string path,
ViewModelInstanceSafeHandle newInstance);
#endregion
}
///
/// SafeHandle implementation for ViewModelInstance native resources
///
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);
}
}