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...
Tags: cross language, Inox, oo model, Point class, Ruby
[...] 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 [...]