WPF Sample in IronRuby talking via C# to Wesabe
John Lam and team have released their first drop the IronRubySource code. It's super-pre-alpha so don't hate, but it's looking pretty sweet, if you're a burgeoning Ruby-head like myself. There's incomplete support for some of the cooler Ruby syntax things, but I have full confidence that it's only going to get better. All the underpinnings are there.
John also announce that the team will host IronRuby source on Rubyforge (which is quite the coup, actually) and that they'll be taking external contributions very soon (this being the open part of open source.)
Not every sample (initially) will look and feel and smell good like Ruby, and this one is no exception. You might remember that last week I did a quicky Wesabe client in C# and put it up on Google Code. We've got four folks up there improving the code, which is cool.
Today, Shady the intern and I decided to do a sample in IronRuby that would call the C# Wesabe client API and display some account data via WPF. This is part of my new plan to take over Money and Quicken's business and you can tell from the hours of intense UI design that Shady and I did, that my plan is inevitable.
We started from ScottGu's HelloWorld sample, and slogged creatively coded our way forward. Here's how it went.
First, get the IronRuby source and upzip. Open on Visual Studio 2005 command prompt and compile the DLR and IronRuby by running build.cmd.
All the interesting stuff will show up in bin/release, including the IronRuby equivalent of IRB, the interactive Ruby Interpreter. You can run this from the command line and try things out interactively if you like.
We used Notepad2 with Ruby Support for our editor, although I'm deeply digging "e" the TextMate for Windows and will likely move over to it for my text editor of choice when I start at Microsoft in September.
Next, we tried to add a require statement to bring in the Wesabe library. You run a IronRuby app (today, in this build) by running rbx.exe YourApp.rb. We put our .rb text file in one folder and had rbx.exe in another, so when we added our wesabelib.dll we had to add it to the same folder that rbx.exe was in, otherwise the Assembly load would fail. You can also put things in the GAC and get to them there. Just remember that Fusion will load from the same folder rbx.exe is in, not the folder that your program is in.
For debugging, we used the classic "got here" debugging of old, via the standard MessageBox, so we added a requre for Windows Forms also and a constant as a alias for the MessageBox class:
require 'System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' require 'wesabelib, Version=1.0.0.0' MessageBox = System::Windows::Forms::MessageBox
As this build is a pre-Alpha, likely created in the dark of the night just hours ago, there's all sorts of weirdness and stuff that didn't "just work."
The naming integration with .NET is persnickety at best. The C# Wesabe client had a lowercase class called "wesaberest" that IronRuby just couldn't see, so I changed it to uppercase and aliased it with a constant.
#this build of IronRuby doesn't like Class Names that are lowercase, # so I made mine Uppercase Wesabe = Wesaberest
As I said, The Wesabe client API that we created is written in C#, and returns a array of Account objects. In the click event of a button, we pass in the username and password from our TextBox and PasswordBox respectively and store our Account object.
Our goal is to spin through the list of Account objects in the Array, but there's a few gotchas, again because of the pre-alphaness. First, we can't use for because in Ruby for is a sugar syntax on top of "each" that assumes an each method exists. Each isn't implemented yet, but the plan is to have for syntax just work over IEnumerable things.
Second, we can't index into arrays because the [] operator isn't implemented yet, but we can call GetValue(), which is the same thing.
#in the future we'll be able to index into CLR arrays... #oneAccount = myAccounts[i] oneAccount = myAccounts.GetValue(i)
Thirdly, in IronRuby there's FixNum, which is a DLR extension of Integer and there's MutableStrings, that have private StringBuilder. A lot of this will be hidden in the future, but in this build I had to be aware of some of the impedance mismatches as I tried to coerce my numbers into strings for output.
For example, none of these worked, but I expect they will one day.
Lastly, currentBalance is a float (System.Single) and IronRuby wouldn't respect it's ToString method, so I changed the value to a System.Double and all was fine.
Here's some failed attempts at String Concatenation. Notice the "secret" call to to_clr_string. Those should all go away and the Ruby types should marshal cleanly in most cases. Note that I'm assuming a lot, and in some cases basing these assumptions on my chats with John, but also as educated guesses as to how it ought to work.
#Ruby string concatenation works fine... my_message.content = "scott" + " hanselman" #Attempt0 my_message.content = oneAccount.name + oneAccount.currentBalance.to_string("C").to_clr_string #Attempt1 my_message.content = oneAccount.name + "$" + oneAccount.currentBalance.to_s.to_clr_string #Attempt2 my_message.content = System::String.Concat("$" + oneAccount.currentBalance.to_s.to_clr_string) #Attempt3 #There's a name conflict when trying to use the StringBuilder: # System.MemberAccessException: uninitialized constant ScriptModule::Text s = System::Text::StringBuilder.new s.Append(oneAccount.name) s.Append("$") s.Append(oneAccount.currentBalance.to_s.to_clr_string) my_message.content = s.to_string
At this point, I just added a new property called "DisplayString" in the Wesabe Account class and moved on. As for the loop we used a while. It's not clean, but it's clean enough:
my_button.click do |sender, args| a = Wesabe.getAccounts(text_user.text,text_password.password) myAccounts = a.Items i = 0 accounts_listbox.items.clear while (i < a.Items.Length) oneAccount = myAccounts.GetValue(i) #GetValue is on System.Array i+=1 #needed for the while # Create new label my_message = Label.new my_message.font_size = 36 my_message.content = oneAccount.DisplayString #Add the Label to the ListBox accounts_listbox.items.add(my_message) end end
There's a couple of ways to make this even more clean, and those ways will come. I'll update this sample as the new features roll into IronRuby.
I think it'd be nice to add some charts and graphs, make the whole thing a ClickOnce app, etc. Probably, for now, it'll be easier in C#, but it was fun to hack it together in today's drop of IronRuby and the interns learned something.
You can get my source for this Ruby App, along with a custom build of the Wesabe client (with my changes) here. Remember to get the IronRuby source, compile it, then copy the Wesabe client to the bin\release folder (or move the .rb files into there along with the Wesabe dll). I've left all the comments in the source for fun.
Download Wesabe IronRuby WPF Client Sample
About Scott
Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.
About Newsletter
Ruby 1.8.6-25, when presented with this:
class lowercaseclass
def do_something_runnable
puts "hello"
end
end
c = lowercaseclass.new
c.do_something_runnable
says this:
class/module name must be CONSTANT
So IronRuby is behaving correctly. Which is what I'm hoping to see continue. It's going to be a fun ride.
E.g. when calling into .NET classes I pass in the Python int type, and it is marshaled to Int32 without me having to think.
Regarding case: I forgot about that Ruby quirk! I guess I'm so used to camel casing it never occurred that people may still lower case their class names.
Again taking a leaf from IronPython, you add a reference to an assembly, and then import the namespace. Case of classes is preserved. I sure don't want to have to mangle System.Xml.XmlDocument into system.xml.xmlDocument in my head in order to be able to create an instance of it in IronRuby :) In the case of a lower case class name : Be able to register a handler to map the .NET class name to a Ruby-valid class name, with this handler being called on as classes are being defined?
E.g.
IronRuby.lowercase_class_name_mapper = proc do |lower_case_class_name|
lower_case_class_name.classify
end
See http://api.rubyonrails.com/classes/Inflector.html#M001088 for an example of a #classify() method added to the Ruby String class. It converts things like "some_lower_case_name" to "SomeLowerCaseName".
Using the handler way, you don't have a hard & fast rule that is unchangeable, yet you can have a default handler that generates camel cased names.
Leaving naming conflicts as an exercise for the user to do in their own custom handler (however, open classes in Ruby may mean you just get unexpected behaviour as someone's "foo" from the newly required library goes and add methods to already existing "Foo".
E.g. if you cared about strong names and the assembly version, you'd use the long form.
I don't know (but I'm by no means a Ruby expert) if this has really arisen as an issue before. Typically (or more often than that) we're not used to dealing directly with classes, but with API function calls.
More fun things to look forward to!
...
my_window.content = LoadXaml("test.xml")
....
I got method missing. I really appriciate if you ever get time to create a small example of xaml in IronRuby.
Thanks again for the great example to start developing on WPF.
Comments are closed.
http://wiki.netbeans.org/wiki/view/RubyBuildInstructions
http://blogs.sun.com/tor/category/Ruby