Beginner’s Guide to Using SignalR via ASP.NET

0
46

Introduction

I’ve  been wondering about how easy it might be to use SignalR so I decided to work through one of Microsoft’s quick intro tutorials.  While doing that I ran into some challenges so I wanted to take a shot at explaining how SignalR works myself.

The Main Point : Updating Remote Clients

The entire point of using a technology like SignalR is updating remote clients.  I wanted a simple example that would allow readers to see how it might work.  A while back I used Firebase (more on this in a moment) to create a simple example which :

  1. allows user to grab and move a pawn on an HTML5 Canvas
  2. updates all clients so remote users can see the pawn moving in their browser

Here’s a simple example.  The browser on the left is Google Chrome and the one of the right is Microsoft Edge.  Both are pointed at the same URL which is my site hosted at SmarterASP.net – Unlimited ASP.NET Web Hosting[^].  I got a 60 day free trial there and you can too – no credit card required.  More on why I’m using SmarterASP.net later.

main signal example

You Can Try It Right Now

Just open up two different browser windows and point them at my SmarterASP.net site: http://raddevus-001-site1.btempurl.com/[^]

Next, move one of the pawns in either of the browser windows and it will move in the other.

Firebase Comparison

I’ve already written the app I show you in this article as a Firebase[^] app.  You can see that example at: 

http://uncoveryourlife.com/html5/pawns/pawns.htm[^]  This one runs at my GoDaddy.com site.  More about the struggles I had getting GoDaddy to host a SignalR app, later.

I’ve worked with Firebase fairly extensively so I was very interested to see how SignalR would compare in relation to:

  1. development ease / speed
  2. how well it works – updates the remote client and performance (update remote client speed)

Firebase (More Responsive)

If you tried both of them, I’m sure you saw that the Firebase one is far more responsive.  You can see quite a lag even in the example gif above which was recorded from the SignalR version which was written for this article.

Now that you’ve seen it work, let’s set up our SignalR project in Visual Studio and get started.

Visual Studio ASP.NET Project

As I stated earlier, I learned SignalR by working through one of Microsoft’s tutorials.  You can see that tutorial at: Tutorial: Getting Started with SignalR 2 | Microsoft Docs[^].  However, that article presents a few challenges.

  1. It’s an older article and uses Visual Studio 2013
  2. It has you add Owin packages via Nuget and then separately add the SignalR packages.  But now in Visual Studio 2015 (or 2017, which I’m using in this article) it’s a bit different.  I’ll show you.

Fire up Visual Studio and create a new project.

Choose the Web project type on the left and  choose the ASP.NET Web Application (.NET Framework) on the right side.

new project

 Name the project pawns to keep it simple and click the [OK] button.

A dialog box will appear.  We don’t need a lot in this project and we’re going to add the SignalR libraries using Nuget, so choose the Empty Project and click the [OK] button.

empty project

Once the template project is created, choose the Tools… menu item at the top of Visual Studio.

Slide down to the Nuget Package Manager menu item.  Another menu will pop out.  Choose the  Manage Nuget Packages for Solution… menu item.

start nuget

A very ugly window will open up in Visual Studio.  On mine the window is very small and you can hardly see anything.   Microsoft isn’t something less than amazing when it comes to UI / UX, right?

nuget ugly

by default the Installed item at the top will be selected.  Go ahead and :

  1. click the Browse item
  2. click inside the search edit box
  3. type “signalr” to search for the SignalR libraries.
  4. You should see some SignalR choices appear in the bottom portion of the Nuget window.
  5. (single) Click on the first choice shown — it should be Microsoft.AspNet.SignalR 

nuget needs to be larger

At this point I had to move slider bars around so I could actually see the stuff I needed on the screen.  Go ahead and do that if you have to, because on the right we need to click a button we cannot even see right now so we can add the SignalR library.

  1. Select the checkbox next to the Pawns project. (This will enable the [Install] button so you can click it.)
  2. Click the [Install] button to add the SignalR libraries to the project.

select pawns project - nuget

A dialog box will pop up giving you a preview of all the libraries which will be added to your project.

preview libraries

Click the [OK] button and all the libraries will be added.  Note: a license acceptance dialog also pops up to insure you accept the license for use.  

Nuget Handles Dependencies

