{"skill":{"slug":"vvvv-channels","displayName":"Vvvv Channels","summary":"Helps work with vvvv gamma's Channel system from C# — IChannelHub, public channels, [CanBePublished] attributes, hierarchical data propagation, channel subsc...","description":"---\nname: vvvv-channels\ndescription: \"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, managing hierarchical channel state, or implementing reactive/observable data flow. Trigger for any mention of IChannel, IChannelHub, reactive binding, observable state, two-way data binding, or TryGetChannel in a vvvv context.\"\nlicense: CC-BY-SA-4.0\ncompatibility: Designed for coding AI agents assisting with vvvv gamma development\nmetadata:\n  author: Tebjan Halm\n  version: \"1.1\"\n---\n\n# vvvv gamma Channels — C# Integration\n\n## What Are Channels\n\nChannels are **named, typed, observable value containers** — the central reactive data flow mechanism in vvvv gamma. Any code (patches, C# nodes, external bindings) can read and write channels by their string path.\n\nKey properties:\n\n- Each channel has a **path** (string), a **type**, and a **current value**\n- Setting a value fires all subscribers (reactive push)\n- vvvv provides built-in channel bindings for MIDI, OSC, Redis, and UI\n- Channels persist state across sessions\n\n## Public Channels and IChannelHub\n\n**Public channels** are channels registered in the app-wide channel hub — accessible by any code via string path lookup.\n\n### Core API (`VL.Core.Reactive`)\n\n```csharp\nusing VL.Core.Reactive;\n\n// Get the app-wide channel hub (singleton)\nvar hub = IChannelHub.HubForApp;\n\n// Safe lookup — returns null if channel doesn't exist yet\nIChannel<object>? ch = hub.TryGetChannel(\"MyApp.Settings.Volume\");\n\n// Read the current value\nobject? value = ch.Object;\n\n// Write a new value (fires all subscribers)\nch.Object = newValue;\n```\n\n**CRITICAL: NEVER use `hub.TryAddChannel()`** — it creates channels with `null` values, which causes `NullReferenceException` in vvvv's `SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels`. The SubChannel system tries to walk properties of the null value and crashes. Always use `TryGetChannel` (lookup only).\n\n## [CanBePublished] Attribute\n\nFor vvvv to expose .NET type properties as public channels, the type must be decorated with `[CanBePublished(true)]` from `VL.Core.EditorAttributes`.\n\n```csharp\nusing VL.Core.EditorAttributes;\n\n// All public properties become channels when this type is published\n[CanBePublished(true)]\npublic class MyModel\n{\n    // Standard .NET types work directly — float, bool, string, Vector3, etc.\n    public float Volume { get; set; } = 0.5f;\n    public bool Muted { get; set; } = false;\n    public string Label { get; set; } = \"Default\";\n\n    // Hidden from the channel system entirely\n    [CanBePublished(false)]\n    public string InternalId { get; } = Guid.NewGuid().ToString();\n}\n```\n\nRules:\n\n- `[CanBePublished(true)]` on a class/struct → all properties are published as channels\n- `[CanBePublished(false)]` on an individual property → hides it from the channel system\n- **.NET types are NOT published by default** — the attribute is required\n- Available from `VL.Core` version `2025.7.1-0163`+\n\n## Channel Path Conventions\n\nChannels use dot-separated hierarchical paths. Spread elements use bracket notation:\n\n```\nRoot.Page.Zone.Group.Parameter          — leaf parameter\nRoot.Page.Zone                          — hierarchy node (model object)\nRoot.Page.Items[0].PropertyName         — spread element sub-channel\nRoot.Page.Items[2].DeleteInstance        — indexed bang channel\n```\n\nSub-channels are **created automatically** by vvvv's SubChannel system when a type with `[CanBePublished(true)]` is published. You don't create them manually.\n\nUse `const string` path constants to avoid typos:\n\n```csharp\npublic static class ChannelPaths\n{\n    public const string Volume = \"Settings.Audio.Volume\";\n    public const string Brightness = \"App.Scene.Display.Brightness\";\n}\n```\n\n## Retry-Bind Pattern\n\nChannels may not exist when your node starts — vvvv publishes them after model initialization. You must retry each frame until the channel appears:\n\n```csharp\n[ProcessNode]\npublic class MyChannelReader : IDisposable\n{\n    private IChannel<object>? _channel;\n\n    public void Update(out float value)\n    {\n        // Retry until channel exists\n        if (_channel == null)\n        {\n            var hub = IChannelHub.HubForApp;\n            if (hub != null)\n                _channel = hub.TryGetChannel(\"Settings.Audio.Volume\");\n        }\n\n        // Read value (with safe cast)\n        value = _channel?.Object is float f ? f : 0f;\n    }\n\n    public void Dispose() { _channel = null; }\n}\n```\n\nOnce bound, the cached reference is valid for the node's lifetime — no need to re-lookup.\n\n## PublicChannelHelper — Reusable Utility\n\nA helper class that encapsulates the retry-bind + optional subscription pattern. Reusable in any project:\n\n```csharp\npublic class PublicChannelHelper : IDisposable\n{\n    private IChannel<object>? _channel;\n    private IDisposable? _subscription;\n    private readonly Action<object?>? _onNext;\n\n    public PublicChannelHelper(Action<object?>? onNext = null)\n    {\n        _onNext = onNext;\n    }\n\n    public IChannel<object>? Channel => _channel;\n    public bool IsBound => _channel != null;\n\n    public bool TryBind(IChannelHub hub, string path)\n    {\n        if (_channel != null) return true;\n\n        var ch = hub.TryGetChannel(path);\n        if (ch == null) return false;\n\n        _channel = ch;\n        if (_onNext != null)\n            _subscription = ch.Subscribe(new CallbackObserver(_onNext));\n        return true;\n    }\n\n    public void Dispose()\n    {\n        _subscription?.Dispose();\n        _subscription = null;\n        _channel = null;\n    }\n\n    private sealed class CallbackObserver : IObserver<object?>\n    {\n        private readonly Action<object?> _onNext;\n        public CallbackObserver(Action<object?> onNext) => _onNext = onNext;\n        public void OnNext(object? value) => _onNext(value);\n        public void OnError(Exception error) { }\n        public void OnCompleted() { }\n    }\n}\n```\n\nUsage in a ProcessNode:\n\n```csharp\n[ProcessNode]\npublic class VolumeReader : IDisposable\n{\n    private readonly PublicChannelHelper _ch = new();\n\n    public void Update(out float value, out IChannel<object>? channel)\n    {\n        if (!_ch.IsBound)\n        {\n            var hub = IChannelHub.HubForApp;\n            if (hub != null) _ch.TryBind(hub, \"Settings.Audio.Volume\");\n        }\n        value = _ch.Channel?.Object is float f ? f : 0f;\n        channel = _ch.Channel;\n    }\n\n    public void Dispose() => _ch.Dispose();\n}\n```\n\n## Channel Accessor Node Patterns\n\nThree patterns for ProcessNodes that wrap channel access:\n\n### Hierarchy Node (reads a model object)\n\n```csharp\n[ProcessNode]\npublic class Camera : IDisposable\n{\n    private readonly PublicChannelHelper _ch = new();\n\n    public void Update(out CameraSettings? value, out IChannel<object>? channel)\n    {\n        if (!_ch.IsBound)\n        {\n            var hub = IChannelHub.HubForApp;\n            if (hub != null) _ch.TryBind(hub, \"App.Scene.Camera\");\n        }\n        value = _ch.Channel?.Object as CameraSettings;\n        channel = _ch.Channel;\n    }\n\n    public void Dispose() => _ch.Dispose();\n}\n```\n\n### Leaf Parameter Node (reads a typed value)\n\nSame pattern as hierarchy node, but the output type is a standard leaf value like `float`, `bool`, or `string`.\n\n### Indexed Spread Element Node (takes `int index`)\n\n```csharp\n[ProcessNode]\npublic class SceneItemAccessor : IDisposable\n{\n    private PublicChannelHelper _ch = new();\n    private int _boundIndex = -1;\n\n    public void Update(out SceneItem? value, out IChannel<object>? channel, int index = 0)\n    {\n        // Rebind when index changes\n        if (index != _boundIndex)\n        {\n            _ch.Dispose();\n            _ch = new();\n            _boundIndex = index;\n        }\n        if (!_ch.IsBound)\n        {\n            var hub = IChannelHub.HubForApp;\n            if (hub != null) _ch.TryBind(hub, $\"App.Effects[{index}]\");\n        }\n        value = _ch.Channel?.Object as SceneItem;\n        channel = _ch.Channel;\n    }\n\n    public void Dispose() => _ch.Dispose();\n}\n```\n\n## Reactive Subscriptions\n\nSubscribe to channel value changes using the standard `IObservable` pattern:\n\n```csharp\nIChannel<object>? ch = hub.TryGetChannel(\"Settings.Audio.Volume\");\nif (ch != null)\n{\n    IDisposable subscription = ch.Subscribe(new CallbackObserver(value =>\n    {\n        // Called whenever the channel value changes\n        if (value is float f)\n            ApplyVolume(f);\n    }));\n\n    // ALWAYS dispose when done (in node's Dispose method)\n}\n```\n\n## Bang Channels\n\nFor trigger/event properties (delete, insert, move operations), use `System.Reactive.Unit` — NOT float:\n\n```csharp\nusing System.Reactive;\n\n[CanBePublished(true)]\npublic class MyInstance\n{\n    public Unit DeleteInstance { get; set; }     // Bang channel\n    public Unit InsertAfterInstance { get; set; } // Bang channel\n}\n```\n\nThe **event IS the bang** — the value is irrelevant. Subscribe and act on the callback:\n\n```csharp\nvar ch = hub.TryGetChannel(\"Project.Items[0].DeleteInstance\");\nch?.Subscribe(new BangObserver(() =>\n{\n    // Triggered when the bang fires — queue the action\n    _pendingDeletions.Add(0);\n}));\n```\n\n`Unit` channels show as \"Unit\" type in vvvv's node browser, not \"Float\".\n\n## Hierarchical Data Propagation\n\nvvvv's channel system automatically propagates changes through the hierarchy:\n\n- **Write a root record** to its channel → all child channels auto-update\n- **Write a leaf channel** → parent channels auto-update\n- This is built into vvvv's SubChannel system — no manual propagation needed\n\nThis enables efficient bulk operations:\n\n```csharp\n// Save: read the root channel → serialize the entire hierarchy\nvar model = rootChannel.Object as AppModel;\nstring json = JsonSerializer.Serialize(model);\n\n// Load: deserialize → write to root channel → ALL children update\nvar loaded = JsonSerializer.Deserialize<AppModel>(json);\nrootChannel.Object = loaded;  // Every sub-channel updates automatically\n```\n\n## Spread Channels and Sub-Channels\n\nWhen a `Spread<T>` property is published (where `T` has `[CanBePublished(true)]`), vvvv automatically creates sub-channels for each element:\n\n```\nProject.Items           → Spread<ItemModel>\nProject.Items[0]        → ItemModel (auto-created)\nProject.Items[0].Name   → string (auto-created)\nProject.Items[1].Name   → string (auto-created)\n```\n\n**Setting the spread channel propagates to all sub-channels automatically.** No need to update individual sub-channels.\n\nWhen modifying a spread (add/remove/reorder elements):\n\n```csharp\n// Build new spread\nvar newSpread = modifiedList.ToArray().AsSpreadUnsafe();\n\n// Set on spread channel — sub-channels update automatically\nspreadChannel.Object = newSpread;\n```\n\n**Warning**: `Spread.AsSpreadUnsafe(array)` wraps without copying — the array must NOT be mutated after.\n\n**Feedback loop prevention**: When writing to a channel you're also subscribed to, use a suppression flag:\n\n```csharp\n_suppressCallback = true;\n_channel.Object = newValue;\n_suppressCallback = false;\n\n// In the subscription callback:\nvoid OnChanged(object? value)\n{\n    if (_suppressCallback) return;\n    // Process the change...\n}\n```\n\n## Critical Rules\n\n1. **NEVER `TryAddChannel`** — only use `TryGetChannel` (lookup-only)\n2. **Always retry-bind** — channels appear after model initialization, not immediately\n3. **`[CanBePublished(true)]` required** on .NET types for channel publication\n4. **Always dispose subscriptions** — in the node's `Dispose()` method\n5. **`System.Reactive.Unit` for bangs** — not `float` or `bool`\n6. **`Spread.AsSpreadUnsafe`** — array must not be mutated after wrapping\n7. **Suppression flags** — prevent feedback loops when writing to subscribed channels\n\nFor code examples, see [examples.md](examples.md).\n","tags":{"latest":"1.0.1"},"stats":{"comments":0,"downloads":547,"installsAllTime":21,"installsCurrent":1,"stars":0,"versions":2},"createdAt":1772720212978,"updatedAt":1778491734013},"latestVersion":{"version":"1.0.1","createdAt":1772750593903,"changelog":"Improved skill descriptions: better triggering for HLSL/shaders, Spreads/collections, ImportAsIs/dotnet, VL packages, reactive channels. Fixed broken cross-skill reference in troubleshooting.","license":null},"metadata":null,"owner":{"handle":"tebjan","userId":"s17eyt3g0j6vzc4hyx71xsxf79884wr1","displayName":"Tebjan Halm","image":"https://avatars.githubusercontent.com/u/1094716?v=4"},"moderation":null}