You are here:   Blog
Register   |  Login

LightSwitch News

Oct 29

Written by: Richard Waddell
10/29/2013 4:00 PM  RssIcon

Part I – Bare Bones Mechanics

Part II - Basic Classes and Interface Implementations

Part III – Game to Demonstrate Derived Classes and more Sophisticated Interface Implementations

The End Result

This article really starts when I start talking about motivation below, but I want to make the point up front that what I started out to do is not what I ended up doing. What I wanted to do was write some really cool simulations / games. What I ended up doing was writing what I hope is a really cool framework for designing / creating really cool simulations / games and a pretty mediocre game to demonstrate it. But I make lemonade out of that lemon by pointing out that a more complex game would obscure the details of how to use the framework.

Anyhoo, here’s a screen shot of three users logged into the same page and all seeing the same thing but all able to interact with it individually. The idea behind the framework is you can plug in any kind of behavior you want for the simulation objects and rules engine.image

Try It Out

You can try the game online at http://sinalrgame1.lightswitchhelpwebsite.com/HTMLClient/

The Beginning - Motivation

I like doing animations with objects that can have individualized behavior plugged in so they move around the screen and interact with each other on their own. What I don’t like is trying to do that in JavaScript. I’ve been down that path and it always eventually gets too top-heavy to be workable (How’s that for a mixed metaphor? I swear I saw one in an Escher print). My real enthusiasm for Silverlight was based on the ability to write strongly-typed animation code that compiles and provides real classes and interfaces.

So I got excited when articles about HTML5 and games started appearing but when I took a closer look, all I found was JavaScript.

No, no, no!!! I came close to bailing on a project that Michael and I were going to do that would resurrect ooNaWorld and ooBer using HTML 5 and LightSwitch.

But then I thought about the way programming has changed since the last time I wrote animations using JavaScript and the DOM; it’s now commonplace for the client to call the server for data and update arbitrary areas of the page with it .  No reason  I can’t use the same technique to run my animations on the server and just update their images on the page.

image

As far as I can see, that’s the best I can do. There’s no way I can do graphics in HTML without JavaScript. But that’s fine, the objects and everything that defines their behavior will be on the server, which answers my primary objection that it’s just too frustrating in JavaScript. Maybe you disagree, but it’s hard to argue that whatever abstractions and behavior you can code in JavaScript, you could not do more easily and reliably in C# or VB.NET. As a matter of fact, as things fell into place, instead of just an animation that ran on the server and updated the browser, I ended up writing an engine that allows dependency injection all over the place and then used that to implement an admittedly very simplistic multi-player game to demonstrate its use. The idea would never even have occurred to me if I was trying to code it on the client in JavaScript. I’m anxious to make it do something more interesting, but I’m chained to this blog, until I get all three parts done, and as you can see, I’m only on the first section of the first article. But hopefully writing about it will help me to improve it, so here goes.

Demonstrating the Concept

I’m going to demonstrate using a JSON Post that defines a success function to process the result from the server. In the long run this is not a viable approach, but it’s very simple, and I’ll use it to also demonstrate the shortcomings that SignalR overcomes.

The demonstration breaks down into the following steps.

image

 

function onCanvasMouseDown(event) {
    // alert("x=" + event.pageX + " y=" + event.pageY);
    var ooNaWorld = $("#ooNaWorld")[0];
    var canvasRect = ooNaWorld.getBoundingClientRect();
    var paramX = event.pageX - canvasRect.left;
    var paramY = event.pageY - canvasRect.top;
    $.ajax({
        type: 'post',
        data: {
            PositionX: paramX,
            PositionY: paramY,
        },
        url: '../web/ooNaClick.ashx',
        success: function success(result) {
            var ooNaThing = jQuery.parseJSON(result);
            var location = ooNaThing.Location;
            var ctx = ooNaWorld.getContext("2d");
            ctx.beginPath();
            ctx.fillStyle = ooNaThing.Color;
            ctx.arc(location.x, location.y, ooNaThing.Width / 2, 0, 2 * Math.PI, false);
            ctx.fill();
        }
    });
}
public class ooNaClick : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        int xPoint = Convert.ToInt32(Double.Parse(context.Request.Params["PositionX"]) + 0.5);
        int yPoint = Convert.ToInt32(Double.Parse(context.Request.Params["PositionY"]) + 0.5);
        ooNaThing newThing = new ooNaThing(xPoint, yPoint, 20, 30, "Red");
        System.Web.Script.Serialization.JavaScriptSerializer jsonSerializer = 
                new System.Web.Script.Serialization.JavaScriptSerializer();
        context.Response.Write(jsonSerializer.Serialize(newThing));
    }
