On top of that it was highly likely that the requirements for this GUI system would change and the level of sophistication behind the GUI system would increase - buttons would need to do more complicated tasks that could potentially be arbitrary. Having a hard-coded system in C# would not be feasible since tweaking logic would require a re-compile and an application restart.
I decided that since the system already had an IronRuby install built in, perhaps I could do this configuration file in Ruby as well. Given an XML configuration file that looks like this:
<group name="Buttons">I could translate this to Ruby that looks like this:
<line>
<button text="Cool Script">
<something_crazy_and_awesome />
</button>
</line>
</group>
group "Buttons" do
line do
button "Cool Script" do
something_crazy_and_awesome
end
end
end
On top of that in order to use this configuration file in a program I don't actually need to parse the file, I can just execute it within the context of different objects. For the dialog box that allows the user to set up the configuration I can just execute the Ruby code within that dialog box object and it will automatically create the elements that allow the user to change the configuration. Then in the part of the application where the user actually uses the configuration, I can just execute the file again but with a different definition of
group
, line
, etc. that construct the proper GUI elements.This is not just applicable to GUI elements, you can use this for anything that uses some sort of structured configuration. Rake uses this to great effect with tasks. But how would you go about implementing this pattern?
Turns out it's really simple using
instance_eval
. Here's an example that constructs the GUI element (this is inIronRuby so it uses .NET for GUI construction):
class GroupBuilder
def initialize parent, name, &block
@group_box = GroupBox.new(name)
parent.Controls.Add(@group_box)
instance_eval &block
end
def line &block
LineBuilder.new(@group_box, &block)
end
end
class LineBuilder
attr_accessor :panel
def initialize parent, &block
# in the GUI environment we use a panel for adding things
@panel = Panel.new
parent.Controls.Add @panel
instance_eval &block
end
def button text, &block
ButtonBuilder.new(@panel, text, &block)
end
# ... anything else that can be placed in a line ...
end
class ButtonBuilder
def initialize parent, text, &block
# create a button
btn = Button.new(text)
# bind the click event of the button to execute the
# Ruby code within the block
btn.Click { self.instance_eval &block }
parent.Controls.Add btn
end
def something_crazy_and_awesome
# do something crazy and awesome
end
end
class ScriptProcessor
def initialize control, script
@control = control
instance_eval File.read(script)
end
def group name, &block
GroupBuilder.new(@control, name, &block)
end
end
f = Form.new
ScriptProcessor.new(f, "my_config_file.rb")
f.Show
For each type of nested element you can create a class which define a method for the various types of sub-elements that you are allowed to have. Each of those methods will then handle the processing that needs to happen when the system sees an element of that type.
I think you could probably do this without classes and just use lambdas, but I think the code is clearer when you have objects since it is very explicit as to what each object is for.
Doing this with C# is a bit trickier since C# doesn't have instance_eval, but it turns out you can have a bit of a hack in order to get it to work quite well. I'll write up a quick post about this at a later date.
No comments:
Post a Comment