Sunday, October 21, 2007

Building 3D Cube in Silverlight 1.0 using Script#

Silverlight 1.0 is out there in the wild for a couple of months righorse riverht now (otherwise to say this is to use the classic Bulgarian folklore - "Уйде коня в ряката", translated into English - "Went the horse into the river"). Silverlight 1.1 is still in alpha (beta was scheduled for "later this year") and it is not a good idea to go in production with 1.1. So if you want to use Silverlight on your site today, you must constrain yourself to 1.0 and pure JavaScript. This is not true if you are aware what tools are out there for software developers.

Script# - building Silverlight 1.0 applications with C# today

Script# is a "C# compiler that generates JavaScript as its output instead of IL". Yes that's right. You write your code in plain old C# and in the end you have clean and optimized JavaScript code, ready for deployment. The genius behind Script# is Nikhil Kothari, who developed it on his spare time. (it seems that if you work for Microsoft you have a lot of spare time :) As you can see from the diagram below Script# parses the C# files and produce JavaScript. Also the C# compiler is run against the code to verify its correctness.

script#

Leaning Script#

Understanding Script# programming metaphor is not hard, but have some learning curve. Must read is the read me file, which can be found here. The readning is not a couple of pages, but full blown document, which is very well structured (thanks Nikhil :). If you are familiar with both C# and JavaScript the learning process will be painless and you should only familiarize yourself with some of the new concepts that Script# introduces - scriptlets for example. A good way to explorer Script# is to use ultimate number one .NET developer tool - Reflector.

Building the Cube

As stated in the beginning Silverlight 1.0 is the choice if you want to rich enable your site. However there are a lot samples out there that target 1.1. One of these is the 3D Rotating Picture Cube in Iron Python. If you follow the forum thread you will see that the sample gets translated into C# again for 1.1. Actually the source for the sample came from this wonderful flash tutorial, which gives deep explanation about how to apply matrix transforms on objects and in the end has the source for the cube in flash.

Algorithm for the cube

I'm not going to explain the algorithm for displaying the cube. I just copy and paste it from the samples and renamed some of the methods and fields to be more meaningful. I must confess that I have problems with my geometry classes (I wish it was only geometry) this year in university, but finally managed to take them. Enough side talks, lets look at some code.

Code behind the cubeCubeSolution

Again I will leave the matrix transformation stuff for geometry geeks and will concentrate how to use Script# to get things done. First of all you must download Script# from its download page. After installation a couple of new projects will be available in the newly created Script# project group. Note: Script# have 2 modes - one is to output JavaScript code based on Script# core library, other is to output code that targets ASP.NET AJAX framework. First option is better for now, because Script# have more powerful library (in out case we will heavily use the Math object). Ok let's create a project of type "Class library in a web site". In order to use Silverlight object model we should also add a reference to the ssagctrl.dll that comes with the installation. All these starting procedures are well explained in the read me file. All the interesting stuff is in the Cube.cs and CubeScriptlet.cs files.

Cube.cs

Cube class is the main class that do all the logic. Let's look at its constructor:

public Cube(SilverlightControl control, string[] imageUrls, double edgeLength)

{

rotation = new Point3D(0, 0, 0);

this.edgeLength = edgeLength;

this.control = control;

root = (Canvas) control.Content.FindName( "Root" );

root.Width = edgeLength;

root.Height = edgeLength;

InitializeImages( imageUrls );

InitializeEdgePoints();

Rotate( 0, 0 );

}

It takes:

  • SilverlightControl for doing the hooks with the Silverlight plug-in. Note that this Silverlight control has the same API that has the original browser plug-in for Silverlight. Actually for producing it another feature of Script# was used - ability to import 3rd party components and controls as Script# libraries.
  • array of strings that holds the URLs to the images that will be used as faces of the cube
  • edgeLength that show how big should be the cube.

A reference to the Root canvas is obtained using the Silverlight control . This canvas should be present in the XAML file that is used for the initialization of the SilverlightControl (we will look at this in the scriptlet file). Then some initialization operations are performed and finally default rotation is used to put the cube in its default position. Let's look how we initialize the images:

private void InitializeImages( string[] imageUrls )

