Tuesday, July 29, 2008

A quick look at Silverlight with IronRuby

This post presents a little Silverlight program written using IronRuby that exercises features such as graphics, dynamic language interaction and style separation.

This is the second of series of posts that explore features across different platforms such as JavaFX Script or Flex. The first post can be found here.

Code is this post is based on Silverlight 2 Beta 2.

The example

As described in the previous post:


A little program that calculates and plots a polynomial that touches four points selected by the user. The user can move any of the given points in the screen and the polynomial will be recalculated and replotted while moving it.


The Neville's algorithm is used in order to find the polynomial that touches all the given points.

Dynamic languages and Silverlight

The Silverlight web site provides documentation and examples to get you started using dynamic languages with Silverlight also the Silverlight Dynamic Languages SDK provides examples and useful code templates.

However, most of the documentation that talks about specific features of Silverlight focused in working with C#.

One of the thing that I wanted to explore was to use the data binding feature, however I didn't find a way to use it in conjunction with IronRuby classes.

John Lam's blog is a good place to find specific samples for working with IronRuby and Silverlight.

One of the benefits of working with dynamic languages and Silveright is that it's very easy to just start trying examples and experiment with it. You only need the Silverlight 2 SDK beta 2 and any text editor (no Visual Studio is required). The Chiron local web server is a very useful tool that let's you execute the Silverlight program and let's you see the changes just by just refreshing the page.


The program

The final program looks like this:



The running version of this program can be found here.

Polynomial class

The following program shows the implementation of the Polynomial class.

class Polynomial
def initialize(coefficients)
@coefficients = coefficients
end
def evaluate(x)
i = @coefficients.length - 1
result = 0.0
while (i >= 0)
result += (x**i)*@coefficients[i]
i = i - 1
end
return result
end

def coefficients
@coefficients
end


def add(p)

if @coefficients.length > p.coefficients.length
c1 = @coefficients
c2 = p.coefficients
else
c1 = p.coefficients
c2 = @coefficients
end
length1 = c1.length
length2 = c2.length

result = (0..length1-1).map do |i|
if i < length2
c1[i]+c2[i]
else
c1[i]
end
end

return Polynomial.new(result)
end

def add_destructive(p)

if @coefficients.length > p.coefficients.length
c1 = @coefficients
c2 = p.coefficients
else
c1 = p.coefficients
c2 = @coefficients
end

length2 = c2.length

(0..length2-1).each do |i|
c1[i] = c1[i]+c2[i]
end

return Polynomial.new(c1)
end


def multiply(p)
results = Array.new(p.coefficients.length+@coefficients.length - 1,0.0)
current_exponent = 0
p.coefficients.each do |coefficient|
if coefficient.abs > 0.000001
current_self_exponent = 0
@coefficients.each do |self_coefficient|
results[current_self_exponent+current_exponent] = (self_coefficient*coefficient) + results[current_self_exponent+current_exponent]
current_self_exponent = current_self_exponent + 1
end
end
current_exponent = current_exponent + 1
end

Polynomial.new(results)
end
end


This class contains a couple of methods for polynomial evaluation, addition and multiplication. These operations are required to implement the algorithm.

Algorithm

The following code shows the implementation of Neville's algorithm.


class NevilleCalculator
def initialize(converter)
@converter = converter
end
def create_polynomial(points)
number_of_points = points.length
matrix = (0..number_of_points-1).map {|i|(0..i).map{|j| Polynomial.new([])}}
(0..number_of_points-1).each do |i|
matrix[i][0] = Polynomial.new([@converter.convert_to_plane_y(points[i].y)])
end
xs = points.map {|point| @converter.convert_to_plane_x(point.x) }

(1..number_of_points-1).each do |i|
(1..i).each do |j|
q = xs[i]-xs[i-j]
p1 = Polynomial.new [-1.0*xs[i-j]/q,1.0/q]
p2 = Polynomial.new [xs[i]/q,-1.0/q]
matrix[i][j] = p1.multiply(matrix[i][j-1]).add_destructive(p2.multiply(matrix[i-1][j-1]))
end
end

return matrix[number_of_points-1][number_of_points-1]
end
end