...
}

We can think of this as a Model and View. The Model, which consists of ooNaThing objects,  is on the Server and the View, images of ooNaThings, is on the Client Browser.

Below is a diagram of the code above.

image

This implementation has several shortcomings, but the main one is the coupling between the click event and the resulting change to the view.

There’s nothing really wrong on the face of it. But it would be far better to have the Server,which has direct access to the Model, make as many of the decisions as possible about the effects, if any, of events that occur on the Client UI onto the View, such as a left mouse click. The Client should see them as events, as raw as possible, that are passed to the Server / Model with unknown consequences. It depends on how the Server interprets the event in relation to the Model.

Here’s an improvement, but vague when it comes to the details:

image

But it’s a step in the right direction. Let make it more specific by handling a single case:

image

This example only handles a mouse click, and the Server always responds in the same way; in a real application there would be a whole API of functions on the the Client. A left mouse click could occur in a variety of contexts causing the Server to respond in a variety of ways. Development consists of providing for new scenarios on the Server and new Client functions the Server can call to handle them.  More about that later.

This is probably impossible to do using HttpHandler and certainly more trouble than I’m going to deal with; fortunately there’s an alternative that not only makes it possible, but is ideally suited for the job and provides multi-user support.

image

Pushing To The Client

image

That second one is kind of huge, actually essential to do it right. The first one too, really. Otherwise the Client would have to be constantly polling the Server for changes. But SignalR does that for us, in the most efficient way it can find available.

Pushing From The Server Means No Expectations On The Client

Events on the Client are completely decoupled from the resulting View changes, if any.  The Client fires off an event to the Server with no associated function to call with the response. If there is a change, it’s up to the Server to call functions on the Client to update the View.

I’ve illustrated that approach in the diagram above that includes the newThing Client function. The Server determined there was a change to the Model due to the click, updated the Model appropriately, and then updated the View by calling the newThing function on the Client Hub. Of course so far we don’t really have a Model because the ooNaThing is not persisted.

The ability of SignalR to push to the Client makes it a natural fit for the View/Model Pattern in a Multiple user context.

image

No Need For Asynchronous Behavior

You don’t need asynchronicity because it’s implicit if the Client never has any expectations of a response from a Server method.

But that doesn’t mean it’s not available.

Is That It?

That’s where we’re headed. And beyond.

I say that, because in the course of making it all workable and almost as important, comprehensible, the project grew much larger than I had expected. It started with making sure all the function names were meaningful and snowballed from there; I ended up with what I hope is a useful framework for plugging in your own classes and behaviors.

But first…

A Detour To Describe A Pattern Variation I Won’t Be Using

This may be useful in some scenarios, but in the end I decided it was more trouble.

One last pattern that instead of an ‘API’ of functions to update the View, provides a single Client function for the server to call

image

It’s perhaps misleading to say the View implementation is independent from the server. The Server determines what the changes to the View will be, but leaves it to the Client to determine which local functions to call to implement the update to the View.

One Function on The Client to Update The View is Tempting, But Is It Really Beneficial?