{

foreach ( string imageUrl in imageUrls )

{

Image image = (Image) control.Content.CreateFromXaml(

string.Format(@"

<Image Width=""{0}"" Height=""{0}"" Source=""{1}"" Visibility=""Collapsed"">

<Image.RenderTransform>

<MatrixTransform Matrix=""1,0,0,1,0,0"" />

</Image.RenderTransform>

</Image>", edgeLength, imageUrl), true);

root.Children.Add( image );

Canvas.SetLeft( image, 0 );

Canvas.SetTop( image, 0 );

}

}

For each url a new instance of type Image is created using the helper method CreateFromXaml(), exposed by the SilverlightControl. Note that this is the only way to create Silverlight 1.0 objects, because they did not have public constructors. When each image is created, it is added to the root canvas and positioned at (0,0). As you can see in the beginning each image won't be visible, because it has its visibility set to collapsed. This is why we call the Rotate() method in the constructor. It is going to apply the correct visibility according to the current position of the cube. Let's look at the Rotate() method itself:

public void Rotate( double offsetX, double offsetY )

{

rotation.X -= offsetY;

rotation.Y += offsetX;

Point[] transformations = CalculateTransformationPoints();

ApplyTransformation( 0, transformations[ 2 ], transformations[ 0 ], transformations[ 3 ] );

ApplyTransformation( 1, transformations[ 5 ], transformations[ 1 ], transformations[ 2 ] );

ApplyTransformation( 2, transformations[ 0 ], transformations[ 2 ], transformations[ 1 ] );

ApplyTransformation( 3, transformations[ 4 ], transformations[ 3 ], transformations[ 0 ] );

ApplyTransformation( 4, transformations[ 3 ], transformations[ 4 ], transformations[ 5 ] );

ApplyTransformation( 5, transformations[ 1 ], transformations[ 5 ], transformations[ 4 ] );

}

Again this is mainly geometry stuff. What is interesting here is the how the offsets are applied to the rotation member. If you have looked closely into the constructor's body you have found that rotation member is of type Point3D - custom class in the solution, which holds the current position of the cube. Finally let's look at the ApplyTransformation() method:

private void ApplyTransformation( int faceIndex, Point a, Point b, Point c )

{

UIElement element = root.Children.GetItem( faceIndex );

element.Visibility = IsFaceVisible(a, b, c) ? Visibility.Visible : Visibility.Collapsed;

if (element.Visibility == Visibility.Visible)

{

MatrixTransform transform = (MatrixTransform) element.RenderTransform;

Matrix matrix = transform.Matrix;

matrix.OffsetX = a.X;

matrix.OffsetY = a.Y;

matrix.M11 = ( b.X - a.X ) / element.Width;

matrix.M12 = ( b.Y - a.Y ) / element.Width;

matrix.M21 = ( c.X - b.X ) / element.Height;

matrix.M22 = ( c.Y - b.Y ) / element.Height;

transform.Matrix = matrix;

}

}

It handles the complex logic for applying the right matrix transform to the item. In case the face should be hidden its visibility is collapsed and the item is not processed.

CubeScriptlet.cs

This file contains the logic for hooking the DOM elements into our newly created Cube class. Let's examine its constructor:

private CubeScriptlet(Dictionary arguments)

{

Application.Current.RegisterDisposableObject( this );

button = new Button( (DOMElement) arguments[ "Button" ] );

button.Click += OnButtonClick;

ControlParameters cubeParameters =

new ControlParameters( (string) arguments[ "MarkupURL" ], (DOMElement) arguments[ "ParentElement" ],

(string) arguments[ "ID" ], (string) arguments[ "CssClass" ] );

cubeParameters.background = "black";

ControlFactory.CreateSilverlight( cubeParameters, OnXamlLoaded, null, arguments );

}

Here you can see the way to create Silverlight plug-in object. It is through the ControlFactory class, which gets a bunch of parameters for initialization. One of them is the url for the XAML markup that we use to find the root canvas in. What is interesting here is the code for initializing our cube class. This code is in a method called OnXamlLoaded(). It is a callback which gets called when the XAML contents are parsed and constructed in memory. Here is the method:

private void OnXamlLoaded(SilverlightControl control, object context)

{

string[] imageUrls = (string[]) Script.Eval( (string)( (Dictionary) context )[ "ImageUrls" ] );

double edgeLength = (double) ( (Dictionary) context )[ "EdgeLength" ];

cube = new Cube( control, imageUrls, edgeLength);

}

You can see that the URLs to the images are passed as one string. As we will see in a minute this string is JavaScript array of strings serialized as JSON. This is why we need to eval it here before passing it to the Cube constructor.

Other interesting part is the handler to a button's click event, which starts and stops the rotating of the cube:

void OnButtonClick(object sender, EventArgs e)

{

if ( intervalToken != 0 )

{

( (InputElement) button.DOMElement ).Value = "Start";

Window.ClearInterval( intervalToken);

intervalToken = 0;

}

else

{

( (InputElement) button.DOMElement ).Value = "Stop";

intervalToken = Window.SetInterval(OnTimer, 50);

}

}

private void OnTimer()

{

cube.Rotate(0.1, 0.1);

}

Note that Script# has a animation framework, which can be used here as well.

Default.aspx

Finally let's look at the chunk of code that we need in our page.

<ssfx:Scriptlet runat="server" ID="scriptlet" PrecompiledScriptlet="Cube.CubeScriptlet"

EnableDebugging="True">

<References>

<ssfx:AssemblyReference Name="sscorlib"></ssfx:AssemblyReference>

<ssfx:AssemblyReference Name="ssfx.Core"></ssfx:AssemblyReference>

<ssfx:AssemblyReference Name="ssfx.UI.Forms"></ssfx:AssemblyReference>

<ssfx:AssemblyReference Name="ssfx.XDAjax"></ssfx:AssemblyReference>

<ssfx:AssemblyReference Name="ssagctrl"></ssfx:AssemblyReference>

<ssfx:AssemblyReference Name="Cube"></ssfx:AssemblyReference>

</References>

<Arguments>

<ssfx:ElementReference ElementID="btnTest" Name="Button"></ssfx:ElementReference>

<ssfx:ElementReference ElementID="cubeContainer" Name="ParentElement"></ssfx:ElementReference>

<ssfx:StringLiteral Value="cubeControl" Name="ID"></ssfx:StringLiteral>

<ssfx:StringLiteral Value="Cube.xaml" Name="MarkupURL"></ssfx:StringLiteral>

<ssfx:StringLiteral Value="root" Name="CssClass"></ssfx:StringLiteral>

<ssfx:StringLiteral Value="[&quot;images/sqeagle.jpg&quot;,&quot;images/sqgorilla.jpg&quot;]" Name="ImageUrls" />

<ssfx:NumericLiteral Value="330" Name="EdgeLength"></ssfx:NumericLiteral>

</Arguments>

</ssfx:Scriptlet>

We are using the Script# server control called Scriptlet that allow us to easily embed our code into the page. You can see here how we pass the array with image URLs as JSON string (for the sake of the sample I have removed the other four pictures from the array, because the string gets big).

So that's it. You can see a action shot bellow and find the download link at the end of the post.

action

Gotchas found during development

  • Cannot create Silverlight objects from code. You can only use the factory method of the Silverlight control, which creates objects from XAML and returns DependencyObject that you should cast to your type. In case you want to create objects that did not derive from DependecyObject it gets complicated. This is way I ended with my own Point class and did not use the Point class from the framework.
  • Using the Scriptlet server control is uncomfortable when your scriptlet is in another assembly. In order to refresh it when you rebuild it, it is required to switch to design mode to remove the reference to your assembly and add it again in the assembly references list of the control.
  • The scriptlet approach is good when you can use the server side control, but when you want to embed just the script there is some extra work.

Hope you will enjoy the sample and start your Silverlight apps today with Script#.

Download the sample solution from here.

P.S.: More translated traditional Bulgarian jokes can be found here.

P.S.S.: For more Silverlight samples with Script # - check Nikhil's carousel series.

17 comments:

Nikhil Kothari said...

Stefan, nice post!

I'll be sure to include a link to this.

Georgi Mitev said...

really very interesting post :) it's good to read something interesting in the morning

qwant said...

Cool post, but it doesn't work in Firefox! Why not?

ligAZ said...

You can find a working in Firefox version here - http://bookvar.net

qwant said...

Ok, I see correct working simple at http://bookvar.net and I need to develop solution like it. I downloaded your sample and I am going to improve it to my requirements, but it doesn't work in Firefox and I don't understand why? Do you can help my?

ligAZ said...

It is not working correctly with the latest version of Script#, because the author of the Script# has remove the Firefox compatibility part (ssmoz.js) and introduce a new one that work in Firefox, Safari and Opera (sscompat.debug.js). If you copy the new javascript file (as well as ssfx.XDAjax.debug.js) into the App_Script folder it will work fine. I have just tested it on Firefox.

However I suggest you try out the telerik's RadCube control for Silverlight for similar (even better functionality) here - http://www.telerik.com/demos/silverlight/

qwant said...

Fine, I testing now Telerik RadCube control :) Now, I have installed ScriptSharp-v0.4.2 and don't have the (sscompat.debug.js) file in it deployment folder. I try to download latest version of Script# from http://www.codeplex.com/scriptsharp but I received next message: "This project is not yet published". How can I get the latest version of Script#?