The create_polynomial method receives an array of user selected points in the screen. The converter object (described below) converts coordinates from the screen to the Cartesian coordinate system.

Screen to plane coordinate conversion

In order to convert the coordinates the Converter class was used.


class PlaneToScreenConverter
def initialize(screen_max_x,screen_min_x,screen_max_y,screen_min_y,
plane_max_x,plane_min_x,plane_max_y,plane_min_y)
...
@m_to_plane_x = ((@plane_max_x - @plane_min_x)/(@screen_max_x - @screen_min_x))
@b_to_plane_x = @plane_min_x- @screen_min_x*@m_to_plane_x

@m_to_plane_y = ((@plane_max_y - @plane_min_y)/(@screen_max_y - @screen_min_y))
@b_to_plane_y = @plane_min_y - @screen_min_y *@m_to_plane_y

end

...

def convert_to_screen_x(x)
m = ((@screen_max_x - @screen_min_x)/(@plane_max_x - @plane_min_x))
b = @screen_min_x - @plane_min_x*m
return (m*x + b)
end
def convert_to_screen_y(y)
m = ((@screen_max_y - @screen_min_y)/(@plane_max_y - @plane_min_y))
b = @screen_min_y - @plane_min_y*m
return (m*y + b)
end
def convert_to_plane_x(x)
return (@m_to_plane_x*x + @b_to_plane_x)

end
def convert_to_plane_y(y)
return (@m_to_plane_y*y + @b_to_plane_y)
end
end


Also a class to represent a user selected point was created:


class ScreenPoint
def x
@x
end

def x=(v)
@x = v
end

def y
@y
end

def y=(v)
@y = v
end
def initialize(x,y)
@x = x
@y = y
end
end


For some reason I couldn't use attr_accessor with the version of IronRuby shipped with the Silverlight SDK 2 Beta 2.

Plotting the polynomial

In order to plot the polynomial a Polyline object will be used. The FunctionPlotter class is used to create the list of points to create the list of Polyline points.


class FunctionPlotter
def initialize(converter,steps,polynomial)
@converter = converter
@steps = steps
@polynomial = polynomial
@points = []
end

def points
@points
end
def polynomial
@polynomial
end
def recalculate_points

increment = ((@converter.plane_max_x - @converter.plane_min_x)/@steps).abs
current_x = [@converter.plane_max_x,@converter.plane_min_x].min
@points = (0..@steps - 1).map do
x = current_x
y = @polynomial.evaluate(x)
current_x = current_x+increment
[x,y]
end
end
end


Dragging elements

Since the user can modify the points across the screen, a way to drag shapes is required. The Implementing Drag-and-Drop Functionality with Mouse Events and Capture (Silverlight 2) article provides a complete guide on how add dragging capabilities to a Silverlight shape.

Here's the implementation of a helper class that implements this functionality.


class ElementDraggingTracker
def is_captured
return @captured
end
def initialize
@captured = false
@last_position = ScreenPoint.new(0,0)
end

def handle_mouse_down(sender,e)
current_pos = e.GetPosition(nil)
@last_position.x = current_pos.X
@last_position.y = current_pos.Y
@captured = true
sender.CaptureMouse()
end
def handle_mouse_move(sender,e)
if @captured
new_position = e.GetPosition(nil)
deltav = new_position.Y - @last_position.y
deltah = new_position.X - @last_position.x
sender.Data.Center = Point.new(sender.Data.Center.X+deltah,sender.Data.Center.Y+deltav)
@last_position.x = new_position.X
@last_position.y = new_position.Y
end
end
def handle_mouse_up(sender,e)
sender.ReleaseMouseCapture()
@captured = false
end
end


This implementation was made to work only with Path objects that have an EllipseGeometry as the Data. That's why the Data and Center properties are used. This code could be improved to be independent of the object being dragged.

Presenting the polynomial

In order to show a textual representation of the calculated polynomial, a sequence of TextBlock objects was used. This was necessary since I wanted to show the exponent of each term of the polynomial. The only way I found to do that was to add an horizontal StackPanel and add TextBlock for each term and use a TranslateTransform for the exponents. Here's the code:


