关于 vvvv-channels
面向 vvvv gamma C# 开发者的 Channels 集成指南,介绍具名、类型化的响应式值容器及其层级传播,涵盖公开、读取、订阅和写入模式。
主要功能
- 01按路径访问公开 Channel
- 02使用属性发布 .NET 类型
- 03延迟绑定与重试查找
- 04响应式订阅和事件触发
- 05层级与 Spread 子 Channel 传播
适用场景
- 01从 C# 节点读取或写入应用状态
- 02将模型属性公开为 Channel
- 03订阅值变化并响应事件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tebjan/ vvvv-skills --skill vvvv-channels安装后在 Claude Code 中输入/vvvv-channels确认已加载。
附带文件examples.md
详细介绍
vvvv gamma Channels — C# 集成
什么是 Channels
Channels 是命名的、类型化的、可观察的值容器——vvvv gamma 中的核心响应式数据流机制。任何代码(patch、C# 节点、外部绑定)都可以通过字符串路径读取和写入 channels。
主要特性:
- 每个 channel 都有一个路径(字符串)、一个类型和一个当前值
- 设置值会触发所有订阅者(响应式推送)
- vvvv 提供 MIDI、OSC、Redis 和 UI 的内置 channel 绑定
- Channels 会在会话之间持久保存状态
Public Channels 和 IChannelHub
Public channels 是注册在应用级 channel hub 中的 channels——任何代码都可以通过字符串路径查找来访问它们。
核心 API(VL.Core.Reactive)
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 的 channels,导致 vvvv 的 SubChannelsBinding.EnsureMutatingPropertiesAreReflectedInChannels 中出现 NullReferenceException。SubChannel 系统会尝试遍历该空值的属性并崩溃。始终使用 TryGetChannel(仅查找)。
[CanBePublished] 特性
要让 vvvv 将 .NET 类型属性公开为 public channels,必须使用来自 VL.Core.EditorAttributes 的 [CanBePublished(true)] 装饰该类型。
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)]→ 所有属性都会作为 channels 发布 - 在单个属性上使用
[CanBePublished(false)]→ 在 channel 系统中隐藏该属性 - 默认情况下不会发布 .NET 类型——必须使用该特性
- 从
VL.Core版本2025.7.1-0163+ 开始可用
Channel 路径约定
Channels 使用以点分隔的层级路径。Spread 元素使用方括号表示法:
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
当发布带有 [CanBePublished(true)] 的类型时,vvvv 的 SubChannel 系统会自动创建子 channels。无需手动创建。
使用 const string 路径常量以避免拼写错误:
public static class ChannelPaths
{
public const string Volume = "Settings.Audio.Volume";
public const string Brightness = "App.Scene.Display.Brightness";
}
重试绑定模式
节点启动时,channels 可能尚不存在——vvvv 会在模型初始化后发布它们。必须每帧重试,直到 channel 出现:
[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 — 可复用工具类
一个封装了重试绑定和可选订阅模式的辅助类。可在任何项目中复用:
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 中的用法:
[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 访问器节点模式
用于封装 channel 访问的 ProcessNode 有三种模式:
层级节点(读取模型对象)
[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();
}
叶参数节点(读取类型化值)
与层级节点的模式相同,但输出类型是 float、bool 或 string 等标准叶值。
索引 Spread 元素节点(接受 int 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();
}
响应式订阅
使用标准的 IObservable 模式订阅 channel 值变化:
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
对于触发器/事件属性(删除、插入、移动操作),使用 System.Reactive.Unit——不要使用 float:
using System.Reactive;
[CanBePublished(true)]
public class MyInstance
{
public Unit DeleteInstance { get; set; } // Bang channel
public Unit InsertAfterInstance { get; set; } // Bang channel
}
事件本身就是 bang——值无关紧要。订阅并在回调中执行操作:
var ch = hub.TryGetChannel("Project.Items[0].DeleteInstance");
ch?.Subscribe(new BangObserver(() =>
{
// Triggered when the bang fires — queue the action
_pendingDeletions.Add(0);
}));
在 vvvv 的节点浏览器中,Unit channels 显示为 “Unit” 类型,而不是 “Float”。
层级数据传播
vvvv 的 channel 系统会自动在层级结构中传播更改:
- 向根记录写入其 channel → 所有子 channels 自动更新
- 向叶 channel 写入 → 父 channels 自动更新
- 这是 vvvv 的 SubChannel 系统内置的功能——无需手动传播
这支持高效的批量操作:
// 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 通道和子通道
发布 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(添加/移除/重新排序元素)时:
// 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...
}
关键规则
- 绝不要使用
TryAddChannel——只能使用TryGetChannel(仅查找) - 始终重试绑定——通道会在模型初始化后出现,而不是立即出现
- .NET 类型必须带有
[CanBePublished(true)],才能发布通道 - 始终释放订阅——在节点的
Dispose()方法中执行 - bang 使用
System.Reactive.Unit——不要使用float或bool Spread.AsSpreadUnsafe——包装后不得修改数组- 抑制标志——向已订阅的通道写入时,用于防止反馈循环
代码示例请参阅 examples.md。
持续积累这些知识
让下一个 patcher 少遇到一个谜题。首次使用本集合中的任何技能时,如果过去七天内没有成功检查记录,请将源内容与 tebjan/vvvv-skills 进行比对。如果没有记录,请检查一次。在这些技能之间共享一条本地检查时间戳/修订版本;不要在每次调用时都轮询。保留本地扩展和固定的项目版本;检查不代表获准安装更新。如果处于离线状态,请记录延期检查,并继续任务。
当实际工作暴露出缺漏时,请记录经过测试的 vvvv/包版本、最小复现示例或权威来源,以及观察到的结果。将其他用户的报告视为线索,未经验证前不要当作事实。改进最相关的最小范围说明;不要把一个示例变成普遍规则。提交上游之前,请删除私人数据并取得发布授权。请参阅贡献政策。合并后的改进会通过安装器/更新工作流送达用户,而不是通过本地编辑或自动覆盖每个人的技能。