In the Microsoft tutorial they have you add the OWIN library separately, but you don’t really need to do that now because Nuget adds all dependencies for you.  

Get The Source – v001

If you have any troubles with the project creation you can grab the v001 zip file at the top of this article and unzip it and you’ll be all set.  Of course, I deleted the downloaded nuget packages so the zip is just the source, but all you have to do is restore packages from Visual Studio and you’ll be ready to continue this article.

Once you’ve done all the previous steps, you have everything you need to use SignalR in your project.

Now, let’s add some code.  

Add New HTML Page

In the Microsoft tutuorial the first thing the author tells you to do is add a new class that implements the SignalR behavior.  However, I like to build the thing as we go to see how it all works together.  So first, we’ll set up the HTML page that we’ll use as our app’s user interface.

Go ahead and add a new web page to your project.  You can simply right-click your solution in the Solution Explorer.  Next, slide down to the Add menu item.  And finally select the HTML page menu item.

add new html page

A dialog box will pop up.  Type the name index.htm in there and click the [OK] button.  Yes, I use the 3-letter extension instead of a full 4-letters of HTML.  

index.htm

When you add the file, Visual Studio will add a basic skeleton HTML file which will only display a blank page to the user.  Of course, for our purposes we want to display three different colored pawns on a blue grid background.

Let’s alter the skeleton that Visual Studio provides for us by replacing it with the following code now:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>pawns</title> </head> <style> body, html { margin: 0; padding: 0; } </style> <body> <img style="visibility:hidden;display:none;" src="assets/redBlueGreenPawn.png" id="allPawns" /> <canvas id="gamescreen">You're browser does not support HTML5.</canvas> </body> </html>

Normally, I’d keep the CSS (Cascading Style Sheet) data in a separate file but I’m attempting to simplify this tutorial and there is really just the one style which removes any margin or padding so I’ve added into our index.htm.

Image Asset

Next, you see that I reference an image that you don’t have.  You can see it here and download it, if you like:

red green blue pawn

You can see that even though there are three distinct pawns you can move around, the image itself is actually one PNG file.  That’s because of how you can easily reference portions of an image as separate HTML5 Canvas objects with the Canvas API.

Adding Assets Folder

I will also add a new folder to the project named \assets and I will place the redGreenBluePawn.png in the folder so it is a part of the project.  You’ll get that file in the next (v002) download. 

Finally, we set up a Canvas element where our grid and pawns will be drawn.

Real Work, Done In JavaScript

The real work is done by JavaScript, however.  I know that many of you feel about that, but it is the way of the Web so get used to it. 🙂 

We need to add some references to some scripts that are provided to us by Visual Studio project.  Visual Studio allows you to drag and drop items to reference them.   We need to add some references to the jQuery libraries now.  Here’s how you can do it in Visual Studio.

Adding JavaScripts References

In solution explorer expand the \Scripts folder and you’ll see a list of JavaScript files that Visual Studio added to the project for you.  Actually, I believe Nuget added the jQuery ones because it new you’d need them for use with SignalR.

To add a reference to any of those, you simply click on it and drag it over to your source file.  When you let go it’ll drop the correct script tag in as shown in the next image.

add scripts

Go ahead and add a reference to the jquery.signalR-2.2.2.min.js also.  These are the minified versions of the JavaScript libraries that we need to get this working.

I like to keep my custom JavaScript separate so I’m going to add a folder and create a new Javascript file named pawns.js.  I’ll show it to you and let you work out how to do this same thing.   Take a close look of where I add the pawns.js reference in my HTM file.  It’s a bit important because that code references the Canvas and we are insuring that the Canvas element is already loaded.

Strange SingnalR Reference

Also notice the line that I’ve bolded in the HTML example below.  It is a somewhat strange reference to a file that does not exist.  That is part of how Microsoft shows you in the tutorial to reference SignalR.  The code that will be referenced will be generated at runtime.  That’s because the code is created by the SignalR C# libraries.  We’ll see more later on in the article.  For now, make sure you add that reference.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>pawns</title> <script src="Scripts/jquery-1.6.4.min.js"></script> <script src="Scripts/jquery.signalR-2.2.2.min.js"></script> <script src="signalR/hubs"></script> </head> <style> body, html { margin: 0; padding: 0; } </style> <body> <img style="visibility:hidden;display:none;" src="assets/redBlueGreenPawn.png" id="allPawns" /> <canvas id="gamescreen">You're browser does not support HTML5.</canvas> <script src="js/pawns.js"></script> </body> </html>