def create_polynomial_text_elements(p)
first_time = false
panel = polyTextPanel

panel.Children.Clear
length = p.coefficients.length
(0..length-1).each do |pos|
i = (length - 1) - pos
c = p.coefficients[i]
exponent = i

if c.abs > 0.000001 then
text = ""
text = text + ("%.4g" % c)
text = text + "x" if exponent > 0

b = create_text_block text,"FormulaTextStyle"
panel.Children.Add b
if exponent > 1
b = create_text_block exponent.to_s,"FormulaExponentTextStyle"
t = TranslateTransform.new
t.Y = -2
b.RenderTransform = t
panel.Children.Add b
end

if !first_time and exponent != 0
b = create_text_block " + " ,"FormulaTextStyle"
panel.Children.Add b
else
first_time = false
end
end

end
end
end


The main program

The XAML for this program is very simple:


<UserControl x:Class="System.Windows.Controls.UserControl"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Name="ucontrol">
<UserControl.Resources>

...

</UserControl.Resources>
<StackPanel>
<StackPanel x:Name="polyTextPanel" Orientation="Horizontal">
</StackPanel>
<Canvas x:Name="theCanvas" Width="480" Height="480" Background="White">
<Polyline x:Name="polynomialPolyline" Style="{StaticResource PolylineStyle}"/>
<Line Style="{StaticResource AxisLineStyle}" X1="0" Y1="240" X2="480" Y2="240"/>
<Line Style="{StaticResource AxisLineStyle}" X1="240" Y1="0" X2="240" Y2="480"/>
</Canvas>
</StackPanel>
</UserControl>


The App class implements the entry point of the program:


require "Silverlight"
require 'screen_point'
require 'element_dragging_tracker'
require 'plane_to_screen_converter'
require 'neville_calculator'
require 'function_plotter'
require 'polynomial'

include System::Windows
include System::Windows::Controls
include System::Windows::Media
include System::Windows::Shapes



class App < SilverlightApplication
use_xaml

...

end

App.new


The SilverlightApplication class and the referenced Silverlight.rb file are provided as part of a template for IronRuby/Silverlight project from the Silvelight Dynamic SDK.

The initialize method of the App class looks like this:


def initialize
(0..(480/24)).each do |i|
theCanvas.Children.Add create_line(i*24,0,i*24,480,"GridLineStyle")
end
(0..(480/24)).each do |i|
theCanvas.Children.Add create_line(0,i*24,480,i*24,"GridLineStyle")
end

@points = [ScreenPoint.new(100,300),
ScreenPoint.new(200,400),
ScreenPoint.new(350,300),
ScreenPoint.new(400,200)
]

@converter = PlaneToScreenConverter.new(480.0,0.0,0.0,480.0,20.0,-20.0,20.0,-20.0)

@tracker = ElementDraggingTracker.new

canvas = theCanvas

@points.each do |p|
c = create_circle p.x,p.y,10,"ControlPointStyle"
t = create_text_block "","PointTextStyle"
adjust_point_text_block(p,t)

c.mouse_left_button_down do |sender,args|
@tracker.handle_mouse_down(sender,args)
end
c.mouse_left_button_up do |sender,args|
@tracker.handle_mouse_up(sender,args)
p.x = sender.Data.Center.X
p.y = sender.Data.Center.Y
adjust_point_text_block(p,t)
replot
end
c.mouse_move do |sender,args|
@tracker.handle_mouse_move(sender,args)
if @tracker.is_captured
p.x = sender.Data.Center.X
p.y = sender.Data.Center.Y
adjust_point_text_block(p,t)

replot
end
end
canvas.Children.Add c
canvas.Children.Add t
end

replot
end


Two important things happen in this method. First the grid lines are added to the canvas and then the control points are added to the canvas and the dragging event handlers are also associated with each point. The event handlers also take care of synchronizing the update of each point's label and to recalculate and re-plot the polynomial.

The replot method executes the Neville's algorithm and create the new points for the Polyline.


def replot
@calc = NevilleCalculator.new(@converter)

@plotter = FunctionPlotter.new(@converter,200,@calc.create_polynomial(@points))

