BT

Building a WPF Application in IronRuby

Posted by Edd Morgan on May 25, 2010 |

In my previous post I introduced IronRuby. I expanded a little on the basics of IronRuby by explaining what little extra needs to be done to a Rails app to get it moving on this .NET implementation of the Ruby language — but there wasn't much to it! So now I would like to progress from talking about IronRuby's compatibility with your existing projects to developing a whole new application demonstrating the interoperability between IronRuby and .NET. In particular, we'll be using WPF (Windows Presentation Foundation) — the component of the .NET Framework stack used to create rich media and graphical interfaces.

Foundations of WPF

To reiterate, WPF is the engine in the .NET Framework responsible for rendering rich user interfaces and other media. It's not the only collection of libraries in the framework with the power to do this — Windows Forms does the trick, too — but WPF is useful when you need to employ eye candy and create impact. Whether you're presenting a document, video, a data entry form, some kind of data visualisation (which I am most hopeful for, especially in terms of IronRuby - more on that later) or chaining all of the above with some flashy animations, you're likely to find that WPF gives you what you need when developing any of these for a Windows target.

Let's demonstrate this with an example. One day, over my lunch break, I created a WPF-based analogue clock - what I like to consider the 'hello, world' of WPF applications - using IronRuby. Any normal person would have just looked at their watch.

Note: It may help to have the WPF documentation handy when working through this example.

The Sample Application

require 'WindowsBase'
require 'PresentationFramework'
require 'PresentationCore'
require 'System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'