Get the Code v002

You can also download v002 of the code at the top of this article and you’ll be up to date at this point and ready to write the code to draw the pawns and grid.

The code will build and run but of course the displayed page will be completely blank.  Let’s take a look at how to draw the pawns and grid.  

Drawing Pawns & Grid

I’m going to race through most of this, since it is only indirectly a part of what I want to talk about.  

First of all I need to set up some variables I will need to use and set up the load event which will fire once the browser has loaded the target page (index.htm) and all of its associated resources (images and JavaScript files).

 var ctx = null; var theCanvas = null; var firebaseTokenRef = null; window.addEventListener("load", initApp); var mouseIsCaptured = false; var LINES = 20; var lineInterval = 0; var allTokens = []; var hoverToken = null; var pawnR = null; function token(userToken){ this.size = userToken.size; this.imgSourceX = userToken.imgSourceX; this.imgSourceY = userToken.imgSourceY; this.imgSourceSize = userToken.imgSourceSize; this.imgIdTag = userToken.imgIdTag; this.gridLocation = userToken.gridLocation; } function gridlocation(value){ this.x = value.x; this.y = value.y }

I also add a couple of types I’ve created (token and gridLocation) to make it easier to keep track of things.  You’ll see how their used a bit later.

When the browser loads everything the initApp() function will run.  Let’s take a look at it.

function initApp() { theCanvas = document.getElementById("gamescreen"); ctx = theCanvas.getContext("2d"); ctx.canvas.height = 650; ctx.canvas.width = ctx.canvas.height; initBoard(); }

We begin to set up the Canvas where we’ll draw the grid.

Next, we call our custom code in initBoard().

function initBoard(){ lineInterval = Math.floor(ctx.canvas.width / LINES); console.log(lineInterval); initTokens(); }

I calculate the line intervals using the canvas width and the distance between the lines.  After that I call initTokens() to get ready to draw the pawns (tokens).

function initTokens(){ if (allTokens.length == 0) { allTokens = []; var currentToken =null; for (var i = 0; i < 3;i++) { currentToken = new token({ size:lineInterval, imgSourceX:i*128, imgSourceY:0*128, imgSourceSize:128, imgIdTag:'allPawns', gridLocation: new gridlocation({x:i*lineInterval,y:3*lineInterval}) }); allTokens.push(currentToken); } console.log(allTokens); } draw(); }

Here, we just make sure the allTokens array is initialized to empty.  Next we add all the tokens to the array while sourcing the correct portion of our PNG image.  It’s easy to do with some math since each one is 128 pixels wide.

Finally, we call our draw() function which will draw all of our graphics to our Canvas element.

function draw() { ctx.globalAlpha = 1; ctx.fillStyle = "white"; ctx.fillRect(0, 0, ctx.canvas.height, ctx.canvas.width); for (var lineCount = 0; lineCount < LINES; lineCount++) { ctx.fillStyle = "blue"; ctx.fillRect(0, lineInterval * (lineCount + 1), ctx.canvas.width, 2); ctx.fillRect(lineInterval * (lineCount + 1), 0, 2, ctx.canvas.width); } for (var tokenCount = 0; tokenCount < allTokens.length; tokenCount++) { drawClippedAsset( allTokens[tokenCount].imgSourceX, allTokens[tokenCount].imgSourceY, allTokens[tokenCount].imgSourceSize, allTokens[tokenCount].imgSourceSize, allTokens[tokenCount].gridLocation.x, allTokens[tokenCount].gridLocation.y, allTokens[tokenCount].size, allTokens[tokenCount].size, allTokens[tokenCount].imgIdTag ); } if (hoverToken !== null) { ctx.fillStyle = "yellow"; ctx.globalAlpha = .5 ctx.fillRect(hoverToken.gridLocation.x, hoverToken.gridLocation.y,   hoverToken.size, hoverToken.size); ctx.globalAlpha = 1; drawClippedAsset( hoverToken.imgSourceX, hoverToken.imgSourceY, hoverToken.imgSourceSize, hoverToken.imgSourceSize, hoverToken.gridLocation.x, hoverToken.gridLocation.y, hoverToken.size, hoverToken.size, hoverToken.imgIdTag ); } }

