Contents
Introduction
In our last article we discussed the implementation of the game Sokoban using WPF. In this article we will examine the porting of Alien Sokoban to Silverlight, and examine some of the differences between WPF and Silverlight (formally WPF/E). We will look at consuming a web service from Silverlight, creating Silverlight User Controls, hosting a Silverlight project in ASP.NET, and using some of the new Microsoft tools such as Expression Design and Expression Blend; we will also explore some more advanced topics such as multithreading in Silverlight. There have also been quite a few enhancements and new features added to the game.
For those eager to take a look, without having to go through the rigmarole of setting it up locally, I have deployed the game to a server: Play Alien Sokoban.
Background
As the builders say, the larger stones do not lie well without the lesser.
-Plato
In our first article we saw how easy it was to define most of the presentation logic for the game in XAML. This allowed us to keep our code behind mostly free from boiler plate code. WPF has many features that are not present in Silverlight, such as data binding, templating, and styling of controls. Alas our Silverlight version contains considerably more plumbing.
As you probably already know, Silverlight is a browser plugin for providing rich web content. It includes a subset of the capabilities of WPF, and it aims to rival Adobe Flash. I have been interested in Silverlight since I heard about it in 2006. Years ago, I spent a while as a Flash developer/animator, and back then I was writing loosely typed Actionscript for Flash 5. I've never been a big fan of scripting, and I had wanted to do more. Now, like Adobe Flex, Silverlight offers the developer just that. But, having a .NET CLR client side is enough to make most .NET web developers drool.
Silverlight is going to be big, really big. Like Flash, it is cross platform. At present the Silverlight 1.1 Alpha September Refresh plugin is available for Windows and Mac, and the MONO team has stated that it intends to have a Linux version, called Moonlight, available by the end of the year [The Inquirer 2007][Wikipedia 2007]. Version 1.0 of Silverlight offered a light-weight browser plugin, and manipulation of Silverlight at client side was limited to scripting of the DOM using JavaScript. Version 1.1 is a lot more attractive to developers, as it offers .NET on the client side. It has a CLR 2.0 compiled CLR and Type System, a reduced set of BCL classes from .NET Framework 3.5 (including DLINQ). I have, however, encountered a number of noticeably absent classes, which has meant that I have had to do some reimplementation. The 1.1 plugin also includes the new dynamic language runtime (DLR), with support coming for Python, Ruby, VBScript VBx (Visual Basic "10") and JavaScript (ECMAScript), but we won't be exploring that here [Hanselman 2007].
While I believe the excitement that Silverlight is generating is well founded, I wouldn't want to see us plunged back into a web dark age, with a return to gratuitous animation as mentioned in Jakob Nielsen's 2000 polemic article Flash: 99% Bad. Like Flash, Silverlight offers the opportunity to once again decrease usability and accessibility. Do you remember those annoying animated intros! I must confess that I created a few! Yet having the freedom, to develop interactive content, without having to write loads of JavaScript makes me happy. After years of building websites using technologies like Java and ASP.NET, and like many developers, spending an exorbitant amount of time contending with browser differences and bending the HTML model to my will, I rejoice at even the thought of being freed of such a burden.
Part of Microsoft's strategy in producing several new Visual Design IDEs, I refer to the Expression product suite, is obviously to provide distinct separation of the Graphic Design, Animation, and Development aspects of projects. I know from first hand that this is not a bad idea. Whether this is a commercial strategy, I won't speculate, but it makes business sense. In a commercial software house, designers output their designs in the form of vector images etc., and this becomes the input to the developers/animators who then proceed to dissect, reformat and generally squash it into something that, as much as possible stays true to the original design. More often than not, however, last minute changes to the design or tweaking needs to be done, and this throws a spanner in the works, and often causes the animator/developer to have to duplicate his or her undocumented squashing process. The nice thing about using a unified format, in this case XAML, is that there isn't a conversion process between handoffs, and to a limited extent it does empower the developer to make minor changes. But, I can see there being some advantage to having some rudimentary support for Storyboarding within Visual Studio, which it sorely lacks. Until it does, smaller organizations will need to seriously consider providing their developers with Expression Blend.
Getting Started with Silverlight
To work with Silverlight 1.1 within Visual Studio 2008 Beta 2, one must download the Microsoft Silverlight Tools Alpha. That will add a new item to the Visual Studio project types.
Figure: Silverlight project types in Visual Studio 2008.
Using a Silverlight Project with Other Regular CLR Projects
In order to debug Silverlight projects hosted within a non-Silverlight project, such as in our case an ASP.NET web application, we must use the Add Silverlight Link from the Visual Studio Project context menu within the Solution Explorer.
Linking to the Silverlight project and hosting it within the ASP.NET project that contains the web service avoids the web service "Cross-Domain problem" that I've heard frequently mentioned.
Figure: Add Silverlight Link in Visual Studio.
Once this is done, files including output assemblies from the ClientBin in the Silverlight project will be automatically copied to your hosting project, and they will be kept in sink when one performs a build. Of course, however, content resources used by your Silverlight project, such as media files, will not be copied, and may require a build event copy task, as show below.
Figure: Visual Studio Build Event copy task.
How to Play the Game
Push the blue power cubes (treasures) onto the yellow power stations. When all cubes are seated in the power stations, our alien friend will be transported to the next level. Only one power cube may be pushed at a time, and they cannot be pulled.
Once a new level has been attained, make note of the level code. This may be used to later return to that level.
Controls
Either the keys i, j, k, l or the mouse can be used to control the actor. If the keyboard is used, then the actor is able to move up, down, left, or right. If the mouse is used, then the actor will attempt to traverse the level grid to a point clicked by the user. There is a new feature in Alien Sokoban: the mouse can be used to move a power cube by positioning the actor adjacent to the power cube and then by single clicking on it, or by holding down the shift key and clicking a square that is in the same row or column as the actor.
Use Ctrl+Z and Ctrl+Y to undo and redo moves and jumps.
Level Items
- Power cubes are to be moved to the goal cells (power stations)
- The actor is controlled by the user
- Wall cells cannot be entered by the actor, nor have content placed in the cell
- Floor cells are where the actor may move, or place power cubes
- Space cells are normally outside of the walled enclosure of the level grid. They can't contain content
Moving Around
The actor may move either in a single step to an adjacent floor cell, or with a jump to any reachable floor cell on the level grid. In the case of a jump, the application will calculate the best path, i.e. that which is comprised of the fewest number of moves.
Extending Playability
Alien Sokoban's playability may be extended by creating or modifying the map files located in the Levels directory.
Map File Format
The following characters are used within the map files to signify the structure of a level:
- "#" Wall
- " " Empty floor cell
- "$" Power cell in a power station (or goal)
- "." Power station (or goal)
- "@" Actor in a floor cell
- "!" Space cell
Level Codes
Level codes are used to jump to a level after it has been successfully completed. The LevelCode
class has 500 pregenerated level codes available. Although there are only 51 levels, if more level files are added (*.skbn files in the Levels directory) the level codes will open up. There are some static
methods in the LevelCode
class to regenerate the level codes if necessary.
About the Code
Asynchronous Property Changes
To simulate the actor walking across cells in a level, when either a Jump
or a Push
occurs, the executing thread needs to be put to sleep for a specific duration. For this to work correctly it is done asynchronously. The base class for Cell
and CellContents
is LevelContentBase
. This class provides for the raising of the PropertyChanged
event asynchronously. Like WPF, Silverlight uses a single thread of execution with thread affinity. This means that we cannot manipulate UI elements from any thread other than the main application thread. To prevent an InvalidOperationException
being thrown from worker threads raising PropertyChanged
events, we use a SynchronizationContext
instance, initialised during the Page.xaml Page_Loaded
handler. This queues calls to the main UI thread as shown in the following excerpt from LevelContentBase.cs.
context.Send(delegate
{
OnPropertyChanged(new PropertyChangedEventArgs(property));
}, null);
Multithreading in Silverlight
At the moment there is no built-in way to marshal updates to the UI from a background thread. System.Threading.SynchronizationContext
is noticeably absent from the Silverlight subset BCL. So, we've had to role our own.
public delegate void SendOrPostCallback(object state);
public class SynchronizationContext
{
static System.Threading.Thread thread =
System.Threading.Thread.CurrentThread;
readonly System.Windows.Browser.HtmlTimer timer =
new System.Windows.Browser.HtmlTimer();
readonly List<CallbackState> callbacks = new List<CallbackState>();
readonly object callbacksLock = new object();
#region Singleton implementation
SynchronizationContext()
{
thread = System.Threading.Thread.CurrentThread;
timer.Interval = 1;
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
public static SynchronizationContext Current
{
get
{
return Nested.instance;
}
}
class Nested
{
static Nested()
{
}
internal static readonly SynchronizationContext instance =
new SynchronizationContext();
}
#endregion
public static void SetContext(object dummy)
{
}
void timer_Tick(object sender, EventArgs e)
{
lock(callbacksLock)
{
if (callbacks.Count > 0)
{
foreach (CallbackState state in callbacks)
{
state.Callback(state.State);
}
callbacks.Clear();
}
}
}
public virtual void Send(SendOrPostCallback callback, object state)
{
if (thread.Equals(System.Threading.Thread.CurrentThread))
{
callback(state);
return;
}
lock (callbacksLock)
{
callbacks.Add(new CallbackState()
{ Callback = callback, State = state });
}
}
class CallbackState
{
public SendOrPostCallback Callback
{
get;
set;
}
public object State
{
get;
set;
}
}
}
We emulate the System.Threading.SynchronizationContext
: calls that would otherwise be executed using a reference to a main UI thread context, are invoked using a timer and a queue. Unfortunately you'll notice that System.Windows.Browser.HtmlTimer
is deprecated, and that a new Timer type is on its way. But for now, it does the trick.
Sokoban Web Service
In the previous article we saw our game load map data directly from a predefined local directory. In this version, rather than sending all maps down the wire, we allow them to be requested at runtime via a web service. That is, the Sokoban web service provides the Silverlight game with map data.
Silverlight supports JSON for communicating with web services, and we specify that the web service should use JSON by decorating our SokobanService
methods with the ScriptMethod
attribute as shown in the following excerpt.
[WebMethod(CacheDuration = cacheTimeoutSeconds)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string GetMap(int levelNumber)
{
string virtualPath = string.Format(@"{0}Level{1:000}.skbn",
levelDirectory, levelNumber);
string filePath = Server.MapPath(virtualPath);
string mapData;
lock (readerLock)
{
using (StreamReader reader = File.OpenText(filePath))
{
mapData = reader.ReadToEnd();
}
}
return mapData;
}
You may notice that I have commented parts of the Game
class for retrieving map data from the web service asynchronously. This is because the Silverlight 1.1 alpha September Refresh throws an InvalidOperationException
if the web service calls are not made from the main UI thread. I hope to see this resolved in a future release.
Silverlight 1.1 Alpha - Keyboard Event Handling
When handling keyboard events, you will notice that there isn't a Silverlight enum
for key codes. Instead the KeyboardEventArgs
of the KeyUp
and KeyDown
event use an int
value to indicate the key. These key codes are non-specific to any particular operating system, and in fact they are a common subset of all the possible key codes for Macintosh and Microsoft Windows. [Microsoft 2007]
Unfortunately a bug present in the Alpha 1.1 Refresh release causes the arrow keys not to fire the KeyUp
event correctly. Instead we have had to change the keyboard mappings to the keys used which are 'I' (up) 'K' (Down) 'J' (Left) 'L' (Right).
Layout Support
Unlike WPF, there is no support for centering text within a TextBlock
[Porter 2007]. This was performed manually within the user controls, as the following excerpt demonstrates:
Canvas parent = (Canvas)Parent;
double left = (parent.Width - base.Width) / 2.0;
double top = (parent.Height - base.Height) / 2.0;
User Controls
Silverlight does not include a standard set of controls that you would normally expect to use to build an interactive interface. Expect these to come later. In the mean time we need to come up with our own.
Textbox Control
There is no textbox control included with the Silverlight distribution yet. So I've had to adapt Stephane Tombeur's one for our needs. You will notice also that it is not integrated into the PlayControl.xaml. This is, unfortunately, because there is no provision for the nesting of controls in Silverlight, which raises a serious reusability issue. One may nest canvases, but not controls. [Silverlight 2007]
Hit Testing
To programmatically focus and unfocus the TextBox
control one must detect when and where a click occurs on the page when the TextBox
has focus. That is, when a user clicks away from the TextBox
, the TextBox
unfocuses itself. The following excerpt demonstrates how this was done. I'm dubious as to the reliability of this method, but it will suffice.
void root_MouseLeftButtonDown(object sender, MouseEventArgs e)
{
if (!hasFocus)
{
return;
}
Point point = e.GetPosition(this);
if (point.X < 0 || point.X > outline.Width || point.Y < 0
|| point.Y > outline.Height)
{
Unfocus();
}
}
Text Effects
WPF has lots of effects, Silverlight doesn't have any. It's DIY here I'm afraid. There is a simple drop shadow effect in the FeedbackControl
which is done using a duplicated TextBlock
with a RenderTransform
to offset it, and to appear as a shadow.
<TextBlock x:Name="textBlock_MessageShadow" Text="TextBlock">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="2" Y="2"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name="textBlock_Message" Text="TextBlock" />
Sokoban Project
Game logic is contained within the Orpius.Sokoban
project. I had originally intended to add the existing Orpius.Sokoban
project from my previous article, and simply reuse that. This was not possible, as Silverlight projects can only reference other Silverlight projects. This means that only assemblies that were especially compiled for Silverlight, i.e. those that come with Silverlight 1.1 or those built by you or others; compiled for the Silverlight CLR, can be referenced. [Zander 2007]
For more information regarding the core Sokoban game project, please see my previous article. There have been a number of enhancements and new features implemented. More on that later.
Animating with Expression Blend
To create an animation within Expression Blend, click the > button in the "Objects and Timeline" window. This will produce a dialogue in which you can define a new Storyboard. A Storyboard consists of a timeline where one defines keyframes to indicate transition points in the animation. Anyone familiar with Flash will be right at home here.
N.B. What you see isn't necessarily what you get. Moreover, I found that animating a TextBlock colour change worked nicely when previewing in Expression Blend, but not in the game. Until I explicitly defined the Foreground colour property of the TextBlock, the animation would not work.
Figure: Creating a storyboard in Expression Blend.
Creating a mouseover/mouseout effect is as simple as creating two Storyboards.
Figure: Expression Blend Storyboard Animation.
And then wiring them to the appropriate events in the code behind:
button_RestartLevel.MouseEnter +=
new MouseEventHandler(button_RestartLevel_MouseEnter);
button_RestartLevel.MouseLeave +=
new EventHandler(button_RestartLevel_MouseLeave);
The Storyboards are then started thus:
void button_RestartLevel_MouseEnter(object sender, MouseEventArgs e)
{
restartLevelMouseOverStoryboard.Begin();
}
void button_RestartLevel_MouseLeave(object sender, EventArgs e)
{
restartLevelMouseOutStoryboard.Begin();
}
Exporting from Expression Design
In our last article, we exported the layers of our Expression Design Cell image to a ResourceDictionary
. Unfortunately Silverlight does not use Resource Dictionaries, and instead we export as a Silverlight Canvas
, where each layer is exported as a named child Canvas
. It works in much the same way, but we must add a little more plumbing in the code behind in order to show, hide, or resize the layers (Canvases).
Figure: Exporting from Expression Design for Silverlight.
Scale Transform
In order to use the canvases that are exported from Expression Design, they had to be resized, and to achieve this, a ScaleTransform
render was used. By placing a ScaleTransform
in a parent Canvas
, resizing the parent will cause all child canvases to be resized by the same proportion. To achieve this in XAML we use:
<Canvas.RenderTransform>
<ScaleTransform x:Name="scale" ScaleX="1" ScaleY="1" />
</Canvas.RenderTransform>
If we do not wish to make modifications to an exported XAML file, lest our changes be lost, we can achieve this programmatically by instantiating a ScaleTransform
,
ScaleTransform scale = new ScaleTransform() { ScaleX = 1, ScaleY = 1 };
and then assign it to your target Canvas
.
rootCanvas.RenderTransform = scale;
Here the aspect ratio is 1:1. That is, if we now change the ScaleX
or ScaleY
properties of our ScaleTransform
instance, we will effect a resize of the parent and all children by the ratio of 1:X or 1:Y where X and Y are the new ScaleX
and ScaleY
property values respectively. As an example, say our canvas is 500 pixels wide and we include a ScaleTransform
render with ScaleX
and ScaleY
of both 1. If we then change the ScaleX
value to 0.5, the width of the canvas will be reduced to 250 pixels, and the height will remain unchanged; effectively skewing the parent and children.
Sokoban Project Continued
Moves
Moves are delivered to the Actor
instance. The actor knows how to perform a move, whether it is a "step" to an adjacent cell, or a "jump or push" to a non-adjacent cell somewhere on the level.
Game Logic Enhancements
Jumping, Pushing, and Path Searching
A Jump
is executed by the Actor
instance with the help of a SearchPathFinder
instance. The Path Search algorithm has been revised from a Depth First search, which would be ok if we weren't interested in finding the best path, to a Breadth First search. (Shiflet 1996) The following excerpt shows the new implementation of the path search algorithm.
bool TryFindPath(Location start)
{
Queue<Location> queue = new Queue<Location>();
queue.Enqueue(start);
Dictionary<Location, Location> previousSteps =
new Dictionary<Location, Location>();
Location location;
while (queue.Count > 0)
{
location = queue.Dequeue();
if (location.Equals(destination))
{
Location parentLocation;
Location childLocation = location;
List<Move> moves = new List<Move>();
while (previousSteps.TryGetValue
(childLocation, out parentLocation))
{
Direction dir = parentLocation.GetDirection(childLocation);
moves.Add(new Move(dir));
childLocation = parentLocation;
}
moves.Reverse();
route = moves.ToArray();
return true;
}
else
{
for (int i = 0; i < 4; i++)
{
Direction direction = GetDirection(i);
Location neighbour = location.GetAdjacentLocation(direction);
if (level[neighbour].CanEnter &&
!previousSteps.ContainsKey(neighbour))
{
previousSteps.Add(neighbour, location);
queue.Enqueue(neighbour);
}
}
}
}
return false;
}
New Push Move
In the previous version of the game, the user was only able to push a power cube via the keyboard, or by positioning the actor adjacent to the cube and then clicking on it. Now we are able to "push" a cube using the mouse. This is done using a Shift+click combination, and is implemented in the PushPathFinder
class. A PushPathFinder
instance calculates a linear route that is obstructed by at most one power cube.
class PushPathFinder
{
readonly Location destination;
readonly Cell startCell;
readonly Level level;
readonly List<Move> moves = new List<Move>();
bool treasurePushed;
Move[] route;
public Move[] Route
{
get
{
return route;
}
}
public PushPathFinder(Cell start, Location destination)
{
this.destination = destination;
startCell = start;
level = start.Level;
}
public bool TryFindPath()
{
Location current = startCell.Location;
if (current.ColumnNumber != destination.ColumnNumber &&
current.RowNumber != destination.RowNumber)
{
return false;
}
moves.Clear();
bool horizontal = current.RowNumber == destination.RowNumber;
if (horizontal)
{
int rowNumber = current.RowNumber;
if (current.ColumnNumber < destination.ColumnNumber)
{
for (int i = current.ColumnNumber + 1;
i <= destination.ColumnNumber; i++)
{
Cell cell = level[rowNumber, i];
if (!Enter(cell, Direction.Right))
{
return false;
}
}
}
else
{
for (int i = current.ColumnNumber - 1;
i >= destination.ColumnNumber; i--)
{
Cell cell = level[rowNumber, i];
if (!Enter(cell, Direction.Left))
{
return false;
}
}
}
}
else
{
int columnNumber = current.ColumnNumber;
if (current.RowNumber < destination.RowNumber)
{
for (int i = current.RowNumber + 1;
i <= destination.RowNumber; i++)
{
Cell cell = level[i, columnNumber];
if (!Enter(cell, Direction.Down))
{
return false;
}
}
}
else
{
for (int i = current.RowNumber - 1;
i >= destination.RowNumber; i--)
{
Cell cell = level[i, columnNumber];
if (!Enter(cell, Direction.Up))
{
return false;
}
}
}
}
route = moves.ToArray();
moves.Clear();
return true;
}
bool Enter(Cell cell, Direction fromDirection)
{
bool result = false;
if (cell.CanEnter)
{
result = true;
}
else if (!treasurePushed && cell.CellContents is Treasure)
{
treasurePushed = true;
result = true;
}
if (result && !(cell.Location == destination && treasurePushed))
{
Move move = new Move(fromDirection);
moves.Add(move);
}
return result;
}
}
The Push
is then enacted by the Actor
not unlike a Jump
, as the following excerpt shows.
public partial class Actor
{
internal bool DoMove(Push push)
{
bool result = false;
if (push.Undo)
{
lock (moveLock)
{
for (int i = push.Route.Length - 1; i >= 0; i--)
{
Move move = push.Route[i];
Location moveLocation = Location.GetAdjacentLocation
(push.Route[i].Direction.GetOppositeDirection());
Cell toCell = Level[moveLocation];
Cell fromCell = this.Cell;
if (!toCell.TrySetContents(this))
{
throw new SokobanException("Unable to follow route.");
}
if (move.PushedContents != null)
{
if (!fromCell.TrySetContents(move.PushedContents))
{
throw new SokobanException
("Unable to undo set contents.");
}
}
}
MoveCount -= push.Route.Length;
result = true;
}
}
else
{
WaitCallback callback = delegate(object state)
{
#region Anonymous Push method.
lock (moveLock)
{
PushPathFinder pathFinder =
new PushPathFinder(Cell, push.Destination);
if (pathFinder.TryFindPath())
{
for (int i = 0; i < pathFinder.Route.Length; i++)
{
Move move = pathFinder.Route[i];
Thread.Sleep(stepDelay);
Location moveLocation =
Location.GetAdjacentLocation(move.Direction);
Cell toCell = Level[moveLocation];
if (!toCell.CanEnter &&
toCell.CellContents is Treasure)
{
move.PushedContents = toCell.CellContents;
if (!toCell.TryPushContents(move.Direction))
{
throw new SokobanException("Unable to push contents.");
}
}
if (!toCell.TrySetContents(this))
{
throw new SokobanException
("Unable to follow route.");
}
MoveCount++;
}
Push newMove = new Push(pathFinder.Route)
{ Undo = true };
moves.Push(newMove);
result = true;
}
}
#endregion
};
ThreadPool.QueueUserWorkItem(callback);
}
return result;
}
}
Future Enhancements
- Actor walking should be able to redirect in mid jump/push, rather than having to block to completion
- Preloading of content, including audio
- Adding some animation to actor/cubes etc.
- Creating a level editor
Conclusion
In this article we looked at the porting of a WPF application to Silverlight, and highlighted some of the differences between the two technologies. We looked at consuming a web service from Silverlight, creating Silverlight User Controls, hosting a Silverlight project in ASP.NET, and using some of the new Microsoft tools such as Expression Design and Expression Blend, and some more advanced topics such as multithreading. We also examined some of the shortcomings of the Alpha 1.1 Refresh, and how they can be worked around. As with any alpha software, it is important to realise that it will undoubtedly change a lot before its production release. At present, however, Silverlight still promises a strong platform on which to build rich web content.
I hope you find this project useful. If so, then you may like to rate it and/or leave feedback below.
Credits
References
- Shiflet, A.B. 1996, 'Data Structures in C++ Including Breadth & Laboratories', West Publishing Company, St. Paul MN, pp. 754-757.
- Farrel, N 2007, Linux Silverlight to arrive by year end
Retrieved 9 November 2007 from The Inquirer
- Hanselman, S. 2007, Putting Mix, Silverlight, the CoreCLR and the DLR into context
Retrieved 9 November 2007 from Scott Hanselman's ComputerZen.com
- Microsoft, 2007, Key Enumeration
Retrieved 8 November 2007 from Microsoft Developer Network
- Porter, S. 2007, Centering and Sizing Text in Silverlight
Retrieved 8 November 2007
- Zander, J. 2007, Origin of the Silverlight CLR and .NET Framework
Retrieved 7 November 2007 from Jason Zander's WebLog
- Nielsen, J. 2000, Flash: 99% Bad
Retrieved 9 November 2007 from useit.com
- Tombeur, S. 2007, Silverlight Alpha 1.1 TextBox
Retrieved 6 November 2007 from Waaargh.NET
- Microsoft, 2007, 'Component' does not support 'Component' as content
Retrieved 6 November 2007 from Silverlight.net
- Wikipedia, 2007, Breadth-first search
Retrieved 9 November 2007 from Wikipedia
- Wikipedia, 2007, Mono (Software)
Retrieved 9 November 2007 from Wikipedia
History
- November 2007: First release