ligAZ said...

http://projects.nikhilk.net/ScriptSharp/
There are links for Visual Studio 2005 and 2008 installations at the top of the page.

qwant said...

Thanks a lot. You are good man :) And I has the latest request for you. I studying RedCube control. Do you have a little simple with RedCube using?

ligAZ said...

You can download the sample application. You just have to register at telerik.com site. Check here:
http://www.telerik.com/home/news/b21ActiveNewsId-cec-b21CategoryId-ddh.aspx

qwant said...

Today, I need to use 3D graphics in Silverlight 1.0 with using Script#. I try to find some 3D engine libraries, that can help my do my project quickly. And I didn't find libraries that can be used in my Script# project. Do you know any solutions?

cialis price said...

Really great post, Thank you for sharing This knowledge.Excellently written article, if only all bloggers offered the same level of content as you, the internet would be a much better place. Please keep it up!

xanax online said...

I officially can�t hold out for this smart phone, just 2 days remaining and I simply hope I could get my hands on one,. I have been reading a lot about the Android two.2 update that can come in August, simply another reason to be psyched about this excellent phone. Motorola and Verizon say they will update the phone to Android 2.2 late summer. It will certainly be an over-the-air update. However out-of-the box on its July 15 launch it will probably have Android 2.1.

xanax said...

very helpful post, cheers!

cheapest viagra said...

hey buddy,this is one of the best posts that I�ve ever seen; you may include some more ideas in the same theme. I�m still waiting for some interesting thoughts from your side in your next post.

Buy Cheap OmniGraffle Pro 5 for MAC said...

Thanks for posting keep it up..

Android app developers said...

This is one of the knowledgeable post.I like your blog details.This is one of the great post.