Here is the pursuit of a series of related articles started by Ruby Inox Part1 where I set out to accomplish every task set by the creator. Promptly followed by his first instructions of implementing a simple Point class.
Althought I left out to publish on this site the specification and implementation of the Size class and Rect class, as they are very similar to that of the Point class; you can find the source on GitHub.
Let’s jump straight to the next challenge: Actions
Actions
The easiest way is to show you what the creator wanted me to achieve:
class Button action :clicked # This method gets called by the OS or WindowManager # we want to delegate this task to a Controller def __ugly_os_call_back(some_cryptic_os_args) sender = __ugly_os_way_get_sedner_from(some_cryptic_os_args) # execute the action clicked if a delegate of some kind # is provided clicked(sender) end end # # Usage example 1 # Assign a block code to do the work my_button = Button.new my_button.clicked { |sender| puts "Button #{sender} was clicked." } # we can call the clicked ourself my_button.clicked(my_button) # if a block is passed, the block is evaluated # in the context of the object my_button.clicked { puts self.class} => Button # we can unset an action by assigning nil my_button.clicked = nil # # Usage 2 # # If we have an existing proc, we can assign it # to the action p = Proc.new { puts "Hello World" } my_button.clicked = p # # Usage 3 # # We can assign a method to the action class MyConroller def clicked(sender) puts "Button #{sender} was clicked". end end my_controller = MyController.new my_button.clicked = my_controller.method(:clicked) # # Usage example 4 # # If we have one controller for every action # we can assign it so every action will be # set to the actions; if they exists my_button.actions = my_controller # we can reset all of the actions by passing nil as delegate my_button.actions = nil
Here are the point that an action should accomplish:
- if no args given follow std ruby behavior e.g call the action if assigned
- if a block/proc/method is given then store it for later use
- if called with argumetns than execute the action
- the block/proc/method are store in an instance variable
- a list of actions is mantained in a class_variable
With these 6 definitions the action is pretty much explain. We only have to add the actions part:
- actions= searches the object for action it responds to and set up the block code
- if passed nil to actions= , sets all the actions to nil
Now we can proceed at the first implementation of the action method.
Action Clicked
class Button def clicked(*sender, &block) @actions = Hash.new if @actions.nil? if block.nil? and sender.empty? then return @actions[:clicked] elsif block.nil? and not sender.empty? then return @actions[:clicked].call(sender.first) elsif not block.nil? and sender.empty? then @actions[:clicked] = block else raise "Do not accept an argument and a code block" end end end
Self-explanatory code? Basically if we had another action it would be exactly the same code, but with another name. This should ring the refracting bell! We can write a more general method for defining such an action, thanks to ruby’s ability of metaprogramming. By the same way we extract the action method and introduce the Delegate Module.
class Button def self.define_action(aname) self.class_eval %{ def #{aname}(*sender, &block) sym = "#{aname}".to_sym @actions = Hash.new if @actions.nil? if block.nil? and sender.empty? then return @actions[sym] elsif block.nil? and not sender.empty? then return @actions[sym].call(sender.first) elsif not block.nil? and sender.empty? then @actions[sym] = block return nil else raise "Do not accept an argument and a code block" end end } end # define the same clicked action as the initial code! define_action :clicked end
We can refractor this code further by introducing the module Delegate which will hold all off our code, so it can be used by any class. We rename the method define_action to simply action.
We also introduce the plural form of action which accepts an array of symbols and passes each of them to action.
module Actions def self.action(aname) self.class_eval %{ def #{aname}(*sender, &block) sym = "#{aname}".to_sym @actions = Hash.new if @actions.nil? if block.nil? and sender.empty? then return @actions[sym] elsif block.nil? and not sender.empty? then return @actions[sym].call(sender.first) elsif not block.nil? and sender.empty? then @actions[sym] = block return nil else raise "Do not accept an argument and a code block" end end } end def self.actions(*args) args.each { |aname| self.action(aname) } end # add the method to the class def self.included(othermod) othermod.module_eval %{ def self.action(aname) Actions::action(aname) end def self.actions(*args) Actions::actions(*args) end } end end class Button include Actions actions :clicked, :pushed, :readToDraw end
Tackle the actions=
We need to keep track of the actions on a per class basis. This is done by adding a class variable @@actions. Everytime and action is added to the class, a symbol is added to the @@actions array. Not to be mistaken with the @action hash holding the instance actions with the attached procs ready to be called.
When actions= is called, we can look to the list of actions and setup the corresponding methods the object we passed responds_to.
Final
Here is the final product:
module Actions # add class methods: action, actions def self.included(othermod) othermod.module_eval %{ @@actions = [] # defines one action def self.action(sym) @@actions << sym self.class_eval %{ def \#{sym}(*args, &block) self.perform_action("\#{sym}".to_sym, *args, &block) end def \#{sym}=(p) if p.kind_of?(Proc) or p.kind_of?(Method) then self.set_action("\#{sym}".to_sym, p) elsif p.nil? self.set_action("\#{sym}".to_sym, nil) else raise "Proc or Method expected" end end } end # called without args returns a list of # defined actions def self.actions(*args) if args.empty? @@actions else args.each { |a| self.action(a) } end end } end # set all the actions to call only the methods of one # delegate object # if obj if nil than resets all the actions to nil def actions=(obj) self.actions.each { |a| if obj.respond_to?(a) self.call_action!(a, assign_method(obj, a)) else self.set_action(a, nil) end } end # return the block/proc/method associated with the action def get_action(sym) @actions.nil? ? nil : @actions[sym] end # associates an action with a proc/block/method def set_action(sym, proc) @actions = Hash.new if @actions.nil? @actions[sym] = proc return nil end # returns true or false whenever a block/proc/method is # assicated with the action def action_assigned?(sym) not get_action(sym).nil? end # returns a list of defined actions def actions self.class.actions end # returns if this object has a given action def has_action?(sym) not self.actions.index(sym).nil? end # executes the action if assigned def call_action(sym, *args) action = self.get_action(sym) action.nil? ? nil : self.instance_exec(*args, &action) end protected # used by the metaprogramming to setup the stup method # for the action def perform_action(sym, *args, &block) case (args.empty? ? 0b0 : 0b1) | (block.nil? ? 0b10 : 0b100) when 0b10 then self.call_action(sym) when 0b100 then self.set_action(sym, block) when 0b11 then self.call_action(sym, *args) when 0b101 then args << block; self.call_action(sym, *args) end end end
Upcoming
With the ability to define actions and this basic Actions module we came a step closer in implementing a uniform MVC paradigm. Upcoming features to be described: delegate, events, properties.
Ruby Inox on GitHub
This file is part of the Ruby Inox project. Inox is a work in progress and this file might not be up to date. The status and latest files are available on GitHub
Related posts:
- Ruby Inox Part 3.1: Actions Here is a short update for the Ruby Inox Part3:...
- Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...
- Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
- Ruby Inox Part 7: Native Originally this article formed one article with Ruby Inox Part...
- Ruby Inox Part 5: Component Something promised is something due. Only two days have passed...
- Ruby Inox Part 2: A Point class Alright, the journey begins. I received the first specification! If...



ShareThis
[...] is a short updated for the Ruby Inox Part3: Actions article. Some stuff worked out just fine, some stuff had to be changed to facilitate [...]