Ruby Inox Part 3: Actions

2009
05.11

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:

  1. if no args given follow std ruby behavior e.g call the action if assigned
  2. if a block/proc/method is given then store it for later use
  3. if called with argumetns than execute the action
  4. the block/proc/method are store in an instance variable
  5. 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:

  1. actions= searches the object for action it responds to and set up the block code
  2. 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:
  1. Ruby Inox Part 3.1: Actions Here is a short update for the Ruby Inox Part3:...
  2. Ruby Inox Part 4: Property Last time, we took a look at Actions. A standard...
  3. Ruby Inox Part 4.1: Property In Ruby Inox Part 4: Property I described I would...
  4. Ruby Inox Part 7: Native Originally this article formed one article with Ruby Inox Part...
  5. Ruby Inox Part 5: Component Something promised is something due. Only two days have passed...

Tags: , , , ,

One Response to “Ruby Inox Part 3: Actions”

  1. [...] 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 [...]


Ironic Wolf is Digg proof thanks to caching by WP Super Cache