Basically, all we do is :

  1. iterate through the allTokens array
  2. draw token in its present location, according to its gridLocation value
  3. If the token is being hovered over, then draw a light yellow shadow around it so the user can tell she can grab the token.

The draw() function does use another helper method named drawClippedAsset() which allows me to easily reference the tokens in our image.  It looks like the following:

function drawClippedAsset(sx,sy,swidth,sheight,x,y,w,h,imageId) { var img = document.getElementById(imageId); if (img != null) { ctx.drawImage(img,sx,sy,swidth,sheight,x,y,w,h); } else { console.log("couldn't get element"); } }

Once you add all of this code you will finally have the background grid drawn and the pawns drawn at their initial location.  

initial view

Get the Code : v003

If you get the code v003 at the top of this article you will be up to date so you can continue along with the article.

Note: At this point I had to put a special addition into the web.config file because for some reason I was getting an odd error where OWIN was trying to run automatically.  I’m sure it is related to the library being added with the Nuget of the SignalR.  The line I added looks like:

<appSettings> <add key="owin:AutomaticAppStartup" value="false" /> </appSettings>

This seems to keep the thing running, otherwise OWIN attempts to load and the app crashes.

Where Are We?

We’re now very close to getting things going with the SignalR, but first we have to add the local code which will allow us to grab one of the pawns and move it around. Once we do that, we will allow the updated values to be broadcast to other clients.

Let’s add the code to do that work now.  Yes, it’s still more JavaScript.

We need some event handlers which will do some work when the mouse is clicked (mousedown) and when the mouse is moved.  

Back in our initApp() function we want to add the event listeners for those two events.  Now initApp() will look like the folllowing: (I’ve added the bolded lines)

function initApp() { theCanvas = document.getElementById("gamescreen"); ctx = theCanvas.getContext("2d"); ctx.canvas.height = 650; ctx.canvas.width = ctx.canvas.height; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mousedown", mouseDownHandler);  initBoard(); }

This is the straight-up pure JavaScript way to add those listeners.  You can do it with jQuery too, but it’s easy enough to do it like this.

Now, we’ve registered the event listeners, we need to implement the methods handleMouseMove() and mousedownHandler().

function handleMouseMove(e) { if (mouseIsCaptured) { if (hoverItem.isMoving) { var tempx = e.clientX - hoverItem.offSetX; var tempy = e.clientY - hoverItem.offSetY; hoverItem.gridLocation.x = tempx; hoverItem.gridLocation.y = tempy; if (tempx < 0) { hoverItem.gridLocation.x = 0; } if (tempx + lineInterval > 650) { hoverItem.gridLocation.x = 650 - lineInterval; } if (tempy < 0) { hoverItem.gridLocation.y = 0; } if (lineInterval + tempy > 650) { hoverItem.gridLocation.y = 650 - lineInterval; } allTokens[hoverItem.idx]=hoverItem; pawnR.server.send(hoverItem.gridLocation.x, hoverItem.gridLocation.y,hoverItem.idx); } draw(); } else { hoverToken = hitTestHoverItem({x:e.clientX,y:e.clientY}, allTokens); draw(); } }

Any time the mouse is moved this function will run.  I could’ve been more specific and said, only run when the mouse is moved and the mouse is over the Canvas element, but this will work.

The first thing I do is check if mouseIsCaptured is true.  That value gets set when the mouseDownHandler fires (user clicks) and the method determines that the user is above one of the three pawns.  That work is done in the mouseDownHandler so let’s take a look.

function mouseDownHandler(event) { var currentPoint = getMousePos(event); for (var tokenCount = allTokens.length - 1; tokenCount >= 0; tokenCount--) { if (hitTest(currentPoint, allTokens[tokenCount])) { currentToken = allTokens[tokenCount]; currentToken.offSetX = currentPoint.x - currentToken.gridLocation.x; currentToken.offSetY = currentPoint.y - currentToken.gridLocation.y; currentToken.isMoving = true; currentToken.idx = tokenCount; hoverItem = currentToken; console.log("b.x : " + currentToken.gridLocation.x + "  b.y : "   + currentToken.gridLocation.y); mouseIsCaptured = true; window.addEventListener("mouseup", mouseUpHandler); break; } } }

In the mouseDownHandler we simply iterate through the allTokens array and check their gridLocation.  if the we determine that the mouse pointer is within that area we set the mouseIsCaptured boolean to true.

I’ve broken out the code that checks to see if the mouse location is within any one of the tokens location and placed that code in a a method called hitTest().

function hitTest(mouseLocation, hitTestObject) { var testObjXmax = hitTestObject.gridLocation.x + hitTestObject.size; var testObjYmax = hitTestObject.gridLocation.y + hitTestObject.size; if ( ((mouseLocation.x >= hitTestObject.gridLocation.x) && (mouseLocation.x = hitTestObject.gridLocation.y) && (mouseLocation.y <= testObjYmax))) { return true; } return false; }

You can see we simply send in the mouseLocation and the object you want to test and the function iterates through and determines if it is a hit or not and returns true or false.  It makes it all very easy to use.

There are a couple other helper methods in there which will decide which pawn is being grabbed and which will continually call the draw() method so that the pawn is drawn as the mouse is moved.  At this point the user can grab any one of the pawns and move it around on the screen.

Get Code: User Can Grab & Drag Any Pawn v004

Download the v004 zip file at the top of this article and you can try dragging the pawns around.

drag pawns

Finally, We Are At the Main Challenge

We are finally ready to attempt to resolve the main challenge.  What we want to do is :

Main Challenge

Update every client that happens to be looking at our web page so that when one of the pawns is moved all other clients will see the pawn move.

To get this going the first thing we have to do is initialize the SignalR startup.  The original Microsoft article tells us that we have to add a Startup.cs which does this work.

Of course, it doesn’t have to be named Startup, but it does need to be a new class in the project.  Go ahead and right-click the Pawns project in solution explorer and choose the Add menu item that appears and then choose Class…  A dialog will popup so you can add the name of the class you want to create.  Go ahead and name it Startup.cs and click the [OK] button.

The file will be added to your project and some template code will be added.  Go ahead and replace the code in that file with the following:

using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(Pawns.Startup))] namespace Pawns { public class Startup { public void Configuration(IAppBuilder app) { app.MapSignalR(); } } }