@plotter.recalculate_points

pc = PointCollection.new
@plotter.points.each do |aP|
pc.Add(Point.new(@converter.convert_to_screen_x(aP[0]),
@converter.convert_to_screen_y(aP[1])))
end

polynomialPolyline.Points = pc
create_polynomial_text_elements @plotter.polynomial
end


Additional methods for creating Silverlight shapes were required:


def create_line(x1,y1,x2,y2,style_key)
p = Path.new
p.Data = LineGeometry.new
p.Data.StartPoint = Point.new x1,y1
p.Data.EndPoint = Point.new x2,y2
p.Style = ucontrol.Resources.Item(style_key.ToString())
return p
end

def create_circle(x1,y1,radius,style_key)
p = Path.new
p.Data = EllipseGeometry.new
p.Data.Center = Point.new x1,y1
p.Data.RadiusX = radius
p.Data.RadiusY = radius
p.Style = ucontrol.Resources.Item(style_key.ToString())
return p
end

def create_text_block(text,style_key)
t = TextBlock.new
t.Text = text
t.Style = ucontrol.Resources.Item(style_key.ToString())
return t
end

def adjust_point_text_block(p,t)
t.Text = "("+("%.3f" % @converter.convert_to_plane_x(p.x)) +
","+("%.3f" % @converter.convert_to_plane_y(p.y)) + ")"
Canvas.SetTop(t,p.y+5)
Canvas.SetLeft(t,p.x-5)
end


Separating the styles

Silverlight style separationprovides a way to separate style definitions for UI elements. For this program, all the style definitions are defined in a separate section of the XAML file.


<UserControl.Resources>
<Style TargetType="TextBlock" x:Key="FormulaTextStyle">
<Setter Property="FontSize" Value="13"/>
</Style>
<Style TargetType="TextBlock" x:Key="FormulaExponentTextStyle">
<Setter Property="FontSize" Value="12"/>
</Style>
<Style TargetType="TextBlock" x:Key="PointTextStyle">
<Setter Property="FontSize" Value="10"/>
</Style>
<Style TargetType="Line" x:Key="AxisLineStyle">
<Setter Property="Stroke" Value="Black" />
<Setter Property="StrokeThickness" Value="3" />
</Style>
<Style TargetType="Polyline" x:Key="PolylineStyle">
<Setter Property="Stroke" Value="Black" />
</Style>
<Style TargetType="Path" x:Key="GridLineStyle">
<Setter Property="Stroke" Value="Cyan" />
</Style>
<Style TargetType="Path" x:Key="ControlPointStyle">
<Setter Property="Fill" Value="Blue" />
</Style>
</UserControl.Resources>


In order to assign styles to elements in IronRuby the following code was used:


p.Style = ucontrol.Resources.Item(style_key.ToString())


Given that style_key is an IronRuby variable with a string. Note that I had to use the ToString method. If the ToString method as not used I received a runtime error saying: ArgumentException: key must be a string.

Deploying the application

The creation of the XAP file to be deployed is VERY simple. The following command creates an app.xap file from a existing app folder.

Chiron.exe /z:app.xap /d:app


The /z parameter adds all the libraries required for executing IronRuby.

Final words

The only issue I see is the lack of documentation for using Silverlight features with scripting languages. The main thing missing is how to use data binding (if possible) which is a very nice feature which I enjoyed using in JavaFX Script for the previous post.

Because of the inclusion of the IronRuby support libraries, the final XAP size was be bigger than I expected (~700K in this case). However it was not as big as the one from the JavaFX example which included all the JavaFX runtime libraries.

For this example I had to take care of the performance. I stared using an almost direct port of the JavaFX script code of the previous post for the algorithm and the polynomial operations which lead to bad performance.

The integration of IronRuby with .NET is very nice, translating examples from C# was very easy, for example with the element dragging code described above. One thing that can be improved is to give more information on where an exception occurred in a program.

Overall the experience of working with IronRuby and Silverlight was very nice. I was very easy to get feedback of code changes just by hitting refresh in the browser.

Code for this post can be found here.

The running version of the example can be found here.

No comments: