Ruby on Rails with Flex
Many developers develop in PHP or Java, but there are some out there that have jumped on board with the dynamic Ruby language, specifically Ruby on Rails. I too have really enjoyed developing sites in Ruby on Rails, but mainly with Ruby on Rails connected to a Flex RIA.
When first getting into Ruby on Rails many people have seen the screen cast “Creating a weblog in 15 minutes with Rails 2″, what I want to do is show developers hope to continue their Ruby on Rails application just a bit further by connecting their newly created weblog to a Flex weblog powered by Ruby on Rails.
Start by watching the screen cast to create the Ruby on Rails weblog.
Continuing on I am going to assume that you have created the weblog exactly from the presentation. We will now inject our flex application into the ruby project. I’ve added a new folder into our ruby blog project titled “flex_app”. Then I added a new Flex project within the flex_app folder with the following settings.
Now we need to add the glue that we will use to bind the Flex app to the Ruby app, RubyAMF. Using terminal (Mac) or your preferred shell scripting application we will go to our ruby application and run this gem installation script.
1 | ruby script/plugin install http://rubyamf.googlecode.com/svn/tags/current/rubyamf |
This will install all the code necessary to run your AMF gateway along with adding a “rubyamf_config.rb” file into your ruby application config folder. The AMF config file includes full documentation and many options, feel free to read through this file and see some of your options. If your ruby server is already running you will need to restart the server for the AMF gateway’s configuration to run on your server. You can test that the gateway is working by using your browser and going to http://yourlocalserver/rubyamf/gateway. Make sure to put in your actual server address and not “yourlocalserver”, for my system it is: http://localhost:3004/rubyamf/gateway.
If your gateway is working you should see the following ruby amf logo.
Now with the AMF Gateway kickin’ we can adjust our Ruby controllers to handle the AMF format so that we can get to our Flex application.
In our Ruby controllers you will find the following script blocks.
1 2 3 4 5 | respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } format.json { render :json => @posts } end |
We will make only the slightest change to all of these code blocks to support the AMF format. The change is shown below.
1 2 3 4 5 6 | respond_to do |format| format.html # index.html.erb format.xml { render :xml => @posts } format.json { render :json => @posts } format.amf { render :amf => @posts } end |
We now have a Ruby application that has full AMF support and we can focus on the Flex programming. The only changes we will need to make in the Ruby application from this point forward will be to make some changes to our AMF config file and make one small adjustment to our controller – we will do this after we finish our Flex Application.
The Flex Application
At this point I am going to assume you know some Flex and know how to make remote object calls to services, if you don’t make some comments and I will go more in detail on certain points. But below is the main Flex application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="horizontal" creationComplete="onCreationComplete(event)"> <mx:Script> <![CDATA[ import mx.events.ListEvent; import mx.utils.ObjectUtil; import mx.rpc.Fault; import com.unitedmindset.util.RubyErrorUtil; import mx.controls.Alert; import mx.collections.ArrayCollection; import com.unitedmindset.vo.PostVO; import com.unitedmindset.vo.CommentVO; import mx.events.FlexEvent; import mx.rpc.events.ResultEvent; import mx.rpc.events.FaultEvent; [Bindable] public var posts:ArrayCollection; private function onCreationComplete(event:FlexEvent):void { getPostsList(event); } private function getPostsList(event:Event=null):void { postRO.getOperation("index").send(); } private function onIndexResult(event:ResultEvent):void { posts = new ArrayCollection(event.result as Array); } private function onSaveResult(event:ResultEvent):void { var post:PostVO = event.result as PostVO; // find old post var found:PostVO; var i:int = -1; if(!posts) posts = new ArrayCollection(); var l:int = posts.length; while(++i<l){ var checkPost:PostVO = posts.getItemAt(i) as PostVO; if(post.id == checkPost.id){ found = checkPost; break; } } // end found post if(!found){ addTitleText.text = ""; addBodyText.text = ""; posts.addItem(post); } else { found.title = post.title; found.body = post.body; } } private function onDestroyResult(event:ResultEvent):void { var a:Array = posts.source; var i:int=-1; var l:int = a.length; while(++i < l){ if(a[i]==postsDG.selectedItem) a.splice(i,1); } ArrayCollection(postsDG.dataProvider).refresh(); } private function onCreateCommentResult(event:ResultEvent):void { var post:PostVO = postsDG.selectedItem as PostVO; if(!post.comments) post.comments = new Array(); post.comments.push(event.result as CommentVO); addCommentText.text = ""; commentsDG.dataProvider = new ArrayCollection(PostVO(postsDG.selectedItem).comments); } private function onFault(event:FaultEvent):void { Alert.show(event.fault.faultString,event.fault.name); } private function onAddPost(event:MouseEvent):void { var post:PostVO = new PostVO(); post.title = addTitleText.text; post.body = addBodyText.text; post.comments = new Array(); postRO.getOperation("save").send({post:post}); } private function onEditPost(event:MouseEvent):void { var post:PostVO = ObjectUtil.copy(postsDG.selectedItem) as PostVO; post.title = editTitleText.text; post.body = editBodyText.text; postRO.getOperation("save").send({post:post}); } private function onDeletePost(event:MouseEvent):void { var post:PostVO = postsDG.selectedItem as PostVO; postRO.getOperation("destroy").send({id:post.id}); } private function onCreateComment(event:MouseEvent):void { var post:PostVO = postsDG.selectedItem as PostVO; var comment:CommentVO = new CommentVO(); comment.body = addCommentText.text; comment.postId = post.id; commentRO.getOperation("save").send({post:post,comment:comment}); } private function onItemClick(event:ListEvent):void { if(postsDG.selectedItem) commentsDG.dataProvider = new ArrayCollection(PostVO(postsDG.selectedItem).comments); } ]]> </mx:Script> <mx:Style source="flex_blog_style.css"/> <!-- remote objects --> <mx:RemoteObject id="postRO" destination="rubyamf" endpoint="rubyamf/gateway" source="PostsController" showBusyCursor="true" fault="onFault(event)"> <mx:method name="index" result="onIndexResult(event)"/> <mx:method name="save" result="onSaveResult(event)"/> <mx:method name="destroy" result="onDestroyResult(event)"/> </mx:RemoteObject> <mx:RemoteObject id="commentRO" destination="rubyamf" endpoint="rubyamf/gateway" source="CommentsController" showBusyCursor="true" fault="onFault(event)"> <mx:method name="save" result="onCreateCommentResult(event)"/> </mx:RemoteObject> <!-- design --> <mx:VBox width="100%" height="100%"> <mx:Label x="10" y="10" text="Posts List" styleName="heading"/> <mx:DataGrid width="100%" height="100%" id="postsDG" dataProvider="{posts}" itemClick="onItemClick(event)"/> <mx:Label x="10" y="10" text="Selected Post Comments" styleName="heading"/> <mx:DataGrid width="100%" height="100%" id="commentsDG"/> </mx:VBox> <mx:Form> <mx:FormHeading label="Add Post"/> <mx:FormItem label="Title" required="true"> <mx:TextInput id="addTitleText"/> </mx:FormItem> <mx:FormItem label="Body" required="true"> <mx:TextArea id="addBodyText"/> </mx:FormItem> <mx:FormItem> <mx:Button label="Add Post" click="onAddPost(event)"/> </mx:FormItem> <mx:FormHeading label="Edit Post"/> <mx:FormItem label="Title" required="true"> <mx:TextInput id="editTitleText" text="{PostVO(postsDG.selectedItem).title}"/> </mx:FormItem> <mx:FormItem label="Body" required="true"> <mx:TextArea id="editBodyText" text="{PostVO(postsDG.selectedItem).body}"/> </mx:FormItem> <mx:FormItem> <mx:Button label="Edit Post" click="onEditPost(event)" enabled="{Boolean(postsDG.selectedItem)}"/> </mx:FormItem> <mx:FormHeading label="Delete Selected Post"/> <mx:FormItem> <mx:Button label="Delete Post" enabled="{Boolean(postsDG.selectedItem)}" click="onDeletePost(event)"/> </mx:FormItem> <mx:FormHeading label="Add Comment To Selected Post"/> <mx:FormItem label="Comment" required="true"> <mx:TextArea id="addCommentText"/> </mx:FormItem> <mx:FormItem> <mx:Button label="Add Comment" enabled="{Boolean(postsDG.selectedItem)}" click="onCreateComment(event)"/> </mx:FormItem> </mx:Form> </mx:Application> |
We will also add ValueObjects to reflect back to the ActiveRecord objects in Ruby. The first is the “Post”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package com.unitedmindset.vo { [RemoteClass(alias="PostVO")] [Bindable] public class PostVO { public function PostVO() { } public var id:int; public var title:String; public var body:String; public var createdAt:Date; public var updatedAt:Date; public var comments:Array; } } |
The second is the “Comment”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | package com.unitedmindset.vo { [RemoteClass(alias="CommentVO")] [Bindable] public class CommentVO { public function CommentVO() { } public var id:int; public var postId:int; public var body:String; public var createdAt:Date; public var updatedAt:Date; } } |
That’s it for our Flex. This is just a simple service tester to make sure our services work properly roundtrip, but you could easily dress this up and make it a real application.
Back to the Ruby.
WIthin the ‘config’ folder you will see that the rubyamf gem added to our config files the file ‘rubyamf_config.rb’. It is good to remember that any changes you make to this folder – as it is in your ‘config’ folder – requires a server restart for the changes to take.
The settings within my ‘rubyamf_config.rb’ are as follows, specifically notice the section where we map the classes between Ruby and Flex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | require 'app/configuration' module RubyAMF module Configuration #set the service path used in all requests # RubyAMF::App::RequestStore.service_path = File.expand_path(RAILS_ROOT) + '/app/controllers' # => CLASS MAPPING CONFIGURATION # => Global Property Ignoring # By putting attribute names into this array, you opt in to globally ignore these properties on incoming objects. # If you want to ignore specific properties on certain objects, use the :ignore_fields property in a # Class Mapping definition (see CLASS MAPPING DEFINITIONS) ClassMappings.ignore_fields = ['created_at','created_on','updated_at','updated_on'] # => Case Translations # Most actionscript uses camelCase instead of snake_case. Set ClassMappings.translate_case to true if want translations to occur. # The translations only occur on object properties # An incoming property like: myProperty gets turned into my_property # An outgoing property like my_property gets turned into myProperty ClassMappings.translate_case = true # => Force Active Record Ids # includes the id field for activerecord objects even if you don't specify it when using custom attributes. This is important for deserialization # where ids are needed to keep active record association integrity. # ClassMappings.force_active_record_ids = true # => Hash key access # You can choose how keys in hashes are created. As :string, :symbol, or :indifferent. # :string creates keys as hash['key'] # :symbol creates keys as hash[:key] # :indifferent uses rails HashWithIndifferentAccess so you can use hash[:key] of hash['key'] # There are performance issues with HashWithIndifferentAccess. Use :symbol or :string for best performance. # The default is :symbol # ClassMappings.hash_key_access = :symbol # => Assume Class Types # This tells RubyAMF to assume class type transfers. So when you register a class Alias from Flash or Flex like this: # Flash:: fl.net.registerClassAlias('User',User) # Flex:: [RemoteClass(alias='User')] # RubyAMF will automagically convert it to a User active record without you having to create a class mapping. # This also works with non active record class mappings. See the wiki on the google code page for a downloadable example. ClassMappings.assume_types = false # => Class Mapping Definitions # A Class Mapping definition conists of at least these two properties: # :actionscript # The incoming action script class to watch for # :ruby # The ruby class to turn it into # # => Optional value object properties: # :type # Used to spectify the type of VO, valid options are 'active_record', 'custom', (or don't specify at all) # :associations # Specify which associations to read on the active record (only applies to active records) # :attributes # Specifically which attributes to include in the serialization # :methods # An array of methods to call and place values in a similarly named attribute on the Actionscript Object (outgoing, or Rails => Actionscript only) # :ignore_fields # An array of field names you want to ignore on incoming classes # # If you are using ActiveRecord VO's you do not need to specify a fully qualified class path to the model, you can just define the class name, # EX: ClassMappings.register(:actionscript => 'vo.Person', :ruby => 'Person', :type => 'active_record') # # If you are using custom VO's you would need to specify the fully qualified class path to the file # EX: ClassMappings.register(:actionscript => 'vo.Person', :ruby => 'org.mypackage.Person') # # ClassMappings.register(:actionscript => 'Person', :ruby => 'Person', :type => 'active_record', :attributes => ["id", "address_id"]) # ClassMappings.register(:actionscript => 'User', :ruby => 'User', :type => 'active_record', :associations=> ["addresses", "credit_cards"]) # ClassMappings.register(:actionscript => 'Address', :ruby => 'Address', :type => 'active_record') # ClassMappings.register(:actionscript => 'User', :ruby => 'User', :type => 'active_record', :associations=> ["addresses", "credit_cards"], :methods => ["friends"]) # # => Class Mapping Scope (Advanced Usage) # You can also specify a class mapping scope if you want. For example, lets say you need certain attributes for a book when you are viewing a book # in flex as opposed to editing a book (where you would need more parameters). You can define a scope mapping parameter for ":attributes" # or for ":associations." You're mapping would look something like this. # ClassMappings.register( # :actionscript => 'com.mixbook.vo.books.BookVO', # :ruby => 'Book', # :type => 'active_record', # :associations => ["access_info", "pages", "page_ratio"], # :attributes => {:viewing => ["description", "title"], :editing => ["id","published_at","theme_id"] } <=== notice the hash instead of an array # # Now, to call the class mapping scope of editing (you are sending objects to the editing application), your controller call would look like this: # EX: render :amf => book, :class_mapping_scope => :editing # # You can also specify a default scope to use. If you don't set this and you don't specify a class mapping scope on an attribute or association, then # it will not have a scope to use and will not add any attributes or associations (whichever it cant match) to that association. # ClassMappings.default_mapping_scope = :viewing ClassMappings.register(:actionscript => 'PostVO', :ruby => 'Post', :type => 'active_record', :attributes => ["id", "title", "body","created_at","updated_at"], :associations=> ["comments"]) ClassMappings.register(:actionscript => 'CommentVO', :ruby => 'Comment', :type => 'active_record', :attributes => ["id", "body","post_id","created_at","updated_at"]) # => Date Conversion # Incoming dates from Flash by default are Time objects, this can conver to DateTime if needed # ClassMappings.use_ruby_date_time = false # => Use Array Collection # By setting this to true, you opt in to using array collections for all the arrays generated by the body of your request. # Note: This only works for amf3 with Remote Object, NOT with Net Connection. # ClassMappings.use_array_collection = false # => Check for Associations # Enabling this will automagically pick up eager loaded association data on objects returned through RubyAMF. # If this is disabled, you will need to specify any associations you DO want picked up in the ClassMapping # ClassMappings.check_for_associations = true # => NAMED PARAMETER MAPPING CONFIGURATION #=> Always Put Remoting Parameters into the "params" hash # If set to true, arguments from Flash/Flex will come in to the controllers as params[0], params[1], etc.. This is especally useful if you are sending huge objects # from Flex into Ruby so it doesnt eat up all your output window with outputting the params in the controller/action header information while in dev mode. # Even if its set to false, if you specify specific ParameterMappings, those will still get entered as the param keys you specify. Likewise, you # always have access to the parameters from rubyamf in your controller by calling rubyamf_params[0], rubyamf_params[1], etc regardless of # if it this is set or not. # ParameterMappings.always_add_to_params = true # => Return Top Level Hash # For those scaffolding users out there, who want the top-level object to come as a hash so scaffolding works out of the box. ParameterMappings.scaffolding = true # => Incoming Remoting Parameter Mappings # Incoming Remoting Parameter mappings allow you to map an incoming requests parameters into rails' params hash # # Here's an example: # ParameterMappings.register(:controller => :UserController, :action => :find_friend, :params => { :friend => "[0]['friend']" }) end end |
Now with our config file properly set we can make a slight change to the controllers so that the objects are consumed properly. The issue is that in Ruby from HTML, the Ruby expects just parameters from HTML and creates a new ActiveRecord object, Flex actually sends an ActiveRecord Object. Therefore the create and update functions will not work properly. That’s why we create a new function called save. You can see the PostsController save function below:
1 2 3 4 5 6 7 8 9 10 11 | # AMF Service def save @post = params[:post] respond_to do |format| if @post.save format.amf { render :amf => @post } else format.amf { render :amf => FaultObject.new(@post.errors.full_messages.to_a.join("n")) } end end end |
And now the CommentsController save function.
1 2 3 4 5 6 7 8 9 | # AMF Services def save @post = params[:post] @post.comments.push(params[:comment]) @comment = params[:comment] respond_to do |format| format.amf { render :amf => @comment } end end |
Honestly, that’s it. Your application will work with full AMF support all within 30 minutes or less.
My final project folder can be downloaded here.
ruby_fx_blog








I’m just starting to use AMF. I don’t have experience using calls thru RemoteObject other than just a controller’s index, returning all records.
I would like to make calls to controllers using associations:
i.e.: Client has_many Policies, so I need to get a result set of:
/clients/[id]/policies
How would I call that from Flex?
Currently Routes is already set up correctly and it’s returning appropriate record set from an HTML version of the call.
@michael The routes shouldn’t matter if you are accessing the data via AMF. You just need to send the correct vars to your controller to receive back the array of policies. Such as:
2
3
4
5
6
7
8
9
10
11
def get_client_policies_by_id
...
@client_id = params[:client_id]
@policies = #your activerecord code to get policies#
...
render :amf => @policies
end
end
And in Flex you would just get the result from your service.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<![CDATA[
private function _onSomeUserGesture(event:MouseEvent):void
{
service.send({client_id:selectedClient.id});
}
private function _onResult(event:ResultEvent):void
{
//do something with the result
}
]]>
</mx:Script>
<mx:RemoteObject id="service" destination="rubyamf" endpoint="..your/path/to/rubyamf/gateway" source="ClientController">
<mx:Method name="get_client_policies_by_id" result="onResult(event)"/>
</mx:RemoteObject>
HTH
[...] troch? wi?cej czasu i poszuka?em materia?ów w internecie. Natrafi?em na wpis w blogu Ruby on Rails with Flex i z jego pomoc? uda?o mi si? dotkn?? r?bka ‘RIA [...]
http://abhisheksoft.wordpress.com/2009/06/19/9/
Great post!
I tried moving this and applying it to a Flex 4 app, and ran into one snag. When I call save with this command:
itemRO.getOperation(“save”).send({item: item});
On the ruby backend to access the value I have to use param[0][:item]
Any ideas?
Good point, thanks for the add. I’ve found depending how you set up your config file there are many different things you may need to tweak, but the theory is the same, which is great!
Does your update of the blog post work? Mine never fires off the query to update. Adding new things works great, but when I try to update, the correct values go to the server, and the logging shows that it has the correct values, but for some reason calling @post.save (or @item.save in my case) returns true but doesn’t actually fire off the sql to update.
Found a bug entry for it on RubyAMF’s site, but was wondering if you had found a workaround.
http://code.google.com/p/rubyamf/issues/detail?id=120
As you can see I made a special “save” method on the controller rather than their original update method. This worked very well for me.
The big issue is that the update method expects the parameters and to create a new object, when in fact RubyAMF sends over an intact object with no new instantiation necessary.
I followed your design and created a save method. The issue I’m running across is that the object is getting into the save function, has the correct values from Flex, however when I call @post.save it returns true but doesn’t actually save to the database. The Flex interface then updates correctly, but if I refresh the page or relaunch the value gets pulled from the database and is the old value.
Yes… very odd. Makes me wonder if there is an issue with your Ruby. Does it work in console? Have you tried debugging in Ruby and seeing what happens? This isn’t normal.
I wonder if it has anything to do with the fact that I’m using sqlite3 vs yours using mysql… I’ll test that out. I’m not really familiar with ruby debugging… but I did log the events as it went through the save function and things appeared to go normal, but didn’t get saved to the database. It seems like for one reason or another to object just isn’t trying to save.
If I do a Post.find_by_id(params[:post].id) and then set the title and body on the post returned by that find to be equal to the ones returned through params, everything works fine… Its a odd bug.
Yup… thats the bug. If I switch from using sqlite3 to mysql things work like a charm. Thanks for the great post… hopefully others having this issue will be helped by the comments.
Very cool to know. I usually stick with Mysql or Postgres for my databases. Sqlite doesn’t support migrations.
I have doubts about the case. Are you Brazilian? Do you speak Portuguese?
I don’t speak Portuguese but I can get translations if it becomes a big issue. Please let me know what your issues are with this example.
Hello, I managed to solve the problem. It was not in code but in a setting that I had done. So far everything is ok, ask for help if any problem. Thankfully, Maria. Sorry for english I’m using the google translator.
No problemo. Entiendo que no todo el mundo habla Ingles.