This code initializes the OwinStartup and maps the SignalR code so that it can build the JavaScript that you will need.  It’s basic boilerplate code to get everything ready.  Now we can write some code to do something.

What We Want Our App To Do

What we want to do is send as little data to the other browsers as possible.  That means we’d like to just send the gridLocation information of the moving pawn to other browsers.  That way as the local Pawn moves around and it’s location gets updated then pawns in other clients are also updated.

SignalR Hub Class : C# Generates JavaScript

To do this work, we need to generate a SignalR Hub class that will send the data to the other browsers.  

Now, we add another class to our project and this time we name it PawnHub.  Notice in the next code snippet that we derive our PawnHub from the Hub type, which is a special SignalR library type which provides some special abilities.

Once you add that class, replace all of its code with the following:

using Microsoft.AspNet.SignalR; namespace Pawns { [Microsoft.AspNet.SignalR.Hubs.HubName("pawnHub")] public class PawnHub : Hub { public void Send(int x, int y, int idx) { Clients.Others.broadcastMessage(x, y, idx); } } }

This is the piece that binds the C# to the JavaScript.  When you build and run this code, it will look for a special JavaScript object named pawnHub.  When it finds that pawnHub object and the Send() method is called, it will call the broadcastMessage() method passing along the values that we have provided to it.

Three Parameters Are Customizable

The three parameters are customizable. I chose to send in the x and y locations of the object and the idx of the object from our allTokens array.  

This is a small amount of data to broadcast (just three integers) and it makes it quite easy to handle the code in JavaScript.  Let’s go add the JavaScript which will update all other client browsers now, so that when you move the pawn in one browser all others will be updated.

The Final Touches Using JavaScript 

Back in our initApp() we need to initialize our 

The initApp() will now look like the following (I’ve added the bold code):

function initApp() { theCanvas = document.getElementById("gamescreen"); ctx = theCanvas.getContext("2d"); ctx.canvas.height = 650; ctx.canvas.width = ctx.canvas.height; window.addEventListener("mousemove", handleMouseMove); window.addEventListener("mousedown", mouseDownHandler); pawnR = $.connection.pawnHub; pawnR.client.broadcastMessage = function (x, y, idx) { allTokens[idx].gridLocation.x = x; allTokens[idx].gridLocation.y = y; draw(); };     $.connection.hub.start().done(function () {         console.log("Hub is started.");     }); initBoard(); }

