Loading...
Loading...
Helps work with vvvv gamma's Channel system from C# — IChannelHub, public channels, [CanBePublished] attributes, hierarchical data propagation, channel subscriptions, bang channels, and spread sub-channels. Use when reading or writing public channels from C# nodes, publishing .NET types as channels, working with IChannelHub, subscribing to channel changes, or managing hierarchical channel state.
npx skill4agent add tebjan/vvvv-skills vvvv-channelsVL.Core.Reactiveusing VL.Core.Reactive;
// Get the app-wide channel hub (singleton)
var hub = IChannelHub.HubForApp;
// Safe lookup — returns null if channel doesn't exist yet
IChannel<object>? ch = hub.TryGetChannel("MyApp.Settings.Volume");
// Read the current value
object? value = ch.Object;
// Write a new value (fires all subscribers)
ch.Object = newValue;hub.TryAddChannel()nullNullReferenceExceptionSubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannelsTryGetChannel[CanBePublished(true)]VL.Core.EditorAttributesusing VL.Core.EditorAttributes;
// All public properties become channels when this type is published
[CanBePublished(true)]
public class MyModel
{
// Standard .NET types work directly — float, bool, string, Vector3, etc.
public float Volume { get; set; } = 0.5f;
public bool Muted { get; set; } = false;
public string Label { get; set; } = "Default";
// Hidden from the channel system entirely
[CanBePublished(false)]
public string InternalId { get; } = Guid.NewGuid().ToString();
}[CanBePublished(true)][CanBePublished(false)]VL.Core2025.7.1-0163Root.Page.Zone.Group.Parameter — leaf parameter
Root.Page.Zone — hierarchy node (model object)
Root.Page.Items[0].PropertyName — spread element sub-channel
Root.Page.Items[2].DeleteInstance — indexed bang channel[CanBePublished(true)]const stringpublic static class ChannelPaths
{
public const string Volume = "Settings.Audio.Volume";
public const string Brightness = "App.Scene.Display.Brightness";
}[ProcessNode]
public class MyChannelReader : IDisposable
{
private IChannel<object>? _channel;
public void Update(out float value)
{
// Retry until channel exists
if (_channel == null)
{
var hub = IChannelHub.HubForApp;
if (hub != null)
_channel = hub.TryGetChannel("Settings.Audio.Volume");
}
// Read value (with safe cast)
value = _channel?.Object is float f ? f : 0f;
}
public void Dispose() { _channel = null; }
}public class PublicChannelHelper : IDisposable
{
private IChannel<object>? _channel;
private IDisposable? _subscription;
private readonly Action<object?>? _onNext;
public PublicChannelHelper(Action<object?>? onNext = null)
{
_onNext = onNext;
}
public IChannel<object>? Channel => _channel;
public bool IsBound => _channel != null;
public bool TryBind(IChannelHub hub, string path)
{
if (_channel != null) return true;
var ch = hub.TryGetChannel(path);
if (ch == null) return false;
_channel = ch;
if (_onNext != null)
_subscription = ch.Subscribe(new CallbackObserver(_onNext));
return true;
}
public void Dispose()
{
_subscription?.Dispose();
_subscription = null;
_channel = null;
}
private sealed class CallbackObserver : IObserver<object?>
{
private readonly Action<object?> _onNext;
public CallbackObserver(Action<object?> onNext) => _onNext = onNext;
public void OnNext(object? value) => _onNext(value);
public void OnError(Exception error) { }
public void OnCompleted() { }
}
}[ProcessNode]
public class VolumeReader : IDisposable
{
private readonly PublicChannelHelper _ch = new();
public void Update(out float value, out IChannel<object>? channel)
{
if (!_ch.IsBound)
{
var hub = IChannelHub.HubForApp;
if (hub != null) _ch.TryBind(hub, "Settings.Audio.Volume");
}
value = _ch.Channel?.Object is float f ? f : 0f;
channel = _ch.Channel;
}
public void Dispose() => _ch.Dispose();
}[ProcessNode]
public class Camera : IDisposable
{
private readonly PublicChannelHelper _ch = new();
public void Update(out CameraSettings? value, out IChannel<object>? channel)
{
if (!_ch.IsBound)
{
var hub = IChannelHub.HubForApp;
if (hub != null) _ch.TryBind(hub, "App.Scene.Camera");
}
value = _ch.Channel?.Object as CameraSettings;
channel = _ch.Channel;
}
public void Dispose() => _ch.Dispose();
}floatboolstringint index[ProcessNode]
public class SceneItemAccessor : IDisposable
{
private PublicChannelHelper _ch = new();
private int _boundIndex = -1;
public void Update(out SceneItem? value, out IChannel<object>? channel, int index = 0)
{
// Rebind when index changes
if (index != _boundIndex)
{
_ch.Dispose();
_ch = new();
_boundIndex = index;
}
if (!_ch.IsBound)
{
var hub = IChannelHub.HubForApp;
if (hub != null) _ch.TryBind(hub, $"App.Effects[{index}]");
}
value = _ch.Channel?.Object as SceneItem;
channel = _ch.Channel;
}
public void Dispose() => _ch.Dispose();
}IObservableIChannel<object>? ch = hub.TryGetChannel("Settings.Audio.Volume");
if (ch != null)
{
IDisposable subscription = ch.Subscribe(new CallbackObserver(value =>
{
// Called whenever the channel value changes
if (value is float f)
ApplyVolume(f);
}));
// ALWAYS dispose when done (in node's Dispose method)
}System.Reactive.Unitusing System.Reactive;
[CanBePublished(true)]
public class MyInstance
{
public Unit DeleteInstance { get; set; } // Bang channel
public Unit InsertAfterInstance { get; set; } // Bang channel
}var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
// Triggered when the bang fires — queue the action
_pendingDeletions.Add(0);
}));Unit// Save: read the root channel → serialize the entire hierarchy
var model = rootChannel.Object as AppModel;
string json = JsonSerializer.Serialize(model);
// Load: deserialize → write to root channel → ALL children update
var loaded = JsonSerializer.Deserialize<AppModel>(json);
rootChannel.Object = loaded; // Every sub-channel updates automaticallySpread<T>T[CanBePublished(true)]Project.Items → Spread<ItemModel>
Project.Items[0] → ItemModel (auto-created)
Project.Items[0].Name → string (auto-created)
Project.Items[1].Name → string (auto-created)// Build new spread
var newSpread = modifiedList.ToArray().AsSpreadUnsafe();
// Set on spread channel — sub-channels update automatically
spreadChannel.Object = newSpread;Spread.AsSpreadUnsafe(array)_suppressCallback = true;
_channel.Object = newValue;
_suppressCallback = false;
// In the subscription callback:
void OnChanged(object? value)
{
if (_suppressCallback) return;
// Process the change...
}TryAddChannelTryGetChannel[CanBePublished(true)]Dispose()System.Reactive.UnitfloatboolSpread.AsSpreadUnsafe