At first I thought this would make it easier to maintain the client server relationship such that changes on the one were less likely to require changes on the other, but I’m not so sure. It moves the mapping of Client View update functions so they are called from one single function visible to the Server rather than making them visible so the Server can call them directly.  In the long run it means maintaining more JavaScript including the complication of the “changes” wrapper.

But as I say, there may be some scenarios where it’s useful. The real difference, coding complications aside

  1. The Server calls a specific function or functions appropriate to the changes that must be made to the View to reflect the model, for instance newOoNaThing().
  2. The Server calls a single function that must determine the changes that must be made to the View to reflect the model , for instance updateTheView().

No matter how you say it, all I hear for option 2 is “more JavaScript code and more detailed JavaScript logic”.

The Projects

As I mentioned before the detour, the project grew much larger than I expected, with too many levels of detail to describe in one article. So there will be three articles and three projects.

image

Beyond that, hopefully, one is are pretty much limited only by one’s imagination. I’ve tried to put in enough opportunities for injection to provide maximum flexibility in defining the game / simulation structure and ooNaThing behavior.  But I haven’t worked with it enough beyond the simple “Jump on the other guy” game to understand the limitations that may still be there.

Project 1: Bare-Bones ooNaWorld

This project will demonstrate the basic operations, ignoring some aspects of the View-Model relationship.

image

 

image

 

image

 

image

 

image

 

image

A real application would push the Model to the new user when they logged-in but that’s beyond the scope of this simple demonstration.

Bare-Bones SignalR

image

 

image

Let’s start with the client browser

Bare-Bones ooNaWorld View

image

More Specifically

The View will be hosted on a LightSwitch page, so let’s take a look at it.

image

Notifications is the control that displays the text received from the Server through the JavaScript notify() function. In the screenshots above, the message “Your ooNaThing is red” is sent from the Server to the individual client through notify().

More interesting is the ooNaView Custom Control, you can see the name above boxed in red. It’s caption is “ooNaWorld”. The Custom Control  can be used to host any amount of HTML, but in this project we’re just using it for the ooNaWorld Canvas.

The  “Canvas Creation Time” code I mentioned is in the render event for the ooNaView control. That code can be accessed through the “Edit Render Code” link, boxed in red above. The code itself can be seen below under the “Canvas Creation Time” caption.

Screen Creation Time

As the comments say, we define the HubClient proxy functions that the server can call here because some need access to the screen objects from within the function and we can provide that without creating a global variable.  That’s the only reason I hookup the HubClient functions here. Otherwise it could be done just before we start the Hub connection at Canvas Creation time.

Everything else is pretty straightforward. In the case of notify, a screen control is updated and in the other two cases local JavaScript functions are called. You could code it all right here, but in JavaScript especially, small and modular is best.

myapp.Main.created = function (screen) {
    // We define the local functions that can be called from the
    // server here because we need a reference to the screen within
    // some of those functions.
    //
    // Closure allows us to create a local variable here and
    // access it from within the functions we attach to
    // the hub client
    var clientScreen = screen;
    // We get the client node from the hub connection
    var hubClient = $.connection.ooBerHub.client;
    // Client function notify is called by server to show a message
    hubClient.notify = function (text) {
        clientScreen.Notifications = text;
    };
    // Client function moveOoNaThing is called by server with
    // ooNaThing to be moved as a parameter
    hubClient.moveOoNaThing = function (ooNaThing) {
        moveOoNaThing(ooNaThing);
    }
    // Client function newOoNaThing is called by server with
    // the new ooNaThing to be added as a parameter
    hubClient.newOoNaThing = function (ooNaThing) {
        drawOonaThing(ooNaThing);
    }
};

Canvas Creation Time

We create the ooNaWorld canvas when we render the Custom Control added to the screen for that purpose. Notice we call _ooNaWorldCanvas.addEventListener to attach a function to the canvas mousedown event:

