Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

Tuesday, November 11, 2014

Elegant undo/redo recording with Reactive Extensions

I've been spending a lot of time lately thinking about functional reactive programming. Since I'm currently primarily a .NET developer, the obvious library for this kind of thing is Reactive Extensions (Rx), which provides a lot of convenient methods (often called combinators) for working with IObservables and IObservers. While not quite as expressive as other FRP libraries (I'm looking at you, reactive-banana), it's the best option for doing this sort of thing on .NET.

To me, the holy grail of FRP is to implement a large-scale application using as little mutable state as possible. Rx goes a long way in making this possible, but there are several things which are still tricky to model using these tools. Because it's a relatively new library, which is part of a relatively new paradigm (as far as mainstream adoption goes), there also isn't a lot of information online about implementing more complicated functionality using only the concepts provided by this framework.

The first difficulty I discovered was implementing undo and redo for an application developed with Rx. Specifically, how do you take several different observable sequences, record their values in a central place (the undo/redo stacks), and then at a later point when an undo or redo operation is invoked, replay those values in their original sequences? Is this even the correct way to approach the problem of undo and redo in Rx? Searching online yielded next to no information on the subject, and not just in Rx, but FRP in general. In my search, I found a compelling example in Haskell that demonstrated how to define mutually recursive reactive circuits, which would allow you to define a reactive value in terms of itself. This would allow me to combine the output of a stream with some form of undo/redo trigger, and then feed the result back into itself. Unfortunately, this kind of thing proved to be absurdly difficult to implement for Rx, because Haskell is lazy, .NET has shoddy tail-call optimization, and the library used in the example is a completely different kind of reactive framework (Rx is monadic, reactive-banana is applicative).

So with minimal help from the internet, I set out to implement some kind of reactive undo/redo recorder that satisfies my original design goal: in the definition of an observable sequence, you can specify a point at which values are recorded for undo and redo. If the recorded value is then re-played via an undo or redo action, then that value is re-inserted into the observable sequence at the exact same point it was recorded.


The source code is rather short and is heavily documented, so if you really want to understand how it works it shouldn't be too difficult. At a high level, the actual undo and redo stacks are stored in an immutable structure. Since this structure changes over time, I represented its various states as an observable sequence. The state can be mutated in three ways: recording a new action, performing an undo, and performing a redo. When performing an undo or redo, not only is the stack updated, but the operation that was originally recorded (or its inverse) is performed. When recording data from a source stream, it is packaged in such a way that when an undo or redo occurs, that data is sent back into the stream via an Rx Subject.

Below are some samples demonstrating some of the various ways you can use the ReactiveUndoRedoRecorder class.


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!