vvvv-channels

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

vvvv 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
)

核心API (
VL.Core.Reactive
)

csharp
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
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).
csharp
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;
重要提示:绝不要使用
hub.TryAddChannel()
——它会创建值为
null
的通道,这会导致vvvv的
SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels
抛出
NullReferenceException
。子通道系统会尝试遍历null值的属性并崩溃。请始终使用
TryGetChannel
(仅查找)。

[CanBePublished] Attribute

[CanBePublished]特性

For vvvv to expose .NET type properties as public channels, the type must be decorated with
[CanBePublished(true)]
from
VL.Core.EditorAttributes
.
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();
}
Rules:
  • [CanBePublished(true)]
    on a class/struct → all properties are published as channels
  • [CanBePublished(false)]
    on an individual property → hides it from the channel system
  • .NET types are NOT published by default — the attribute is required
  • Available from
    VL.Core
    version
    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 channel
Sub-channels are created automatically by vvvv's SubChannel system when a type with
[CanBePublished(true)]
is published. You don't create them manually.
Use
const string
path constants to avoid typos:
csharp
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通道
当带有
[CanBePublished(true)]
的类型被发布时,子通道会由vvvv的子通道系统自动创建。无需手动创建。
使用
const string
路径常量避免拼写错误:
csharp
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
float
,
bool
, or
string
.
与层级节点模式相同,但输出类型是标准叶子值,如
float
bool
string

Indexed Spread Element Node (takes
int index
)

索引化Spread元素节点(接收
int index

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();
}
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
IObservable
pattern:
csharp
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)
}
使用标准
IObservable
模式订阅通道值变更:
csharp
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
System.Reactive.Unit
— NOT float:
csharp
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
channels show as "Unit" type in vvvv's node browser, not "Float".
对于触发/事件属性(删除、插入、移动操作),请使用
System.Reactive.Unit
——不要使用float:
csharp
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);
}));
Unit
通道在vvvv的节点浏览器中显示为"Unit"类型,而非"Float"。

Hierarchical 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 automatically
vvvv的通道系统会自动在层级中传播变更:
  • 向根记录通道写入值 → 所有子通道自动更新
  • 向叶子通道写入值 → 父通道自动更新
  • 这是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 automatically

Spread Channels and Sub-Channels

Spread通道与子通道

When a
Spread<T>
property is published (where
T
has
[CanBePublished(true)]
), vvvv automatically creates sub-channels for each element:
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:
Spread.AsSpreadUnsafe(array)
wraps without copying — the array must NOT be mutated after.
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...
}
Spread<T>
属性被发布时(其中
T
带有
[CanBePublished(true)]
),vvvv会自动为每个元素创建子通道:
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

重要规则

  1. NEVER
    TryAddChannel
    — only use
    TryGetChannel
    (lookup-only)
  2. Always retry-bind — channels appear after model initialization, not immediately
  3. [CanBePublished(true)]
    required
    on .NET types for channel publication
  4. Always dispose subscriptions — in the node's
    Dispose()
    method
  5. System.Reactive.Unit
    for bangs
    — not
    float
    or
    bool
  6. Spread.AsSpreadUnsafe
    — array must not be mutated after wrapping
  7. Suppression flags — prevent feedback loops when writing to subscribed channels
For code examples, see examples.md.
  1. 绝不要使用
    TryAddChannel
    ——仅使用
    TryGetChannel
    (仅查找)
  2. 始终重试绑定——通道在模型初始化后才会出现,不会立即可用
  3. 必须添加
    [CanBePublished(true)]
    ——.NET类型需要该特性才能被发布为通道
  4. 始终释放订阅——在节点的
    Dispose()
    方法中执行
  5. Bang通道使用
    System.Reactive.Unit
    ——不要使用
    float
    bool
  6. 使用
    Spread.AsSpreadUnsafe
    ——包装后的数组绝不能被修改
  7. 使用抑制标志——向已订阅的通道写入值时防止反馈循环
代码示例请参考examples.md