class Clock

    CLOCK_WIDTH     = 150
    CLOCK_HEIGHT    = 150
    LABEL_HEIGHT    = CLOCK_HEIGHT / 7
    LABEL_WIDTH     = CLOCK_WIDTH / 7
    RADIUS          = CLOCK_WIDTH / 2
    RADS            = Math::PI / 180
    MIN_LOCATIONS   = {}
    HOUR_LOCATIONS  = {}

    def run!
        plot_locations

        # build our window
        @window = System::Windows::Window.new
        @window.background = System::Windows::Media::Brushes.LightGray
        @window.width = CLOCK_WIDTH * 2
        @window.height = CLOCK_HEIGHT * 2
        @window.resize_mode = System::Windows::ResizeMode.NoResize
        
        @canvas = System::Windows::Controls::Canvas.new
        @canvas.width = CLOCK_WIDTH
        @canvas.height = CLOCK_HEIGHT

        # create shapes to represent clock hands
        @minute_hand = System::Windows::Shapes::Line.new
        @minute_hand.stroke = System::Windows::Media::Brushes.Black
        @minute_hand.stroke_thickness = 1
        @minute_hand.x1 = CLOCK_WIDTH / 2
        @minute_hand.y1 = CLOCK_HEIGHT / 2

        @hour_hand = System::Windows::Shapes::Line.new
        @hour_hand.stroke = System::Windows::Media::Brushes.Black
        @hour_hand.stroke_thickness = 3
        @hour_hand.x1 = CLOCK_WIDTH / 2
        @hour_hand.y1 = CLOCK_HEIGHT / 2
        
        # .. and stick them to our canvas
        @canvas.children.add(@minute_hand)
        @canvas.children.add(@hour_hand)
        
        plot_face # draw a clock face
        plot_labels # draw clock numbers
        plot_hands # draw minute / hour hands

        @window.content = @canvas
        app = System::Windows::Application.new
        app.run(@window)
        # the Application object handles the lifecycle of our app
        # including the execution loop
    end
    
    # determine 2 sets of equidistant points around the circumference of a circle
    # of CLOCK_WIDTH and CLOCK_HEIGHT dimensions.
    def plot_locations
        for i in (0..60) # 60 minutes, and 12 hours
            a = i * 6
            x = (RADIUS * Math.sin(a * RADS)).to_i + (CLOCK_WIDTH / 2)
            y = (CLOCK_HEIGHT / 2) - (RADIUS * Math.cos(a * RADS)).to_i
            coords = [x, y]
            HOUR_LOCATIONS[i / 5] = coords if i % 5 == 0 # is this also an 'hour' location (ie. every 5 minutes)?
            MIN_LOCATIONS[i] = coords
        end
    end
    
    # draws a circle to represent the clock's face
    def plot_face
        extra_x = (CLOCK_WIDTH * 0.15) # pad our circle a little
        extra_y = (CLOCK_HEIGHT * 0.15)
        face = System::Windows::Shapes::Ellipse.new
        face.fill = System::Windows::Media::Brushes.White
        face.width = CLOCK_WIDTH + extra_x
        face.height = CLOCK_HEIGHT + extra_y
        face.margin = System::Windows::Thickness.new(0 - (extra_x/2), 0 - (extra_y/2), 0, 0)
        face.stroke = System::Windows::Media::Brushes.Gray # give it a slight border
        face.stroke_thickness = 1 
        System::Windows::Controls::Canvas.set_z_index(face, -1) # send our circle to the back
        @canvas.children.add(face) # add the clock face to our canvas
    end
    
    # at each point along the hour locations, put a number
    def plot_labels
        HOUR_LOCATIONS.each_pair do |p, coords|
            unless p == 0
                lbl = System::Windows::Controls::Label.new
                lbl.horizontal_content_alignment = System::Windows::HorizontalAlignment.Center
                lbl.width = LABEL_WIDTH
                lbl.height = LABEL_HEIGHT
                lbl.content = p.to_s
                lbl.margin = System::Windows::Thickness.new(coords[0] - (LABEL_WIDTH / 2), coords[1] - (LABEL_HEIGHT / 2), 0, 0)
                lbl.padding = System::Windows::Thickness.new(0, 0, 0, 0)
                @canvas.children.add(lbl)
            end
        end
    end
    
    def plot_hands
        time = Time.now
        hours = time.hour
        minutes = time.min
        
        if !@minutes || minutes != @minutes
            @hours = hours >= 12 ? hours - 12 : hours
            @minutes = minutes == 0 ? 60 : minutes
            # Dispatcher.BeginInvoke() is asynchronous, though it probably doesn't matter too much here
            @minute_hand.dispatcher.begin_invoke(System::Windows::Threading::DispatcherPriority.Render, System::Action.new {
                @minute_hand.x2 = MIN_LOCATIONS[@minutes][0]
                @minute_hand.y2 = MIN_LOCATIONS[@minutes][1]
                @hour_hand.x2 = HOUR_LOCATIONS[@hours][0]
                @hour_hand.y2 = HOUR_LOCATIONS[@hours][1]
            })
        end
    end
    
end

clock = Clock.new

timer = System::Timers::Timer.new
timer.interval = 1000
timer.elapsed { clock.plot_hands }
timer.enabled = true

clock.run!

Click here to see this sample on GitHub.

Let it be said that this is by no means perfect, but I think it does the job of illustrating IronRuby's interoperability with WPF using a familiar data visualisation. I'm sure you'll want to dissect the code yourself, but allow me to step through the important bits. (By the way, feel free to run this through ir first to see what actually happens).

Now we're using IronRuby - unlike my previous post where we took pure Ruby code and ran it through ir, the IronRuby interpreter, to demonstrate compatibility. The main thing of note is, of course, the very distinct parallels between .NET namespaces and Ruby modules, .NET classes and Ruby classes. I guess there's not much to say about it other than at this point, you may as well be working with a purely Ruby graphics-drawing library.

You're instantiating .NET objects, but you're doing it with the standard Ruby .new method you know from Ruby as Object#new. You're calling methods on these objects (and classes, for example in the call to System.Windows.Controls.Canvas.SetZIndex()) using the underscored, lowercase convention established for the Ruby language. The integration is so seamless, the surprise of the fact that you're using a dynamic language on top of .NET's CLR (Common Language Runtime, which needs some help from the Dynamic Language Runtime to support dynamic languages) is completely abstracted from you, allowing you to just build your software.