myapp.Main.ooNaView_render = function (element, contentItem) {
    var HTML_STRING = $("<canvas id='ooNaWorld' width='500' height='300'></canvas>");
    $(element).append(HTML_STRING);
    _ooNaWorldCanvas = $("#ooNaWorld")[0];
    _ooNaWorldColor = "lightyellow";
    $(_ooNaWorldCanvas).css("backgroundColor", _ooNaWorldColor);
    _ooNaWorldCanvas.addEventListener("mousedown", onCanvasMouseDown, false);
    // The canvas is ready. We could not start the hub until now because
    // 1. We cannot call the hub server until it's ready, not immediately after we start it
    // 2. We know when the server is ready by attaching a function to the hub.start.done event
    // 3. The function attached to  the done event calls the server to create the ooNaThing
    // 4. The ooNaThing cannot be created until the canvas is ready
    // Ipso facto so there you go
    startHubConnection();
};

For reasons stated in the comments, after we create the canvas we start the hub server.

Starting the Connection

The only point of interest about starting the connection is the ability to have it run a function when it has started, or failed to start.

// Starts the hub and attaches a function to be run when start is complete
function startHubConnection() {
    $.connection.hub.start()
    .done(function () {
        // We can now make calls to the server, so create the ooNaThing
        var canvasRect = _ooNaWorldCanvas.getBoundingClientRect();
        var paramX = canvasRect.width / 2;
        var paramY = canvasRect.height / 2;
        $.connection.ooBerHub.server.createDefaultOoNaThing(paramX, paramY, "Circle");
    })
    .fail(function () {
        alert("Could not Connect! - ensure EnableCrossDomain = true");
    });
}

In our case, when start has succeeded, it calls createDefaultOoNaThing on the server, and we’re off!!!

The Mouse Click

Pretty simple.

// There was a left mouse click on the canvas. Send it to the hub server.
function onCanvasMouseDown(event) {
    var canvasRect = _ooNaWorldCanvas.getBoundingClientRect();
    var paramX = event.pageX - canvasRect.left;
    var paramY = event.pageY - canvasRect.top;
    $.connection.ooBerHub.server.leftMouseClick(paramX, paramY);
}

Drawing / Moving ooNaThings

Also pretty simple:

/ Move the image of ooNaThing from its previous location to its new location
function moveOoNaThing(ooNaThing)
{
    eraseOoNaThing(ooNaThing);
    drawOonaThing(ooNaThing);
}
// Erase by overdrawing with canvas color.
function eraseOoNaThing(ooNaThing) {
    // Using actual width leaves a trace outline
    var ERASE_FUDGE_FACTOR = 2;
    drawOoNaShape(ooNaThing, _ooNaWorldColor,
                    ooNaThing.PreviousLocation.x, ooNaThing.PreviousLocation.y,
                    ooNaThing.Width + ERASE_FUDGE_FACTOR, ooNaThing.Height);
}
// Draw as per the ooNaThing properties
function drawOonaThing(ooNaThing)
{
    drawOoNaShape(ooNaThing, ooNaThing.Color,
                    ooNaThing.Location.x, ooNaThing.Location.y,
                    ooNaThing.Width, ooNaThing.Height);
}
// Common drawing function. All ooNaThing properties can be specified differently
// from actual ooNaThing except for shape. 
function drawOoNaShape(ooNaThing, color, xCoordinate, yCoordinate, width, height) {
    // Avoiding mysterious unnamed arguments
    var ENDING_ANGLE_OF_FULL_CIRCLE = 2 * Math.PI;
    var STARTING_ANGLE_OF_FULL_CIRCLE = 0;
    var context = _ooNaWorldCanvas.getContext("2d");
    context.beginPath();
    context.fillStyle = color;
    switch (ooNaThing.Shape) {
        case "Circle":
            context.arc(    xCoordinate, yCoordinate, // location
                            width / 2, // radius
                            STARTING_ANGLE_OF_FULL_CIRCLE,
                            ENDING_ANGLE_OF_FULL_CIRCLE);
            context.fill();
            break;
        case "Rectangle":
            context.fillRect(xCoordinate - width / 2,
                            yCoordinate - height / 2,
                            width, height);
            break;
        default:
            alert("Unknown Shape");
            break;
    }
    context.closePath();
}

