I believe this is a bug, but maybe there is another (better?) way to asynchronously handle changes to the values of an IOBox when its connected at the top. I am aware that I can poll its values, but that solution does not perform well, as I have to check the pin on every evaluation. Any hints welcome!
Thanks,
k
Expected Behavior
When an IOBox input pin is connected to something I would like to be able to register the Change event handler to be able to handle those events asynchronously instead of polling the pin for changes every frame.
Actual Behavior
A registeredChange handler on e.g. Y Input Value never fires when connected.
Example Plugin Code
#region usings
using System;
using System.ComponentModel.Composition;
using VVVV.PluginInterfaces.V1;
using VVVV.PluginInterfaces.V2;
using VVVV.PluginInterfaces.V2.Graph;
using VVVV.Utils.VColor;
using VVVV.Utils.VMath;
using VVVV.Core.Logging;
#endregion usings
namespace VVVV.Nodes
{
#region PluginInfo
[PluginInfo(AutoEvaluate = true, Name = "Test", Category = "Value", Help = "Basic template with one value in/out", Tags = "")]
#endregion PluginInfo
public class ValueTestNode : IPluginEvaluate
{
#region fields & pins
[Input("Input", DefaultValue = 1.0)]
public ISpread<double> FInput;
[Import()]
public ILogger FLogger;
[Import()]
public IHDEHost Host;
private Boolean Initialized = false;
#endregion fields & pins
private void InputChanged (Object o, EventArgs args)
{
FLogger.Log(LogType.Debug, "input changed!");
}
private void OutputChanged (Object o, EventArgs args)
{
FLogger.Log(LogType.Debug, "output changed!");
}
private void Initialize()
{
FLogger.Log(LogType.Debug,"Initialized!");
foreach(INode2 node in Host.ExposedNodeService.Nodes)
{
if (node.NodeInfo.ToString() == "IOBox (Value Advanced)")
{
foreach(IPin2 pin in node.Pins)
{
if(pin.Name == "Y Input Value")
{
FLogger.Log(LogType.Debug, "input intialized");
pin.Changed += InputChanged; // only fires on mouse interaction when not connected
}
if(pin.Name == "Y Output Value")
{
FLogger.Log(LogType.Debug, "output intialized");
pin.Changed += OutputChanged; // never fires
}
}
}
}
Initialized = true;
}
//called when data for any output pin is requested
public void Evaluate(int SpreadMax)
{
if(!Initialized)
Initialize();
//FLogger.Log(LogType.Debug, "hi tty!");
}
}
}
Hey there - just wanted to give you a heads up, we’ve discussed this issue internally and I’ll try to provide an example code of how to solve what you’re looking for in the next days (if possible). I’ll keep you posted.
Ok here you go, I hope the code is self explanatory enough. I’m using the newly introduced IValueData, IStringData etc. interfaces to ask the pins for data. The IPin.Changed event only works for user changes on unconnected input pins. I’ve updated the documentation for it a little to get rid of that confusion. We don’t really want to implement that event on all pins as it would’ve quite an impact on the overall performance.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using VVVV.Core.Logging;
using VVVV.PluginInterfaces.V1;
using VVVV.PluginInterfaces.V2;
using VVVV.PluginInterfaces.V2.Graph;
using VVVV.Utils.VColor;
namespace VVVV.Nodes
{
[PluginInfo(AutoEvaluate = true, Name = "IOBoxWatcher", Category = "VVVV")]
public class IOBoxWatcherNode : IPluginEvaluate, IPartImportsSatisfiedNotification, IDisposable
{
/// <summary>
/// Little wrapper around the native IO boxes so we've a unified view on them.
/// </summary>
abstract class IOBox
{
public static IOBox Wrap(INode2 node)
{
return Wrap(node.InternalCOMInterf, node.NodeInfo);
}
public static IOBox Wrap(INode node, INodeInfo nodeInfo)
{
var name = nodeInfo.ToString();
switch (name)
{
case "IOBox (Value Advanced)":
return new ValueIOBox(node, nodeInfo);
case "IOBox (Color)":
return new ColorIOBox(node, nodeInfo);
case "IOBox (String)":
return new StringIOBox(node, nodeInfo);
default:
break;
}
return null;
}
public IOBox(INode node, INodeInfo nodeInfo)
{
Id = node.GetNodePath(useDescriptiveNames: false);
Name = nodeInfo.ToString();
Node = node;
InputPin = GetInputPin(node);
}
/// <summary>
/// Pointer to the native IO box node.
/// </summary>
public INode Node { get; private set; }
/// <summary>
/// Pointer to the native input pin of the IO box.
/// </summary>
public IPin InputPin { get; private set; }
/// <summary>
/// The ID of the IO box.
/// </summary>
public string Id { get; private set; }
/// <summary>
/// The name of the IO box.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Retrieve the input pin with which to sync.
/// </summary>
protected abstract IPin GetInputPin(INode node);
/// <summary>
/// Sync our spread with the native pin.
/// </summary>
/// <returns>True if the data changed.</returns>
public abstract bool Sync();
public override string ToString()
{
return Name + " " + Id;
}
}
abstract class IOBox<T> : IOBox
{
public IOBox(INode node, INodeInfo nodeInfo) : base(node, nodeInfo)
{
Spread = new Spread<T>();
}
public Spread<T> Spread { get; private set; }
}
sealed class ValueIOBox : IOBox<double>
{
readonly IValueData FData;
public ValueIOBox(INode node, INodeInfo nodeInfo) : base(node, nodeInfo)
{
FData = InputPin as IValueData;
}
protected override IPin GetInputPin(INode node)
{
return node.GetPin("Y Input Value");
}
public override bool Sync()
{
var changed = false;
Spread.SliceCount = InputPin.SliceCount;
for (int i = 0; i < Spread.SliceCount; i++)
{
double value;
FData.GetValue(i, out value);
if (value != Spread[i])
{
Spread[i] = value;
changed = true;
}
}
return changed;
}
}
sealed class StringIOBox : IOBox<string>
{
readonly IStringData FData;
public StringIOBox(INode node, INodeInfo nodeInfo) : base(node, nodeInfo)
{
FData = InputPin as IStringData;
}
protected override IPin GetInputPin(INode node)
{
return node.GetPin("Input String");
}
public override bool Sync()
{
var changed = false;
Spread.SliceCount = InputPin.SliceCount;
for (int i = 0; i < Spread.SliceCount; i++)
{
string value;
FData.GetString(i, out value);
if (value != Spread[i])
{
Spread[i] = value;
changed = true;
}
}
return changed;
}
}
sealed class ColorIOBox : IOBox<RGBAColor>
{
readonly IColorData FData;
public ColorIOBox(INode node, INodeInfo nodeInfo) : base(node, nodeInfo)
{
FData = InputPin as IColorData;
}
protected override IPin GetInputPin(INode node)
{
return node.GetPin("Color Input");
}
public override bool Sync()
{
var changed = false;
Spread.SliceCount = InputPin.SliceCount;
for (int i = 0; i < Spread.SliceCount; i++)
{
RGBAColor value;
FData.GetColor(i, out value);
if (value != Spread[i])
{
Spread[i] = value;
changed = true;
}
}
return changed;
}
}
[Import]
public ILogger FLogger;
[Import]
public IHDEHost Host;
[Output("Watched IO Boxes")]
public ISpread<string> FWatchedIOBoxesOut;
[Output("Changed")]
public ISpread<bool> FChangedOut;
readonly Dictionary<INode, IOBox> FWatchedIOBoxes = new Dictionary<INode, IOBox>();
public void OnImportsSatisfied()
{
// Listen to changes on exposed IO boxes
var nodeService = Host.ExposedNodeService;
nodeService.NodeAdded += NodeService_NodeAdded;
nodeService.NodeRemoved += NodeService_NodeRemoved;
foreach (var node in nodeService.Nodes)
TryWrap(node);
}
public void Dispose()
{
var nodeService = Host.ExposedNodeService;
nodeService.NodeAdded -= NodeService_NodeAdded;
nodeService.NodeRemoved -= NodeService_NodeRemoved;
FWatchedIOBoxes.Clear();
}
public void Evaluate(int spreadMax)
{
FWatchedIOBoxesOut.SliceCount = FWatchedIOBoxes.Count;
FChangedOut.SliceCount = FWatchedIOBoxes.Count;
var i = 0;
foreach (var ioBox in FWatchedIOBoxes.Values)
{
FWatchedIOBoxesOut[i] = ioBox.ToString();
FChangedOut[i] = ioBox.Sync();
i++;
}
}
void NodeService_NodeAdded(INode2 node)
{
TryWrap(node);
}
void NodeService_NodeRemoved(INode2 node)
{
Remove(node);
}
void TryWrap(INode2 node)
{
var ioBox = IOBox.Wrap(node);
if (ioBox != null)
FWatchedIOBoxes.Add(ioBox.Node, ioBox);
else
FLogger.Log(LogType.Error, "Wrapper for IO box " + node + " not implemented.");
}
void Remove(INode2 node)
{
FWatchedIOBoxes.Remove(node.InternalCOMInterf);
}
}
}