Kiddo is a Ruby DSL for building simple WPF and Silverlight applications, inspired by the Shoes library. It’s still an early alpha, but a very cool concept none-the-less. I rather like the idea of being able to produce a slick looking WPF (or Silverlight) UI using a simple Ruby API. The basic syntax looks like this (taken from the post linked to above):
require "kiddo"
Kiddo.start do
draw :stack do
draw :text, "Text to reverse:", :font_size => 18
draw :edit_box, :named => "input"
draw :text, "Your string will show up here.", :named => "result"
draw :button, "Reverse It!", :named => "fun_button"
end
after :fun_button.is_clicked do
:result.text = :input.text.reverse
end
end
If you run this, you’ll get something like this on your screen:
You type in some text in the textbox, click the button, and the app will reverse the string and show it above the button. I think the sample code is fairly self explanatory, but if you’re not sure what it’s doing, check out the link above.
The only issue I had with Kiddo is that it wasn’t clear how one could test the logic that was occurring in one of the event handlers (like the string reversing logic in the “after” clause above). I wasn’t able to figure out how I could plug in an MVVM (Model View View-Model: the approximate thick-client equivalent of the Model View Controller pattern for web applications) framework without modifying Kiddo, so I went ahead and monkey patched it to support a very basic type of MVVM that would enable me to easily test the logic that handled UI events. The MVVM extension would need enhancing to be used for probably anything significant, but I think that’s probably true of Kiddo also. Here is the modified Kiddo example, hooking into the extensions I added…
require "kiddo"
require "kiddo_mvvm"
require "main_view_model"
Kiddo.start(:view_model => MainViewModel) do
draw :stack do
draw :text, "Text to reverse:", :font_size => 18
draw :edit_box, :named => "input"
draw :text, "Your string will show up here.", :named => "result"
draw :button, "Reverse It!", :named => "fun_button"
end
command :fun_button.is_clicked, :reverse_text
end
There are three differences between this and the original example. First, the updated example requires the file with the Kiddo extension I added (kiddo_mvvm.rb) and file with the ViewModel that has the logic that will respond to the button press (main_view_model.rb) Next, the ViewModel class is associated with the sample UI by passing it in as an option on the Kiddo.start method call. Finally, instead of having an “after” clause with the actual logic that responds to the button click, we have a call to the new “command” method which ties the button click event (:fun_button.is_clicked) to the method on the ViewModel that should be executed when the event fires (reverse_text).
Here is the code for the ViewModel class containing the logic (main_view_model.rb):
class MainViewModel
attr_accessor :input
attr_accessor :result
def reverse_text
@result = @input.reverse
@input = "thank you"
end
end
It defines public getter/setter properties that correspond to the text of the “input” textbox and the “result” text (label). It also defines the “reverse_text” method that is invoked by the MVVM extensions to Kiddo in response to the button being clicked. That method reverses the string in “input” and sets it into “result”. To illustrate the advantage that this approach has to simply calling a method on a business object in an “after” clause (that the command can easily read from and write to several bound widgets), it also sets “input” to “thank you”. Now, you can run the revised sample which behaves like the original sample except that it also puts “thank you” in the input box after you click the button.
Now you can write a test directly against MainViewModel to test the logic without bringing the actual UI into play. Here is a simple example of that (test_MVVM_custom.rb), using the very simple Test::Unit ruby testing framework:
require "test/unit"
require "main_view_model"
class MainViewModelTest < Test::Unit::TestCase
def test_reversing
vm = MainViewModel.new
vm.input = "mytest"
vm.reverse_text
assert_equal "tsetym", vm.result
end
end
The fixture defines a single test that instantiates the ViewModel, sets the input to “mytest”, calls reverse_text and verifies that the result is “tsetym”. Running a test/unit test is very simple, simply execute the ruby file! (ir test_MVVM_custom.rb)
Finally, here is the code for the MVVM extensions I added to Kiddo (kiddo_mvvm.rb):
module Kiddo
class App
attr_accessor :view_model
end
class << self
alias_method :original_start, :start
end
def self.start(options={}, &block)
view_model = options.delete(:view_model)
if view_model
App.instance.view_model = view_model.new
end
original_start options, &block
end
def command(hash, view_model_method)
view_model = App.instance.view_model
if !view_model
raise "Must pass a ViewModel instance to Kiddo.start in order to bind a command"
end
hash[:instance].send hash[:method] do |sender, args|
#Copy data from widgets into ViewModel
view_model.methods.each do |method|
if method.index("=") == method.length - 1
property = method.chop
widget = App.instance.locate property
if widget and defined?(widget.text)
view_model.send method, widget.text
end
end
end
view_model.send view_model_method
#Copy data from ViewModel back into widgets
view_model.methods.each do |method|
if method.index("=") != method.length - 1
property = method
widget = App.instance.locate property
if widget and widget.respond_to?("text=")
new_text = view_model.send method
widget.text = new_text
end
end
end
end
end
end
This code theoretically shouldn’t need to get modified on an app by app basis (except to enable new types of controls, etc.) It essentially just adds in support for the “view_model” option on Kiddo.start and then defines the new “command” method. That method loops through all the methods on the ViewModel that end in “=” (Ruby property setters) and tries to match them to UI widgets based on their name. When a match is found, it sets the ViewModel property to the text of the corresponding widget. (Obviously this approach won’t work for all types of widgets, but it’s a start!) Next, it calls the method on the ViewModel that has the name passed in from the caller. Finally, it loops through all the methods on the ViewModel that don’t end in “=” and tries to match those to widget names. When a match is found, it sets the text on the widget to the corresponding property value from the ViewModel. This enables the ViewModel class to effectively “bind” to widgets simply by creating a getter/setter property with the same name as the widget.
I’m contemplating porting MongoDB Management Studio to use Kiddo, so if that comes to pass, then there will likely be a number of enhancements to this extension.