Here’s The ooNaThing

Literally everything in ooNaWorld is an ooNaThing. Everything knowable about ooNaWorld can be derived from the collection of ooNaThings and their properties.

namespace ooNaClasses
{
    [Serializable]
    public class ooNaThing
    {
        [Serializable]
        public class Position
        {
            public int x { get; set; }
            public int y { get; set; }
        }
        // The creator can never change
        public readonly string Creator;
        public Position Location { get; set; }
        public Position PreviousLocation { get; set; }
        public int Height {get; set;}
        public int Width {get; set;}
        public string Color { get; set; }
        public string Shape { get; set; }
        public ooNaThing(string creator, int xLocation, int yLocation, int height, int width, string color, string shape)
        {
            Creator = creator;
            Location = new Position() { x = xLocation, y = yLocation };
            PreviousLocation = new Position { x = xLocation, y = yLocation };
            Height = height;
            Width = width;
            Color = color;
            Shape = shape;
        }
    }
}

All the properties are pretty much self-documenting except perhaps Creator and PreviousLocation.

ooNaThing.Creator is what it says, but why does it exist? I’m using it to save the connection id of the client for whom it was created, but to the ooNaThing class itself it’s just an arbitrary field. I called it Creator and made it readonly so it would have a meaning within the class itself, although there’s nothing to force the caller to use it correctly. There’s also nothing to make it unique, although in our simple demo it always will be.

ooNaThing.PreviousLocation allows the client to erase the image of the ooNaThing from the View when it is moved from one location to another.

ooBer: The Model

image

ooBer provides methods to manipulate the Model and return the changes.

namespace LightSwitchApplication
{
    public class ooBer
    {
        // ooBer is a singleton meaning it cannot be instantiated by a caller. Instead the static
        // method, Instance returns the one and only instantiation referenced by _instance.
        public static ooBer Instance
        {
            get {return _instance.Value;}
        }
        
        // 'Lazy' delays initialization of _instance until the first time it's asked for
        private readonly static Lazy<ooBer> _instance = new Lazy<ooBer>(() => new ooBer());
        // _ooNaThings is a list containing all the ooNaThings in the ooNaverse.
        //
        // Since ooBer will be called from many threads _ooNaThings needs to be lockable. 
        // I'm not going to get into that for this bare-bones example. But if you're 
        // impatient, just let me say that ReaderWriterLockSlim is a great solution
        private List<ooNaThing> _ooNaThings = new List<ooNaThing>();
        // Return a new ooNaThing initialized with the requested values
        public ooNaThing CreateOoNaThing
            (string userId, Double xCoordinate, Double yCoordinate, string color, string shape)
        {
            int xPoint = Convert.ToInt32(xCoordinate + 0.5);
            int yPoint = Convert.ToInt32(yCoordinate + 0.5);
            ooNaThing newThing = new ooNaThing(userId, xPoint, yPoint, 30, 30, color, shape);
            _ooNaThings.Add(newThing);
            return newThing;
        }
        // Return an ooNaThing with its location changed to the requested values
        public ooNaThing MoveOoNaThing
            (string userId, Double xCoordinate, Double yCoordinate)
        {
            int xPoint = Convert.ToInt32(xCoordinate + 0.5);
            int yPoint = Convert.ToInt32(yCoordinate + 0.5);
            var theThing = _ooNaThings.Where(o => o.Creator == userId).FirstOrDefault();
            if (theThing != null)
            {
                theThing.PreviousLocation.x = theThing.Location.x;
                theThing.PreviousLocation.y = theThing.Location.y;
                theThing.Location.x = xPoint;
                theThing.Location.y = yPoint;
            }
            return theThing;
        }
        // Provide a private default constructor to prevent a user from instantiating ooBer
        private ooBer() { }
    }
}