Note: When working with IronRuby, the .NET stack really does integrate at all levels. One thing to note is that root object of all your IronRuby objects isn't actually Object, its System.Object.

Events

Events are a big part of developing client applications in .NET as well as under every other environment I can think of. In case you aren't aware, event-driven programming is essentially the practice of telling your code to call a particular method, or other chunk of code (a delegate) when something happens at an unpredictable time. You can never predict when a user is going to click a button, strike a key or perform any other kind of input, so the advent of the GUI is what neccessitated event-driven programming.

This is where one of my favourite aspects of the Ruby language, blocks, can really help us. In traditional C#, for instance, you may subscribe to an event (assign a block of code to execute when an event occurs) in one of two ways: by passing a reference to a named method, or by providing an anonymous code block. You'd be right for seeing the parallel with Ruby's concept of blocks, Procs and lambdas here. As demonstrated at the very end of this rather basic script, we are using .NET's System.Timers.Timer to (attempt to) update the clock every second (I know it's probably not the best way of doing this, but for example's sake).

Note: Diverting a little from what I said above, the ticking of a clock is very predictable, yet we still use the event our Timer throws to do this updating as one of many ways to perform that task outside of the main thread.

You'll see that all that's needed to assign a block of code to be triggered on an event is to provide that block to the method of the name of the event as it is known to the CLR. This drawback to this is that it only allows the delegation of one code block to each event. You may use the add method to subscribe multiple handlers to that event - pushing that to the end of a queue. Like so:

def tick
    puts "tick tock"
end

timer.elapsed.add method(:tick)
timer.elapsed.add proc { puts "tick tock" }
tick_handler = lambda { puts "tick tock" }
timer.elapsed.add(tick_handler)

The ability to just provide a block of code as an event handler helps IronRuby towards a concept that is important to a good dynamic language; low ceremony — reducing the amount of boilerplate code. Anonymous methods are, of course, available in other more conventional .NET languages such as C# and VB but, as usual, feel ever so much more elegant and natural in IronRuby.

Note: Whether it's a named method or an anonymous chunk o' code, the block you delegate to the handling of an event can take arguments - commonly, a sender object and some args.

XAML & IronRuby

XAML is Microsoft's XML-like language for defining CLR objects and their properties mainly for use in WPF and Silverlight applications. With it, you can declaratively create an entire UI, complete with hook-ups to events in your procedural code and bindings to data only present at run-time; create graphics; and even storyboard animations for those graphics. I'm not going to go into detail on how XAML is structured, but if you have any experience with XML-based languages you should have no problem understanding what's happening.

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Rectangle x:Name="mySquare" Width="50" Height="50">
            <Rectangle.Fill>
                <SolidColorBrush Color="Green" />
            </Rectangle.Fill>
        </Rectangle>
        <TextBlock Text="Hello, world">
            <TextBlock.Foreground>
                <SolidColorBrush Color="Red" />
            </TextBlock.Foreground>
        </TextBlock>
    </StackPanel>
</Window>

Note: Window, StackPanel, TextBlock, SolidColorBrush and Rectangle are all WPF classes. Anything you write in your XAML code can just as easily be implemented programmatically in C#, VB or indeed IronRuby.

This code represents a solitary window of indeterminate size. Inside that window is a certain object called a "StackPanel", which is a type of WPF control used to define how it's child controls will flow in terms of layout. Inside that StackPanel are two different objects: a block of text (TextBlock) and a rectangle shape. Objects defined in XAML can be either named for later reference, or anomymous (our Rectangle is named 'mySquare', whereas the TextBlock has not been given a name). These objects' properties can also be given values in two ways: as XML element attributes (e.g. Width="50") or as their own child elements when the expected value is of a non-primitive type (e.g. <Rectangle.Fill> expects a Brush (or a derivative of Brush)).

Without going any further into the labyrinth that is WPF and XAML, because one could easily write volumes on it, let's run this with IronRuby.

