Tuesday, November 27, 2012

Dynamo - Revit Transaction Modes

I'm sitting at an airport bar waiting for my flight back to Boston to start boarding, so I figure now is as good a time as any to discuss how Dynamo interacts with the Revit API via transactions.

All Dynamo nodes know whether or not they require a transaction in order to evaluate. This is necessary in order to make sure that they are run within a transaction, but also so that they aren't run in a transaction if they don't need to be.

Dynamo has three internal "transaction modes" which are set based on various factors.

Automatic

The default mode. If any node in the workflow requires a transaction to evaluate then the whole Dynamo expression is run in a transaction in the Revit idle thread. Otherwise, it is run in its own thread.

Manual

This mode is active if the workflow contains a Transaction node. In this mode, it is up to the user to make sure that all nodes which require a transaction are connected to a Transaction node (either directly or through one or more "parent" nodes). This is desired if you would like to see intermediate results in Revit or if you need a complete Transaction before continuing to evaluate. A good example is the Solar Radiation Optimizer sample, which has to end a Transaction in order for Solar Radiation to update and feed data back into Dynamo.

Debug

Activated when the Debug box is checked in the UI, every node which requires a transaction is evaluated in its own transaction. This allows you to see every node's effect on the document, but is also really slow. Fun fact: this was the first (and for a while, the only) transaction mode.

Monday, November 26, 2012

Dynamo - Sliders

The End Result


Slider nodes now have a text box that displays the current value underneath the slider as you move it.

Thoughts

I feel like any technology substantially cool also makes me want to pull my hair out.

Sometimes I'm amazed at how well WPF can get things done. I can usually link together how I'd like a UI to work using abstract notions that don't translate well to paper, let alone code, but WPF somehow has the ability to do this translation as simply as possible.

That doesn't mean that there isn't a lot of trial and error, however!

One long requested feature for Dynamo was the ability to see the exact value of a slider. The reason why this is useful is obvious, but because I didn't really know how I'd like the UX to work, I put the feature off for a while. Besides, I'm more of an engine guy and not so much into UI (ironic, considering I work on a programming language who's defining feature is a UI).

This weekend, in a alcohol turkey-induced haze, I came up with a decent idea:  have a TextBox appear when the user's mouse is over the slider, and have it disappear when the mouse is removed. "But wait Stephen," you exclaim excitedly, "Why don't you just use a tool tip? It does almost exactly that!" You see, Dynamo has tool tips for nodes reserved for node descriptions and error messages, and there is no clean way to override that functionality. Also, that's not really what tool tips are for, in my (humble, UX-lacking) experience.

OK. So how was it implemented?

Early in Dynamo's life, Ian Keough added a Canvas to the UI for Dynamo nodes. This Canvas was used to draw the chrome on the nodes, as well as the title text. I was able to use the Canvas in order to position a TextBox at an arbitrary coordinate relative to the node. Then, it was simply a matter of hooking the appropriate events to some logic that controls when the TextBox is displayed and hidden, and setting up a Binding that binds the TextBox's Text property to the current value of the slider.

Unfortunately for fans of XAML (but fortunately for myself, who isn't one), this has to be done programmatically in C#, due to how Dynamo nodes are initialized. This is what the code looks like:
1:  Slider tb_slider;  
2:  dynTextBox mintb;  
3:  dynTextBox maxtb;  
4:  TextBox displayBox;  
5:    
6:  public dynDoubleSliderInput()  
7:  {  
8:    tb_slider.ValueChanged += delegate  
9:    {  
10:      var pos = Mouse.GetPosition(elementCanvas);  
11:      Canvas.SetLeft(displayBox, pos.X);  
12:    };  
13:      
14:    tb_slider.PreviewMouseDown += delegate  
15:    {  
16:      if (this.IsEnabled && !elementCanvas.Children.Contains(displayBox))  
17:      {  
18:        elementCanvas.Children.Add(displayBox);  
19:    
20:        var pos = Mouse.GetPosition(elementCanvas);  
21:        Canvas.SetLeft(displayBox, pos.X);  
22:      }  
23:    };  
24:      
25:    tb_slider.PreviewMouseUp += delegate  
26:    {  
27:      if (elementCanvas.Children.Contains(displayBox))  
28:        elementCanvas.Children.Remove(displayBox);  
29:    };  
30:    
31:    displayBox = new TextBox()  
32:    {  
33:      IsReadOnly = true,  
34:      Background = Brushes.White,  
35:      Foreground = Brushes.Black  
36:    };  
37:    Canvas.SetTop(displayBox, this.Height);  
38:    Canvas.SetZIndex(displayBox, int.MaxValue);  
39:    
40:    var binding = new System.Windows.Data.Binding("Value")  
41:    {  
42:      Source = tb_slider,  
43:      Mode = System.Windows.Data.BindingMode.OneWay,  
44:      Converter = new DoubleDisplay()  
45:    };  
46:    displayBox.SetBinding(TextBox.TextProperty, binding);  
47:  }  
48:    
49:  [ValueConversion(typeof(double), typeof(String))]  
50:  private class DoubleDisplay : IValueConverter  
51:  {  
52:    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)  
53:    {  
54:      return ((double)value).ToString("F4");  
55:    }  
56:    
57:    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)  
58:    {  
59:      return null;  
60:    }  
61:  }  

(Note that I've removed code that's irrelevant to this explanation, which I'll summarize quickly: the initialization and configuration of the fields were removed, as well as the initialization code for the inherited Dynamo node UI.)

On line 8: When the slider's ValueChanged event occurs, I get the current mouse position relative to the Canvas, which the TextBox is a child of, and then use the X-coordinate to place the TextBox in line with the mouse.

On line 14: When the user clicks on the slider, first I check to see if the TextBox should be displayed. If it actually should, then I add the TextBox to the Canvas, and set it's X position in the Canvas just like I did in the previous event handler.

On line 25: When the user releases the mouse button over the slider, then I check if the TextBox is currently being displayed, and if it is, I stop displaying it.

On line 31: I initialize and configure the TextBox. Note that on line 37, I set the Y coordinate to the height of the node, thus making sure that it is always displayed right below the node.

On line 40: I create a Binding that ties what text that the TextBox displays to the value of the slider. Note that I set an explicit Converter. I wan't familiar with programmatically making WPF Bindings, but fortunately MSDN has a great tutorial on the subject.

On line 52: I create a new implementation of an IValueConverter, that converts double to String. This is used to truncate the number of decimals to 4, as opposed to the default, which was a lot more.

And that's all there is to it!