ooBer is a singleton, as described in the code comments. Each client will get its own hub, but every hub will be making calls into a single instance of ooBer, who maintains the one and only model of ooNaWorld, a simple list of ooNaThings.

ooBer knows how to create an ooNaThing and add it to ooNaWorld and how to change the location of an ooNaThing in ooNaWorld. Pretty simple.

ooBerHub: The Middle Man With Too Many Responsibilities

image

In the end, ooBerHub will just be there for its connectivity. It will receive calls from the client and pass them on. But for now…

image

Way too many. Also we won’t really have the Model receive requests from the View, such as createDefaultOoNaThing . It’s possible to write methods that return a value, so that createDefaultOoNaThing could return the ooNaThing from the method. But that doesn’t fit into the View/Model scenario. Simply put, the View cannot have “intent” – something happened on the View that gets transmitted. Something may happen on the View as a result; the View has no concept of the linkage. But that’s later.

namespace LightSwitchApplication
{
    public class ooBerHub : Hub
    {
        // ooBer is the repository of the ooNaVerse (the Model) and controls all actions within. The
        // hub receives messages from the clients (Views) and passes them to ooBer, the broadcasts the
        // result to all the clients. There can be variants, but for our purposes that's the pattern.
        private readonly ooBer _ooBer;
        // Constructor initializes the _ooBer reference to point to the ooBer singleton object
        public ooBerHub() 
        {
            _ooBer = ooBer.Instance;
        }
        // Easy way to assign colors for bare-bones demo
        private static List<string> _ooNaThingColorList = new List<string>() { "Red", "Green", "Pink", "Blue", "Orange", "Black" };
        private static int _colorMax = _ooNaThingColorList.Count() - 1;
        private static int _colorIndex = 0;
        // This method is called by a client to create an ooNaThing at the requested coordinates.
        public void createDefaultOoNaThing(Double xCoordinate, Double yCoordinate, string shape)
        {
            // Assign the next color
            string color = _ooNaThingColorList[_colorIndex++];
            if (_colorIndex > _colorMax)
                _colorIndex = 0;
            // Get the new ooNaTHing from ooBer
            ooNaThing newThing = _ooBer.CreateOoNaThing(Context.ConnectionId, xCoordinate, yCoordinate, color, shape);
            // Broadcast to all clients
            Clients.All.newOoNaThing(newThing);
            // Send a message to the caller
            Clients.Caller.notify(string.Format("{0}: Your ooNaThing is {1}", DateTime.Now, newThing.Color));
        }
        // This method is called when there is a left mouse click on the "surface" of ooNaWorld. The
        // id of the client who owns the thing and the coordinates are passed to ooBer. The 
        // moved thing returned by ooBer is broadcast to all the clients.
        public void leftMouseClick(Double xCoordinate, Double yCoordinate)
        {
            ooNaThing movedThing = _ooBer.MoveOoNaThing(Context.ConnectionId, xCoordinate, yCoordinate);
            Clients.All.moveOoNaThing(movedThing);
        }
    }
}

Bare-Bones Summary

There are still problems, even with this simple implementation. Overlaid ooNaThings don’t get refreshed:

image

When you first arrive at the screen, you don’t see pre-existing ooNaThings until their owners cause them to move:

image

But these are errors of omission which can be easily fixed. More important is that everything is too tightly coupled for this code to be useful beyond writing a single application. In the next project we’ll add classes and interfaces to break this coupling and provide for dependency injection.

Download Code

The project will be available at http://lightswitchhelpwebsite.com/Downloads.aspx within hours.

Tags:
Categories:

Your name:
Gravatar Preview
Your email:
(Optional) Email used only to show Gravatar.
Your website:
Title:
Comment:
Security Code
CAPTCHA image
Enter the code shown above in the box below
Add Comment   Cancel 

Microsoft Visual Studio is a registered trademark of Microsoft Corporation / LightSwitch is a registered trademark of Microsoft Corporation