require 'PresentationFramework'
require 'PresentationCore'

@window = System::Windows::Markup::XamlReader.parse(File.open('my_xaml.xaml', 'r').read)
System::Windows::Application.new.run(@window)

WPF's Application.Run method takes a Window as it's argument. Looking back to our XAML, you can see that our root element is a Window, and that is what gets returned after parsing. All of the controls defined within the XAML are returned as a tree of controls reflecting the structure of the XAML document, with the Window as the root, StackPanel as the only child of Window, Rectangle and TextBlock as the children of StackPanel, etc. You can also address named controls like so:

@window.find_name("mySquare").class # => "System::Windows::Shapes::Rectangle"

A Side-note on Inheriting from CLR Classes

We've covered compatibility, interoperability but only briefly extendability. I think I have made clear how closely IronRuby integrates with .NET - even to the point where you can extend your CLR classes using inheritance. To demonstrate this, I'd like to revisit the Person class we built in C# in my previous article.

namespace MyClassLibrary
{
    public class Person
    {
        public string Name { get; set; }

        public string Introduce()
        {
            return String.Format("Hi, I'm {0}", Name);
        }
    }
}

Let's extend it using Ruby, and model a programmer and their drinking habits.

require 'MyClassLibrary.dll'

class Programmer < MyClassLibrary::Person
    ACCEPTABLE_DRINKS = [:coffee, :tea, :cola, :red_bull]

    def drink(liquid)
        if ACCEPTABLE_DRINKS.include? liquid
            puts "Mmm... #{name} likes code juice!"     
        else
            raise "Need caffeine!"
        end
    end
end

me = Programmer.new
me.name = "Edd"
puts me.introduce
me.drink(:coffee)

A Brief Note on Verbosity

Personally, I don't mind verbose chaining of references in my code as long as it doesn't interfere with performance - as evidenced in the example above. While I love clean code, there's a certain feeling of safety that comes with the terse explicitness of long-winded addressing and the describing of objects as opposed to ambiguity (not unlike this sentence). However, when working with IronRuby, even I grow tired of typing System::Whatever::Something. Some people enjoy simply assuming namespaces and forgetting about them, regardless of the language they're using. Don't worry, IronRuby has those guys covered.

Because .NET namespaces are modules in IronRuby it is completely possible to, with a call to include, bring the contents of a .NET namespace into context of your IronRuby code - just as you would if you wanted to bring in an 'organic' Ruby module. To refactor the style of the above example, I could place the following at the top of my Clock class:

class Clock
    include System::Windows::Shapes
    include System::Windows::Media
    include System::Windows::Threading
    # and so on...

And by doing so, reduce calls to System::Windows::Shapes::Ellipse.new to simply Ellipse.new or references to System::Windows::Threading::DispatcherPriority.Render to a friendlier DispatcherPriority.Render.

Another way to DRY up your IronRuby code and deal with these very long names that occur frequently in the .NET framework is by using namespace aliasing, achievable with an assignment.

require 'System.Windows.Forms'

WinForms = System::Windows::Forms
WinForms::Form.new
WinForms::Label.new

Conclusion

I hope by now you can understand better how IronRuby interoperates with .NET and how you can harness the .NET framework with the dynamic nature and elegant idioms of the Ruby language.

The manner and parlance of Ruby that makes it a joy to work with sets of data is, of course, present in IronRuby — couple that with WPF's capability to produce graphics and I hope you can visualise the possibilities of data visualisation using these two things. Using IronRuby to create visual representations of data and infographics is very exciting to me. Although today, with this project, we're only presenting one simple piece of information - the time - the potential is grand.

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

IronRuby is interesting again by Balaji Mohandas

Thanks for the article, I feel .NET application development using IRuby is a breeze.

Re: IronRuby is interesting again by Edd Morgan

Great, I'm glad you liked it. Personally, I think .NET development itself is a breeze across the board, but it's great to be able to approach solutions in a dynamic fashion with things like IronRuby and IronPython.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT