Topic: Subclassing controllers and views

In my RoR application, I'm not making use of the model layer because the model is implemented through a C++ server and my RoR application communicates with that server using a middleware product.

Relay nice about my C++ server is that it holds very rich meta information describing the business objects it returns in great details. This allows me to implement a single controller/view that can render any object type in a very generic way.

While this model works perfectly, there is a downside. For some object types, the generic representation is not quite sufficient. I might have to override or augment the default behaviour.

In a truly object oriented environment I would simply subclass my ObjectController. The sub-classed controller could neatly alter the behaviour of the ancestor class. The problem is, I have no idea how I can direct RoR to instantiate the sub-classed controller. Is this even possible?

Pete

Re: Subclassing controllers and views

Thanks to Justin French I got a bit further on this.

I now have the following controller hierarchy:

         ActionController::Base
                    ^
                    |
          ApplicationController
                    ^
                    |
             VehicleController
            ^                  ^
           /                    \
Vehicle::CarController   Vehicle::BikeController

While this goes some ways to address my design issue, one of the things it doesn't seem to do "out-of-the-box" is to allow me to override views.

Consider my VehicleController implements the method show. In app/views/vehicle I have a file show.rhtml. I never instantiate a VehicleController object though: I either instantiate a Vehicle::CarController or a Vehicle::BikeController. Either of these two controllers inherits the show method from VehicleController.

However, there is now an issue with selecting the views. Consider I send the show message to a Vehicle::CarController. Ruby correctly invokes the VehicleController.show method which might call render(:action => "show"). The desired behaviour is now that if a show.rhtml was found in app/views/vehicles/car, rails should render that template. If none was found there, it should try and pick the show.rhtml from app/views/vehicle and if that didn't exist ether, it should fail.

Can I somehow achieve this behaviour with known techniques?

Last edited by petehug (2007-10-23 20:51:17)

Re: Subclassing controllers and views

Finally, i managed to get view inheritance working. It isn't perfect as it requires that you use one specific way to render templates as you'll see further down, but if you stick with it you get some cool benefits.

Let's use the controller hierarchy from my previous post as an example and assume that I have a Vehicle::Bike controller that receives the show message. Here are some of the choices I now have with the new code:

- I can let VehicleController handle it and don't even bother to implement anything

- I implement _show.rhtml for Vehicle::Bike which will override Vehicle's _show.rhtml

- I implement _show.rhtml for Vehicle::Bike which will override Vehicle's _show.rhtml but I render Vehicle's _show.rhtml after and/or before rendering additional output

- I can omit implementing a _show.rhtml in any of the controllers but create one in my app/views/shared folder which will now be picked by the system

The way I got this working was by augmenting ActionController::Base with two versions of the method template_for(template_name). One of these methods is a class method and the other an instance method. The instance method simply invokes the class method and returns what it returns: the fully qualified path of the view it found. This path can be used with a call to render(:file => view_path, ...).

The class method builds an array of paths. Firstly, it adds it's own path to the array. It then gets it's superclass' path and adds it to the array. Then it gets the superclass' superclass and adds it's path to the array and continues to do so until the found superclass is ActionController::Base when which terminates generating this search path array. At this stage we have an array which stores a system path in each element, the first element containing the most specialised path and the last the most generic path.

Now, starting with idx=0 we try and locate the view "#{array[idx]}/_#{name}.rhtml" and if found this is what we return. If we can't find it we increment idx and continue until we either find the view template or until we checked each path. If we couldn't locate the view template we finally check app/views/shared (change this behaviour as you wish).

So here is what I added to my application.rb file:

# We augment the ActionController::Base class so that subclassed
# controllers can better specialise views.
class ActionController::Base
  def self.template_for(template_name)
    # populate the path array with the first element containing the
    # most specific path and the last the least significant path
    sc = self
    path = []
   
    while sc != ::ActionController::Base
      path[path.length] = "#{read_inheritable_attribute(:template_root)}/#{sc.controller_path()}"
      sc = sc.superclass
    end
   
    # check if a file exists from the most to the least significant
    # path and if successful, return the fully qualified file path
    path.each do | path_name |
      view_file = "#{path_name}/_#{template_name}.rhtml"
      return view_file if File.exists? view_file
    end
   
    # look in shared views also
    view_file = "#{RAILS_ROOT}/app/views/shared/_#{template_name}.rhtml"
    return view_file if File.exists? view_file
   
    # no template found error
    redirect_to :action => "error"
  end

  def template_for(template_name)
    self.class.template_for(template_name)
  end
end


With this in place, my sub-classed controller can do stuff like:

render(:file => template_for("name"), :layout => true)

Which would look for _name.rhtml from the most significant to the least significant path. Also, _name.rhtml can have nested render calls which behave in the same way:

<h1>This template embeds template _embed.rhtml</h1>
<%= render(:file => @controller.template_for("embed"), :use_full_path => false, :locals => {:id => "blah"}) %>
<p>More to follow</p>

Note that in the above template, _embed.rhtml is loaded through the current controller and hence, I can implement my own version of _embed.rhtml in the specific view folder associated with my specialised controller. In other words, overloading view templates works also for embedded templates in the exact same way.

I can also call the ancestors template and do additional stuff before and/or after calling the ancestors template. My Vehicle::Bike controller might have a view (aka partial template) called _name.rhtml:

<p>Stuff to appear before the original template</p>
<%= render(:file => VehicleController.template_for("name"), :use_full_path => false) -%>
<p>Stuff to appear after the original template</p>

As you can see, the above template simply adds a paragraph before and after rendering Vehicle's _name.rhtml.

Pete

Last edited by petehug (2008-09-08 02:17:39)

Re: Subclassing controllers and views

This is great. Any comments from the core team as to integrate this? Inheriting views is very useful.