One of the highlights of last weekend’s AEC Hackathon in Berlin was getting to meet Michael and Keith from the Dynamo team. They’d delivered a pre-event workshop on the Friday and stayed on to tutor the many Dynamo-centric teams participating in the weekend’s Hackathon.
On afternoon on Saturday Keith and Michael presented an additional session that briefly mentioned Refinery (the optimization engine for Dynamo that’s currently in Beta) but focused mainly on the API infrastructure that enabled its integration into Dynamo: the View Extension API. It was really helpful for me to see how it’s possible to extend Dynamo with packages that contain binary components that extend the standard UI.
At the end of the weekend, I gave Michael a list of issues that we’d come across during the project Autodesk Research had worked on for Van Wijnen. One of these issues related to more easily finding issues in the graph: the Van Wijnen project resulted in a huge graph which was often quite challenging to navigate. One frustration I regularly had was scanning through the graph to find the “first” problem in the flow of data… in a data-flow environment you almost always need to fix the earliest nodes that causes a problem. Once that’s fixed you’ll need to look to see if there are still any later warnings, of course.
Anyway, in a huge graph – such as the one above – it’s sometimes really hard to find out which problem is the first that needs addressing. My thinking was that it’d be great to have a “take me to the earliest problem” button inside Dynamo. Keith and Michael both suggested this would be a great feature to implement via the View Extension API. This was music to my ears… not only an opportunity to dig into the Dynamo API, but a way to get a feature implemented much more quickly that it would otherwise be (given the need for it to be prioritised among other outstanding features). Oh, and a great topic to blog about and share the code for, of course. :-)
So it was that this weekend – as we had a very welcome rainy day in Switzerland on Saturday – I dug into the Dynamo API and created my first working Dynamo package (at least the first that contains binaries and not just custom nodes). It was actually really fun to have my own private Hackathon…. after spending last weekend helping people solve problems and deliver their own cool projects, I was itching to work on something similar.
At Keith’s suggestion, the starting point for my work was the SampleViewExtension project in the Dynamo GitHub repo. It was really easy to create a simple View Extension and get it loaded into Dynamo Sandbox. Keither also suggested some other great resources, such as the material from a recent Dynamo workshop in the UK (the various Dynamo workshop material can be found here).
After that I managed, feature by feature (and with a bit more guidance from Keith), to work out how to select nodes in the graph, zoom to fit them into the current view and then display their warning messages, driving all of this from a WPF DataGrid you could use to navigate through the various problem nodes that had been sorted by location. My logic for the sorting was fairly rudimentary: as data in Dynamo flows from left to right, I just take the X coordinate of the node’s top-left corner. With a cluttered graph this could easily not be the first problem node, but hey: at some point I may extend the implementation to follow connectors and really understand the graph’s layout, flowing from inputs to outputs.
Given the need to keep large graphs cleanly structured, the current implementation worked very well for our recent projects, at least.
By the end of the weekend I had a first cut of the package that I uploaded to the Dynamo Package Manager under the name “Warnamo”. (I almost went with “DynaWarn” but figured that “Walmsley’s Wonderful Warnamo” had a better ring to it. ;-)
Here’s a quick video of how it works. You can go through the list of nodes with the up and down arrow keys, which makes navigating warnings really easy.
I’ve now uploaded the code to GitHub, in case you’d like to check it out (or even extend it and submit a pull request). As it’s been ages since I’ve posted C# code to my blog, here’s one of the main source files (as it stands today, anyway):
using Dynamo.Core;
using Dynamo.Extensions;
using Dynamo.Graph.Nodes;
using Dynamo.Graph.Workspaces;
using Dynamo.Models;
using Dynamo.ViewModels;
using Dynamo.Wpf.ViewModels.Core;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
namespace WarningsViewExtension
{
public class WarningsWindowViewModel : NotificationObject, IDisposable
{
private ObservableCollection<NodeInfo> _warningNodes;
private ReadyParams _readyParams;
private DynamoViewModel _dynamoViewModel;
private NodeViewModel _displaying;
public ObservableCollection<NodeInfo> WarningNodes
{
get
{
_warningNodes = getWarningNodes();
return _warningNodes;
}
set
{
_warningNodes = value;
}
}
public ObservableCollection<NodeInfo> getWarningNodes()
{
if (_displaying != null)
{
HideTooltip(_displaying);
_displaying = null;
}
// Collect error/warning nodes, sorting by X position
// The second Select replaces the blank (0) ID with the row number
var nodeList =
(from n in _readyParams.CurrentWorkspaceModel.Nodes
where n.State != ElementState.Active && n.State != ElementState.Dead
orderby n.Rect.TopLeft.X ascending
select new NodeInfo(0, n.Name, n.GUID)).Select(
(item, index) => new NodeInfo(index + 1, item.Name, item.GUID)
);
// Return a bindable collection
return new ObservableCollection<NodeInfo>(nodeList);
}
// Construction & disposal
public WarningsWindowViewModel(ReadyParams p, DynamoViewModel dynamoVM)
{
_readyParams = p;
_dynamoViewModel = dynamoVM;
_readyParams.CurrentWorkspaceChanged +=
ReadyParams_CurrentWorkspaceChanged;
AddEventHandlers(_readyParams.CurrentWorkspaceModel);
}
public void Dispose()
{
_readyParams.CurrentWorkspaceChanged -=
ReadyParams_CurrentWorkspaceChanged;
RemoveEventHandlers(_readyParams.CurrentWorkspaceModel);
}
// Event handlers
void ReadyParams_CurrentWorkspaceChanged(
Dynamo.Graph.Workspaces.IWorkspaceModel obj
)
{
if (_readyParams != null)
{
RemoveEventHandlers(_readyParams.CurrentWorkspaceModel);
}
AddEventHandlers(obj);
RaisePropertyChanged("WarningNodes");
}
private void CurrentWorkspaceModel_NodeAdded(NodeModel node)
{
node.PropertyChanged += node_PropertyChanged;
}
private void CurrentWorkspaceModel_NodeRemoved(NodeModel node)
{
node.PropertyChanged -= node_PropertyChanged;
}
private void CurrentWorkspaceModel_NodesCleared()
{
foreach (var node in _readyParams.CurrentWorkspaceModel.Nodes)
{
node.PropertyChanged -= node_PropertyChanged;
}
RaisePropertyChanged("WarningNodes");
}
private void node_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "State")
{
RaisePropertyChanged("WarningNodes");
}
}
// Attach and remove handlers
private void AddEventHandlers(IWorkspaceModel model)
{
foreach (var node in model.Nodes)
{
node.PropertyChanged += node_PropertyChanged;
}
model.NodeAdded += CurrentWorkspaceModel_NodeAdded;
model.NodeRemoved += CurrentWorkspaceModel_NodeRemoved;
model.NodesCleared += CurrentWorkspaceModel_NodesCleared;
}
private void RemoveEventHandlers(IWorkspaceModel model)
{
foreach (var node in model.Nodes)
{
node.PropertyChanged -= node_PropertyChanged;
}
model.NodeAdded -= CurrentWorkspaceModel_NodeAdded;
model.NodeRemoved -= CurrentWorkspaceModel_NodeRemoved;
model.NodesCleared -= CurrentWorkspaceModel_NodesCleared;
}
public void ZoomToPosition(NodeInfo nodeInfo)
{
foreach (var node in _readyParams.CurrentWorkspaceModel.Nodes)
{
if (node.GUID == nodeInfo.GUID)
{
// node.Select();
var cmd = new DynamoModel.SelectInRegionCommand(node.Rect, false);
_readyParams.CommandExecutive.ExecuteCommand(cmd, null, null);
// Call this twice as otherwise the zoom level altertnates been close
// and far
_dynamoViewModel.FitViewCommand.Execute(null);
_dynamoViewModel.FitViewCommand.Execute(null);
// Display the error/warning message
var hsvm = (HomeWorkspaceViewModel)_dynamoViewModel.HomeSpaceViewModel;
foreach (var nodeModel in hsvm.Nodes)
{
if (nodeModel.Id == node.GUID)
{
// First hide the previously displayed one if there was one
if (_displaying != null)
{
HideTooltip(_displaying);
}
ShowTooltip(nodeModel);
_displaying = nodeModel;
break;
}
}
}
}
}
// Is the state a warning?
private bool IsWarning(ElementState state)
{
return
state == ElementState.Warning ||
state == ElementState.PersistentWarning;
}
// Expand the warning bubble for the provided NodeViewModel
private void ShowTooltip(NodeViewModel nvm)
{
var data = new InfoBubbleDataPacket();
data.Style =
IsWarning(nvm.State) ?
InfoBubbleViewModel.Style.Warning :
InfoBubbleViewModel.Style.Error;
data.ConnectingDirection = InfoBubbleViewModel.Direction.Bottom;
nvm.ErrorBubble.ShowFullContentCommand.Execute(data);
}
// Collapse he warning bubble for the provided NodeViewModel
private void HideTooltip(NodeViewModel nvm)
{
var data = new InfoBubbleDataPacket();
data.Style =
IsWarning(nvm.State) ?
InfoBubbleViewModel.Style.WarningCondensed :
InfoBubbleViewModel.Style.ErrorCondensed;
data.ConnectingDirection = InfoBubbleViewModel.Direction.Bottom;
nvm.ErrorBubble.ShowCondensedContentCommand.Execute(data);
}
}
}
I’m thinking of generalizing the code to create a core, generic “node manager” implementation that could be used (among other things) to search through a graph for all the “Preview” nodes in a graph and make sure they’re only located in a specified region. The fact that “Preview” is set to true by default on newly created nodes often causes problems when you’re trying to make sure only certain layers of the graph contribute preview graphics (other than during development and debugging). But maybe that’s just something that frustrates me? It’s quite possible.
Anyway, if you’d like to check out Warnamo, please install it from the package manager and let me know how you get on with it. This is my first attempt at publishing something for Dynamo, so I’m really keen to get your feedback!