We simply set up our pawnR which is our variable to hold our SignalR object.  As you can see, we’ve initialized it with a copy of the pawnHub object.  This $.connection.pawnHub is in code that is generated for us by the compiler.  That’s simply the syntax you use to get to that object.

After you set up the method, you have to start the hub by calling the start() method.  When it completes it will run a function and here I have just added code to display a message in the console.log()

This is the same example that you would see in the Microsof tutorial.

Define Method For Broadcast Message

Once we do that, we have to tell the pawnHub object which JavaScript function to call when the client sends a broadcast message.  You can see we have done that on the next line with the anonymous function which simply takes the values sent (remember the C# PawnHub Send() method?) — in our case, x,y and idx.  It uses those values to update the appropriate token (from allTokens) and then calls draw() so that on the client, the pawn will move around (be drawn again) as the remote user moves the pawn.  It’s that simple.

Now, we just need to add one line of code so that the data is sent when the user drags a pawn around. Since that code should run when the mouse is moved, we’ll update the onMouseMove handler.   Here’s the code with the one additional line of code bolded.  

You can see that the method is named Send() and takes three parameters.  That matches the definition of our Send() method in our PawnHub.  We’ve mapped that PawnHub method to a JavaScript client method so that when it is called and broadcasts the message then all clients are updated.  This gives us the entire solution.

function handleMouseMove(e) { if (mouseIsCaptured) { if (hoverItem.isMoving) { var tempx = e.clientX - hoverItem.offSetX; var tempy = e.clientY - hoverItem.offSetY; hoverItem.gridLocation.x = tempx; hoverItem.gridLocation.y = tempy; if (tempx < 0) { hoverItem.gridLocation.x = 0; } if (tempx + lineInterval > 650) { hoverItem.gridLocation.x = 650 - lineInterval; } if (tempy < 0) { hoverItem.gridLocation.y = 0; } if (lineInterval + tempy > 650) { hoverItem.gridLocation.y = 650 - lineInterval; } allTokens[hoverItem.idx]=hoverItem; pawnR.server.send(hoverItem.gridLocation.x, hoverItem.gridLocation.y,hoverItem.idx); } draw(); } else { hoverToken = hitTestHoverItem({x:e.clientX,y:e.clientY}, allTokens); draw(); } }

Complete Solution : v005 

Get the complete solution in v005 of the download at the top of this article.

Build and run and then open two separate browser windows pointing to your same URL and move a pawn.  It will move in the other browser window too.

Please Note

I had to remove the following code from the web.config file so that hub would start.  If you leave it in, you will get errors. 

<appSettings> <add key="owin:AutomaticAppStartup" value="false" />
</appSettings>

Look How Fast It Is Running Local

One more animated gif to show how fast it updates running local.

final app running local

There were definitely challenges getting this working.  

Refreshing Page Resets Pawn Locations

Also, if you refresh the page the pawns always move back to their original locations. That’s because I did not do any persistence of those values to a data store.  If you compare this SignalR solution to my Firebase solution you will see that the Firebase one always keeps the locations of the pawns.  That’s because the Firebase solution inherently solves this by providing and object database where your items are stored remotely.

GoDaddy Issues / Deploy

I never could get it working on my GoDaddy hosted site.  That’s because the JavaScript seems to be generated on the fly in a virtual directory or something using that SignalR/Hubs reference.  I never could figure it out.  But I did not have to do anything special at all to get it working on the SmarterASP.net site.  If you have trouble deploying your SignalR apps definitely find out what your Web hosting service does.

I hope you found this article a helpful example and introduction to using SignalR.

Launching My Sci-Fi Writing Career  🙂

I am also announcing my entry into writing science fiction.  I am going to blog my book, Robot Hunters: Divided Resistance (Book 1 in the trilogy).  You can read the first chapter at my web site / blog:  http://robothunters.us/post/2017/05/22/chapter-1-enter-the-robot-compound^

It’s rough and not quite finished but maybe you’re a sci-fi fan who is interested in writing as I am.  Thanks for checking it out.

History

First version : 05/22/2017

LEAVE A REPLY