By Daniel Du
This is my another practice with Windows Azure and WebGL, an AutoCAD Command Usage Statistics “Magic Ball” on Cloud. The idea is to monitor user’s command usage while he is drafting and save it up to cloud, then view the statistics in web page with a rotatable ball, I call it “magic ball”, it does not look like a ball just because I am lazy not to draft much to use many AutoCAD commands.
It contains three parts – an AutoCAD plugin to monitor command usage statistics, an web service on Windows Azure to receive and save the command statistics, and a Web page client to render the statistics as magic ball with HTML5/WebGL. In this solution, it contains 5 projects, AcadCommandViewer is the service and web site, which is an ASP.NET MVC website on windows Azure. AcadCommandViewerConsole is the AutoCAD plugin.
In this post, I will introduce how to implement the server side. You probably is thinking to create a Windows Azure project in Visual Studio with Azure SDK. Yes, you are correct, but I found that it is not efficient to launch the project to test when developing, even with Azure Emulator. As it is easy to migrate an ASP.net application to Azure latter, I’d rather start from a simple ASP.net MVC Application first. So I created an ASP.net MVC 4 Web Application in Visual Studio.
First step is to create the Model of ASP.net MVC, by adding a class in Models folder of web application, but considering that the data model will also be used in AutoCAD plugin project as well, so I did some refactoring work to make the model a separate project. It is a very simple one, just one class, code goes as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AcadCommandViewer.Models
{
public class UserCommandsHit
{
public int Id { get; set; }
public string UserName { get; set; }
public virtual ICollection<CommandHit> CommandHits { get; set; }
}
public class CommandHit
{
public int Id { get; set; }
public string CommandName { get; set; }
public int HitNumber { get; set; }
}
}
Model is OK now, next step is to create controller. In Visual Studio Solution Explorer, right click “Controllers” folder, Add –> Controller, I choose “API controller with read/write actions, using Entity Framework” template, which creates a controller automatically with ASP.NET web API and Entity Framework.
Select a “<New data context>” to create a new data context automatically.
If the your model class does show up in “Model class” list, you need add reference to your model project “AcadCommandViewerModels”.
The automatically created controller is pretty good, but there are some minor problems with update action. Entity Framework does not update the children elements, so we need to do some changes to the code, please refer to the code and comments inline:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using AcadCommandViewer.Models;
namespace AcadCommandViewer.Controllers
{
public class AcadCommandsController : ApiController
{
private AcadCommandViewerContext db = new AcadCommandViewerContext();
// GET api/AcadCommands
public IEnumerable<UserCommandsHit> GetCommandsDatas()
{
return db.UserCommandsHits.OrderBy(p => p.UserName)
.AsEnumerable();
}
// GET api/AcadCommands/5
public UserCommandsHit GetCommandsData(int id)
{
UserCommandsHit commandsdata = db.UserCommandsHits.Find(id);
if (commandsdata == null)
{
throw new HttpResponseException(
Request.CreateResponse(HttpStatusCode.NotFound));
}
return commandsdata;
}
// GET api/AcadCommands?username=Daniel
public UserCommandsHit GetCommandsData(string userName)
{
UserCommandsHit commandsdata = db.UserCommandsHits
.FirstOrDefault<UserCommandsHit>(p =>
p.UserName.ToUpper() == userName.ToUpper());
if (commandsdata == null)
{
throw new HttpResponseException(
Request.CreateResponse(HttpStatusCode.NotFound));
}
return commandsdata;
}
// PUT api/AcadCommands/5
public HttpResponseMessage PutCommandsData(int id,
UserCommandsHit commandsdata)
{
if (ModelState.IsValid && id == commandsdata.Id)
{
var usrCmdHitInDb = db.UserCommandsHits.Find(id);
//http://stackoverflow.com/questions/7968598/entity-4-1-updating-an-existing-parent-entity-with-new-child-entities
//SetValues never updates navigation properties.
//EF doesn't have any magic to update the children
// - which means: adding new children, deleting
// removed children, updating existing children
// this procedure forces you to delete the old
// children also from the database and insert the new one
db.Entry(usrCmdHitInDb).CurrentValues.SetValues(commandsdata);
//workaroud is to remove
foreach (var ch in usrCmdHitInDb.CommandHits.ToList())
{
usrCmdHitInDb.CommandHits.Remove(ch);
}
usrCmdHitInDb.CommandHits.Clear();
foreach (var item in commandsdata.CommandHits)
{
usrCmdHitInDb.CommandHits.Add(item);
}
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.OK);
}
else
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
// POST api/AcadCommands
public HttpResponseMessage PostCommandsData(UserCommandsHit commandsdata)
{
if (ModelState.IsValid)
{
db.UserCommandsHits.Add(commandsdata);
db.SaveChanges();
HttpResponseMessage response = Request
.CreateResponse(HttpStatusCode.Created, commandsdata);
response.Headers.Location = new Uri(
Url.Link("DefaultApi",
new { id = commandsdata.Id })
);
return response;
}
else
{
return Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
// DELETE api/AcadCommands/5
public HttpResponseMessage DeleteCommandsData(int id)
{
UserCommandsHit commandsdata = db.UserCommandsHits.Find(id);
if (commandsdata == null)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
db.UserCommandsHits.Remove(commandsdata);
try
{
db.SaveChanges();
}
catch (DbUpdateConcurrencyException)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
return Request.CreateResponse(HttpStatusCode.OK, commandsdata);
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
Please note that the context class is using SQL Express, it is easy to migrate to SQL Azure latter:
<connectionStrings>
<add name="AcadCommandViewerContext" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=AcadCommandViewerContext; Integrated Security=True;
MultipleActiveResultSets=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
Now make some sample data for testing, back to AcadCommandViewer project and add a class --SampleData.cs -- in Models folder:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AcadCommandViewer.Models
{
public static class SampleData
{
public static UserCommandsHit danielCmdHits = new UserCommandsHit
{
Id = 1,
UserName = "Daniel",
CommandHits = new CommandHit[] {
new CommandHit{
CommandName = "PLINE",
HitNumber = 1
},
new CommandHit{
CommandName = "ZOOM",
HitNumber = 2
},
new CommandHit{
CommandName = "LINE",
HitNumber = 3
},
new CommandHit{
CommandName = "Save",
HitNumber = 4
}
}
};
public static UserCommandsHit jerryCmdHits = new UserCommandsHit
{
Id = 2,
UserName = "Jerry",
CommandHits = new CommandHit[]
{
new CommandHit{
CommandName = "CIRCLE",
HitNumber = 2
},
new CommandHit{
CommandName = "Quit",
HitNumber = 1
}
}
};
public static UserCommandsHit[] userCmdsHits = new UserCommandsHit[]
{
danielCmdHits,
jerryCmdHits
};
}
}
To generate the test data in database automatically, I need to enable database migration. Open Package Manager Console from Tools—> Library Package Manager, and type commands as below:
PM> Enable-Migrations
A class named as Configuration.cs and a folder ”Migrations” will to added into Visual Studio project. Open the class and implement the Seed method to populate the test data :
namespace AcadCommandViewer.Migrations
{
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
using AcadCommandViewer.Models;
internal sealed class Configuration : DbMigrationsConfiguration<AcadCommandViewer.Models.AcadCommandViewerContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
protected override void Seed(AcadCommandViewer.Models.AcadCommandViewerContext context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
//SampleData s = new SampleData();
context.UserCommandsHits.AddOrUpdate(
p => p.UserName,
SampleData.userCmdsHits
);
}
}
}
Then run Update-Database command in package Manager Console to create the database and test data.
PM> Update-Database
Okay, with that we are done the RESTful server side. I can use the tool fiddler to verify whether my Web Service works. The usage of Fiddler to test a RESTful service has been introduced in this post.
Next post will introduce how to implement the viewer side, to render the magic ball with WebGL.
Comments