vvvv-channels
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesevvvv gamma Channels — C# Integration
vvvv gamma 通道系统 — C# 集成
What Are Channels
什么是通道
Channels 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.
Key properties:
- Each channel has a path (string), a type, and a current value
- Setting a value fires all subscribers (reactive push)
- vvvv provides built-in channel bindings for MIDI, OSC, Redis, and UI
- Channels persist state across sessions
通道是命名化、类型化、可观察的值容器——是vvvv gamma中核心的响应式数据流机制。任何代码(补丁、C#节点、外部绑定)都可以通过字符串路径读写通道。
关键特性:
- 每个通道都有路径(字符串)、类型和当前值
- 设置值时会触发所有订阅者(响应式推送)
- vvvv 为MIDI、OSC、Redis和UI提供内置通道绑定
- 通道会跨会话保留状态
Public Channels and IChannelHub
公共通道与IChannelHub
Public channels are channels registered in the app-wide channel hub — accessible by any code via string path lookup.
公共通道是注册在应用级通道中心的通道——任何代码都可以通过字符串路径查找访问。
Core API (VL.Core.Reactive
)
VL.Core.Reactive核心API (VL.Core.Reactive
)
VL.Core.Reactivecsharp
using 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;CRITICAL: NEVER use — it creates channels with values, which causes in vvvv's . The SubChannel system tries to walk properties of the null value and crashes. Always use (lookup only).
hub.TryAddChannel()nullNullReferenceExceptionSubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannelsTryGetChannelcsharp
using 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;重要提示:绝不要使用——它会创建值为的通道,这会导致vvvv的抛出。子通道系统会尝试遍历null值的属性并崩溃。请始终使用(仅查找)。
hub.TryAddChannel()nullSubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannelsNullReferenceExceptionTryGetChannel[CanBePublished] Attribute
[CanBePublished]特性
For vvvv to expose .NET type properties as public channels, the type must be decorated with from .
[CanBePublished(true)]VL.Core.EditorAttributescsharp
using 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();
}Rules:
- on a class/struct → all properties are published as channels
[CanBePublished(true)] - on an individual property → hides it from the channel system
[CanBePublished(false)] - .NET types are NOT published by default — the attribute is required
- Available from version
VL.Core+2025.7.1-0163
要让vvvv将.NET类型的属性暴露为公共通道,该类型必须使用中的特性装饰。
VL.Core.EditorAttributes[CanBePublished(true)]csharp
using 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)] - .NET类型默认不会被发布——必须添加该特性
- 从版本
VL.Core+开始可用2025.7.1-0163
Channel Path Conventions
通道路径约定
Channels use dot-separated hierarchical paths. Spread elements use bracket notation:
Root.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 channelSub-channels are created automatically by vvvv's SubChannel system when a type with is published. You don't create them manually.
[CanBePublished(true)]Use path constants to avoid typos:
const stringcsharp
public static class ChannelPaths
{
public const string Volume = "Settings.Audio.Volume";
public const string Brightness = "App.Scene.Display.Brightness";
}通道使用点分隔的层级路径。Spread元素使用括号表示法:
Root.Page.Zone.Group.Parameter — 叶子参数
Root.Page.Zone — 层级节点(模型对象)
Root.Page.Items[0].PropertyName — Spread元素子通道
Root.Page.Items[2].DeleteInstance — 索引化Bang通道当带有的类型被发布时,子通道会由vvvv的子通道系统自动创建。无需手动创建。
[CanBePublished(true)]使用路径常量避免拼写错误:
const stringcsharp
public static class ChannelPaths
{
public const string Volume = "Settings.Audio.Volume";
public const string Brightness = "App.Scene.Display.Brightness";
}Retry-Bind Pattern
重试绑定模式
Channels may not exist when your node starts — vvvv publishes them after model initialization. You must retry each frame until the channel appears:
csharp
[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; }
}Once bound, the cached reference is valid for the node's lifetime — no need to re-lookup.
当你的节点启动时,通道可能还不存在——vvvv会在模型初始化后发布它们。你必须每帧重试直到通道出现:
csharp
[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; }
}一旦绑定,缓存的引用在节点的生命周期内都是有效的——无需重新查找。
PublicChannelHelper — Reusable Utility
PublicChannelHelper — 可复用工具类
A helper class that encapsulates the retry-bind + optional subscription pattern. Reusable in any project:
csharp
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() { }
}
}Usage in a ProcessNode:
csharp
[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();
}一个封装了重试绑定+可选订阅模式的工具类。可在任何项目中复用:
csharp
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中使用:
csharp
[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();
}Channel Accessor Node Patterns
通道访问器节点模式
Three patterns for ProcessNodes that wrap channel access:
封装通道访问的ProcessNode有三种模式:
Hierarchy Node (reads a model object)
层级节点(读取模型对象)
csharp
[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();
}csharp
[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();
}Leaf Parameter Node (reads a typed value)
叶子参数节点(读取类型化值)
Same pattern as hierarchy node, but the output type is a standard leaf value like , , or .
floatboolstring与层级节点模式相同,但输出类型是标准叶子值,如、或。
floatboolstringIndexed Spread Element Node (takes int index
)
int index索引化Spread元素节点(接收int index
)
int indexcsharp
[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();
}csharp
[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();
}Reactive Subscriptions
响应式订阅
Subscribe to channel value changes using the standard pattern:
IObservablecsharp
IChannel<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)
}使用标准模式订阅通道值变更:
IObservablecsharp
IChannel<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)
}Bang Channels
Bang通道
For trigger/event properties (delete, insert, move operations), use — NOT float:
System.Reactive.Unitcsharp
using System.Reactive;
[CanBePublished(true)]
public class MyInstance
{
public Unit DeleteInstance { get; set; } // Bang channel
public Unit InsertAfterInstance { get; set; } // Bang channel
}The event IS the bang — the value is irrelevant. Subscribe and act on the callback:
csharp
var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
// Triggered when the bang fires — queue the action
_pendingDeletions.Add(0);
}));Unit对于触发/事件属性(删除、插入、移动操作),请使用——不要使用float:
System.Reactive.Unitcsharp
using System.Reactive;
[CanBePublished(true)]
public class MyInstance
{
public Unit DeleteInstance { get; set; } // Bang channel
public Unit InsertAfterInstance { get; set; } // Bang channel
}事件本身就是Bang——值无关紧要。订阅并在回调中执行操作:
csharp
var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
// Triggered when the bang fires — queue the action
_pendingDeletions.Add(0);
}));UnitHierarchical Data Propagation
层级数据传播
vvvv's channel system automatically propagates changes through the hierarchy:
- Write a root record to its channel → all child channels auto-update
- Write a leaf channel → parent channels auto-update
- This is built into vvvv's SubChannel system — no manual propagation needed
This enables efficient bulk operations:
csharp
// 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 automaticallyvvvv的通道系统会自动在层级中传播变更:
- 向根记录通道写入值 → 所有子通道自动更新
- 向叶子通道写入值 → 父通道自动更新
- 这是vvvv子通道系统内置的功能——无需手动传播
这支持高效的批量操作:
csharp
// 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 Channels and Sub-Channels
Spread通道与子通道
When a property is published (where has ), vvvv automatically creates sub-channels for each element:
Spread<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)Setting the spread channel propagates to all sub-channels automatically. No need to update individual sub-channels.
When modifying a spread (add/remove/reorder elements):
csharp
// Build new spread
var newSpread = modifiedList.ToArray().AsSpreadUnsafe();
// Set on spread channel — sub-channels update automatically
spreadChannel.Object = newSpread;Warning: wraps without copying — the array must NOT be mutated after.
Spread.AsSpreadUnsafe(array)Feedback loop prevention: When writing to a channel you're also subscribed to, use a suppression flag:
csharp
_suppressCallback = true;
_channel.Object = newValue;
_suppressCallback = false;
// In the subscription callback:
void OnChanged(object? value)
{
if (_suppressCallback) return;
// Process the change...
}当属性被发布时(其中带有),vvvv会自动为每个元素创建子通道:
Spread<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)设置Spread通道会自动传播到所有子通道。无需更新单个子通道。
修改Spread时(添加/删除/重新排序元素):
csharp
// Build new spread
var newSpread = modifiedList.ToArray().AsSpreadUnsafe();
// Set on spread channel — sub-channels update automatically
spreadChannel.Object = newSpread;警告:会直接包装数组而不复制——包装后绝不要修改该数组。
Spread.AsSpreadUnsafe(array)反馈循环预防:当向你已订阅的通道写入值时,请使用抑制标志:
csharp
_suppressCallback = true;
_channel.Object = newValue;
_suppressCallback = false;
// In the subscription callback:
void OnChanged(object? value)
{
if (_suppressCallback) return;
// Process the change...
}Critical Rules
重要规则
- NEVER — only use
TryAddChannel(lookup-only)TryGetChannel - Always retry-bind — channels appear after model initialization, not immediately
- required on .NET types for channel publication
[CanBePublished(true)] - Always dispose subscriptions — in the node's method
Dispose() - for bangs — not
System.Reactive.Unitorfloatbool - — array must not be mutated after wrapping
Spread.AsSpreadUnsafe - Suppression flags — prevent feedback loops when writing to subscribed channels
For code examples, see examples.md.
- 绝不要使用——仅使用
TryAddChannel(仅查找)TryGetChannel - 始终重试绑定——通道在模型初始化后才会出现,不会立即可用
- 必须添加——.NET类型需要该特性才能被发布为通道
[CanBePublished(true)] - 始终释放订阅——在节点的方法中执行
Dispose() - Bang通道使用——不要使用
System.Reactive.Unit或floatbool - 使用时——包装后的数组绝不能被修改
Spread.AsSpreadUnsafe - 使用抑制标志——向已订阅的通道写入值时防止反馈循环
代码示例请参考examples.md。