Friday, January 24, 2014

Using ASP.NET Web API to Automate AutoCAD

Prior to AutoCAD ObjectARX .NET API, we used AutoCAD's COM API for both in-process (VBA or DLL that loads into VBA) and out-process (EXE) automation of AutoCAD. Then AutoCAD .NET API won most, if not all, AutoCAD programmers' favorite and AutoCAD COM API has been gradually fade into background. However, there are still times when people want to automate AutoCAD from external process/application, in which AutoCAD COM API still has significant advantage due to its fairly complete COM object model.

AutoCAD can also be automated from external application in pure .NET way. WCF as an mature .NET communication mechanism between processes/applications can be used to automate AutoCAD. There are a few samples/articles/post on this topic can be found on the Internet (unfortunately I did not keep their links:-().

I recently worked on a web application development project, in which ASP.NET Web API is used to expose business data as OData to be consumed by user side application (web application, mobile application or desktop). For those who want to know more about ASP.NET Web API, go to here for more details.

In spite its name "ASP.NET Web API" say "ASP.NET" and "Web", Web API is not just for web application. Like WCF, it is regarding communication between processes/applications, a much simplified in comparison to WCF. Its communication conduit is purely based on Http protocol, which, besides can be hosted in Windows' IIS server, can also be self hosted easily, thus effectively make its host's computing power available to outside world.

So, I explored a bit to see how I could use this technology to automate AutoCAD. To be honest, I am not a fan of automating AutoCAD from external application, even though I did develop a few applications doing this (mostly some kind of drawing batch processing operations), because AutoCAD is a very complicated desktop application and it is very often that an AutoCAD process needs user interaction to complete. So, the code I show here may not have much practical value to my real word AutoCAD development. It only shows a new way how AutoCAD can be automated from external application.

I used Visual Studio 2012/.NET 4.5 and AutoCAD 2014. The reason of using .NET 4.5 over .NET 4.0 is because the NuGet Manager only allows to get latest ASP.NET Web API Self-Host package, which requires .NET 4.5.

The Visual Studio solution and 3 projects in the solution are shown in pictures below:



 

1. Project AcadHttpDto

This project is a class library containing data classes used for communication between Http Web API server application (hosted inside AutoCAD) and Http client application. Dto in the project name stands for Data Transfer Object, which is commonly used in WCF with the class is decorated with attribute [DataContract] and its public member decorated with attribute [DataMember]. But in this development, since it is not WCF based, I just borrow the meaning of "DTO" for this project, implying the data classes here are used in similar way as DTO.

Currently the project only has 2 classes:

    1 namespace AcadHttpDto
    2 {
    3     public class AcadSysVar
    4     {
    5         public string Name { set; get; }
    6         public object Value { set; get; }
    7     }
    8 }

    1 namespace AcadHttpDto
    2 {
    3     public class CircleArgs
    4     {
    5         public double Radius { set; get; }
    6         public double X { set; get; }
    7         public double Y { set; get; }
    8         public double Z { set; get; }
    9     }
   10 }

2. Project AcadHttpServerHost

This project is an AutoCAD .NET DLL project that host Http Web API server. To host Web API server, the project needs to have references to a few libraries. I use NuGet Package Manager to add ASP.NET Web API Self Host package into this project:


As aforementioned, the ASP.NET Web API 2.1 Self Host package requires .NET 4.5, therefore the entire solution of this development is based on .NET 4.5.

This project also references project AcadHttpDto.

Class HttpServerHostInitializer is the an IExtensionApplication class that starts an HttpSelfHostServer as soon as the DLL is loaded into AutoCAD:

    1 using System.Web.Http.SelfHost;
    2 using System.Web.Http;
    3 using Autodesk.AutoCAD.ApplicationServices;
    4 using Autodesk.AutoCAD.EditorInput;
    5 using Autodesk.AutoCAD.Runtime;
    6 
    7 [assembly: ExtensionApplication(
    8     typeof(AcadHttpServerHost.HttpServerHostInitializer))]
    9 
   10 namespace AcadHttpServerHost
   11 {
   12     public class HttpServerHostInitializer : IExtensionApplication
   13     {
   14         static HttpSelfHostServer _httpServer = null;
   15 
   16         #region IExtensionApplication Members
   17 
   18         public void Initialize()
   19         {
   20             Document dwg = Application.DocumentManager.MdiActiveDocument;
   21             Editor ed = dwg.Editor;
   22 
   23             try
   24             {
   25                 ed.WriteMessage("\nInitializing HTTP server hosting...");
   26 
   27                 _httpServer =
   28                     CreateHttpSelfHostServer("http://localhost:54321");
   29                 _httpServer.OpenAsync().Wait();
   30 
   31                 ed.WriteMessage("completed.");
   32                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   33             }
   34             catch (System.Exception ex)
   35             {
   36                 ed.WriteMessage("failed:\n");
   37                 ed.WriteMessage(ex.Message);
   38                 Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
   39             }
   40 
   41 
   42         }
   43 
   44         public void Terminate()
   45         {
   46             if (_httpServer != null)
   47             {
   48                 _httpServer.Dispose();
   49             }
   50         }
   51 
   52         #endregion
   53 
   54         #region private methods
   55 
   56         private HttpSelfHostServer CreateHttpSelfHostServer(string baseUrl)
   57         {
   58             HttpSelfHostConfiguration config = ConfigurateHost(baseUrl);
   59             HttpSelfHostServer server = new HttpSelfHostServer(config);
   60             return server;
   61         }
   62 
   63         private HttpSelfHostConfiguration ConfigurateHost(string baseUrl)
   64         {
   65             HttpSelfHostConfiguration config =
   66                 new HttpSelfHostConfiguration(baseUrl);
   67 
   68             config.Routes.MapHttpRoute(
   69                 name: "Default Api",
   70                 routeTemplate: "api/{controller}/{id}",
   71                 defaults: new { id = RouteParameter.Optional }
   72                 );
   73 
   74             return config;
   75         }
   76 
   77         #endregion
   78     }
   79 }

As we can see, self-hosting Web API server can be easily done with just a few lines of code. Once a few Web API controllers are added into the project, the self-hosted server would be open through Http channel for external applications to communicate to the server and AutoCAD, the host.

With self-host server ready to run, it is time to add a few Web API Controllers that accept Http requests (GET/POST/PUT/DELETE...). I first created a base ApiController class AcadActionController:

    1 using System.Text;
    2 using System.Web.Http;
    3 
    4 namespace AcadHttpServerHost
    5 {
    6     public class AcadActionController : ApiController
    7     {
    8         private static StringBuilder _msg = new StringBuilder();
    9         public static StringBuilder ActionMessage
   10         {
   11             get { return _msg; }
   12         }
   13     }   
   14 }

Then I added 2 classes that does the actual work: accepting Http request, doing something (in AutoCAD) based on the request and sending response back to the client. For the purpose of the simple exploration, I created one controller that get and set AutoCAD's system variables, and the other one that makes AutoCAD draw something in current AutoCAD working database.

Here is class SysVariableController:

    1 using System.Net;
    2 using System.Net.Http;
    3 using System.Web.Http;
    4 using AcadHttpDto;
    5 using Autodesk.AutoCAD.ApplicationServices;
    6 using Autodesk.AutoCAD.DatabaseServices;
    7 
    8 namespace AcadHttpServerHost
    9 {
   10     public class SysVariableController : AcadActionController
   11     {
   12         public string Get(string varName)
   13         {
   14             try
   15             {
   16                 object varValue = Application.GetSystemVariable(varName);
   17                 return varValue.ToString();
   18             }
   19             catch
   20             {
   21                 return "Invalid SYSTEM VARIABLE name: \"" + varName + "\"";
   22             }
   23         }
   24 
   25         public string Get()
   26         {
   27             return "Please supply SYSTEM VARIABLE name!";
   28         }
   29 
   30         public HttpResponseMessage Put([FromBody]AcadSysVar sysVar)
   31         {
   32             ActionMessage.Length = 0;
   33             HttpStatusCode code = HttpStatusCode.Accepted;
   34 
   35             if (sysVar == null)
   36             {
   37                 code = HttpStatusCode.ExpectationFailed;
   38                 ActionMessage.Append("SysVar argument is not supplied.");
   39             }
   40             else
   41             {
   42                 if (!UpdateSystemVariable(sysVar))
   43                 {
   44                     code = HttpStatusCode.ExpectationFailed;
   45                 }
   46             }
   47 
   48             if (code != HttpStatusCode.Accepted)
   49                 return Request.CreateErrorResponse(
   50                     code, ActionMessage.ToString());
   51             else
   52                 return Request.CreateResponse<string>(
   53                     code, ActionMessage.ToString());
   54         }
   55 
   56         private bool UpdateSystemVariable(AcadSysVar sysVar)
   57         {
   58             try
   59             {
   60                 Database db = HostApplicationServices.WorkingDatabase;
   61                 Document doc = Application.DocumentManager.GetDocument(db);
   62                 using (DocumentLock l = doc.LockDocument())
   63                 {
   64                     Application.SetSystemVariable(sysVar.Name, sysVar.Value);
   65                 }
   66                 ActionMessage.Append(
   67                     "System variable \"" + sysVar.Name +
   68                     "\" is updated successfully.");
   69                 return true;
   70             }
   71             catch (System.Exception ex)
   72             {
   73                 ActionMessage.Append(
   74                     "Setting system variable \"" + sysVar.Name +
   75                     "\" failed:\n" + ex.Message);
   76                 return false;
   77             }
   78         }
   79     }
   80 }

Here is class DrawController:

    1 using System.Net.Http;
    2 using System.Net;
    3 using Autodesk.AutoCAD.ApplicationServices;
    4 using Autodesk.AutoCAD.DatabaseServices;
    5 using Autodesk.AutoCAD.Geometry;
    6 using AcadHttpDto;
    7 
    8 namespace AcadHttpServerHost
    9 {
   10     public class DrawController : AcadActionController
   11     {
   12         public HttpResponseMessage Put(CircleArgs circleArgs)
   13         {
   14             ActionMessage.Length = 0;
   15             HttpStatusCode code = HttpStatusCode.Created;
   16 
   17             if (circleArgs == null)
   18             {
   19                 code = HttpStatusCode.ExpectationFailed;
   20                 ActionMessage.Append("Circleargs argument is not supplied.");
   21             }
   22             else
   23             { 
   24                 if (!DrawCircle(circleArgs))
   25                 {
   26                     code = HttpStatusCode.ExpectationFailed;
   27                 }
   28             }
   29 
   30             if (code != HttpStatusCode.Created)
   31                 return Request.CreateErrorResponse(
   32                     code, ActionMessage.ToString());
   33             else
   34                 return Request.CreateResponse<string>(
   35                     code, ActionMessage.ToString());
   36         }
   37 
   38         #region private methods
   39 
   40         private bool DrawCircle(CircleArgs args)
   41         {          
   42             Database db = HostApplicationServices.WorkingDatabase;
   43             Document doc = Application.DocumentManager.GetDocument(db);
   44 
   45             using (DocumentLock l = doc.LockDocument())
   46             {
   47                 try
   48                 {
   49                     using (Transaction tran =
   50                         db.TransactionManager.StartTransaction())
   51                     {
   52                         BlockTableRecord model = tran.GetObject(
   53                             SymbolUtilityServices.GetBlockModelSpaceId(db),
   54                             OpenMode.ForWrite)
   55                             as BlockTableRecord;
   56 
   57                         Circle c = new Circle();
   58                         c.Center = new Point3d(args.X, args.Y, args.Z);
   59                         c.Radius = args.Radius;
   60                         c.SetDatabaseDefaults(db);
   61 
   62                         model.AppendEntity(c);
   63                         tran.AddNewlyCreatedDBObject(c, true);
   64 
   65                         tran.Commit();
   66                     }
   67 
   68                     ActionMessage.Append(
   69                         "Cicle has been added into drawing successfully.");
   70 
   71                     return true;
   72                 }
   73                 catch (System.Exception ex)
   74                 {
   75                     string error = ex.Message;
   76                     ActionMessage.Append(
   77                         "Drawing circle failed:\n" + ex.Message);
   78                     return false;
   79                 }
   80             }
   81         }
   82 
   83         #endregion
   84     }
   85 }

If looking into the code carefully, one would notice that I get a reference to current drawing document via HostApplicationServices.WorkingDatabase->Application.DocumentManager.GetDocument(Database)

Due to the way the Http self-host server runs, the MdiActiveDocuement is not available. I did not bother, or have time, to dig out the reason, as long as my exploration worked the way I did it.

Also, by following Http command tradition, the Put() method is meant for updating, thus the DrawController uses Put() to take client's request to draw something in AutoCAD. If I want to drawing something else rather than circle, I would create another Dto data class in AcadHttpDto project (say, LineArgs, which has data for a line's 2 end points) and add an overloaded Put() method that has different argument (LineArgs for drawing a line).

Now with just the 2 projects (AcadHttpDto and AcadHttpServerHost) being built, AutoCAD is ready to host the Http Web API inside and accept external requests and acts accordingly.

For any programmer who is familiar to web programming, Fiddler is a very well-known, a must-have free tool. I used Fiddler for testing the above self-host Web API code in AutoCAD before I actually wrote a Http client application.

This video clip shows using Fiddler to get/set AutoCAD system variable.
This video clip shows using Fiddler to have AutoCAD draw a circle.

After verifying the Web API server hosted in AutoCAD works as expected with Fiddler, I then continued the exploration to start the third project, a WinForm application with its UI looks like:

 
 
3. Project AcadHttpClient

This project also need to set reference to Web API client library. Again, I used NuGet Package Manager to get this done:


Of course this project also references project AcadHttpDto.

Here is the code behind the UI form (Form1):

    1 using System;
    2 using System.Net.Http;
    3 using System.Windows.Forms;
    4 using AcadHttpDto;
    5 
    6 namespace AcadHttpClient
    7 {
    8     public partial class Form1 : Form
    9     {
   10         HttpClient _client = null;
   11 
   12         public Form1()
   13         {
   14             InitializeComponent();
   15         }
   16 
   17         #region private methods
   18 
   19         private void ValidateVariable()
   20         {
   21             btnChangeVariable.Enabled = txtVariable.Text.Trim().Length > 0;
   22         }
   23 
   24         private void ValidateDraw()
   25         {
   26             if (txtRadius.Text.Trim().Length > 0 &&
   27                 txtX.Text.Trim().Length > 0 &&
   28                 txtY.Text.Trim().Length > 0 &&
   29                 txtX.Text.Trim().Length > 0)
   30             {
   31                 double d;
   32                 try
   33                 {
   34                     d = double.Parse(txtRadius.Text);
   35                     d = double.Parse(txtX.Text);
   36                     d = double.Parse(txtY.Text);
   37                     d = double.Parse(txtZ.Text);
   38                     btnDrawCircle.Enabled = true;
   39                 }
   40                 catch
   41                 {
   42                     btnDrawCircle.Enabled = false;
   43                 }
   44             }
   45             else
   46             {
   47                 btnDrawCircle.Enabled = false;
   48             }
   49         }
   50 
   51         private void GetSystemVariable()
   52         {
   53             string sysVarName=cboVariable.Text;
   54             HttpResponseMessage resMsg = _client.GetAsync(
   55                 "api/SysVariable/?varName=" + sysVarName).Result;
   56             resMsg.EnsureSuccessStatusCode();
   57 
   58             var txt = resMsg.Content.ReadAsAsync<string>().Result;
   59             txtVariable.Text = txt;
   60         }
   61 
   62         private void SetSystemVariable()
   63         {
   64             AcadSysVar sysVar = new AcadSysVar();
   65             sysVar.Name = cboVariable.Text;
   66             if (cboVariable.Text.ToUpper() == "DIMSCALE")
   67                 sysVar.Value = Convert.ToDouble(txtVariable.Text);
   68             else
   69                 sysVar.Value = txtVariable.Text.Trim();
   70 
   71             HttpResponseMessage resMsg =
   72                 _client.PutAsJsonAsync("api/SysVariable", sysVar).Result;
   73 
   74             var txt = resMsg.Content.ReadAsAsync<string>().Result;
   75             MessageBox.Show(txt);
   76 
   77         }
   78 
   79         private void DrawCircle()
   80         {
   81             double r, x, y, z;
   82             if (!GetCircleInputs(out r, out x, out y, out z))
   83             {
   84                 MessageBox.Show("Invalid circle parameter input!");
   85                 return;
   86             }
   87 
   88             CircleArgs args = new CircleArgs()
   89             {
   90                 Radius = r,
   91                 X = x,
   92                 Y = y,
   93                 Z = z
   94             };
   95 
   96             HttpResponseMessage resMsg =
   97                 _client.PutAsJsonAsync("api/Draw", args).Result;
   98 
   99             var txt = resMsg.Content.ReadAsAsync<string>().Result;
  100             MessageBox.Show(txt);
  101         }
  102 
  103         private bool GetCircleInputs(
  104             out double r, out double x, out double y, out double z)
  105         {
  106             r = 0.0;
  107             x = 0.0;
  108             y = 0.0;
  109             z = 0.0;
  110 
  111             try
  112             {
  113                 r = double.Parse(txtRadius.Text);
  114                 x = double.Parse(txtX.Text);
  115                 y = double.Parse(txtY.Text);
  116                 z = double.Parse(txtZ.Text);
  117             }
  118             catch
  119             {
  120                 return false;
  121             }
  122 
  123             return true;
  124         }
  125 
  126 
  127         #endregion
  128 
  129 
  130         private void Form1_Load(object sender, EventArgs e)
  131         {
  132             cboVariable.SelectedIndex = 0;
  133             ValidateDraw();
  134             ValidateVariable();
  135 
  136             _client = new HttpClient();
  137             _client.BaseAddress = new Uri("http://localhost:54321");
  138             _client.DefaultRequestHeaders.Accept.Add(
  139                 new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue(
  140                     "application/json"));
  141         }
  142 
  143         private void btnExit_Click(object sender, EventArgs e)
  144         {
  145             if (_client != null)
  146             {
  147                 _client.Dispose();
  148             }
  149 
  150             this.Close();
  151         }
  152 
  153         private void txtVariable_TextChanged(object sender, EventArgs e)
  154         {
  155             ValidateVariable();
  156         }
  157 
  158         private void txtRadius_TextChanged(object sender, EventArgs e)
  159         {
  160             ValidateDraw();
  161         }
  162 
  163         private void txtX_TextChanged(object sender, EventArgs e)
  164         {
  165             ValidateDraw();
  166         }
  167 
  168         private void txtY_TextChanged(object sender, EventArgs e)
  169         {
  170             ValidateDraw();
  171         }
  172 
  173         private void txtZ_TextChanged(object sender, EventArgs e)
  174         {
  175             ValidateDraw();
  176         }
  177 
  178         private void cboVariable_SelectedIndexChanged(object sender, EventArgs e)
  179         {
  180             if (cboVariable.SelectedIndex == 0)
  181             {
  182                 txtVariable.Text = "";
  183                 txtVariable.Enabled = false;
  184             }
  185             else
  186             {
  187                 GetSystemVariable();
  188                 txtVariable.Enabled = true;
  189             }
  190         }
  191 
  192         private void btnChangeVariable_Click(object sender, EventArgs e)
  193         {
  194             SetSystemVariable();
  195         }
  196 
  197         private void btnDrawCircle_Click(object sender, EventArgs e)
  198         {
  199             DrawCircle();
  200         }
  201     }
  202 }

Here is the video clip showing how the Windows EXE application interacts with AutoCAD through ASP.NET Web API server hosed inside AutoCAD.

Download the source code of the Visual Studio 2012 solution here:

4 comments:

Maxence said...

> Due to the way the Http self-host server runs, the MdiActiveDocuement is not available.

It is because your web server run on a thread different of the UI thread.

Try this:

[CommandMethod("MDIACTIVEDOC")]
public void MdiActiveDoc()
{
var t = new Thread(() =>
{
if (null == Application.DocumentManager.MdiActiveDocument)
Debug.WriteLine("MdiActiveDocument is not set");
else
Debug.WriteLine("MdiActiveDocument is set");
});
t.Start();
}

I'm really surprised that you do not have a problem with this.

Norman Yuan said...

Yeah, I was afraid that the Http server may have difficult to call into AutoCAD operation when I started, but went ahead for a try anyway. As the code showed, HostApplicationServices.WorkingDatabase is reachable and a Document can be obtained via the database. That is good enough to lock the document and perform some operation against WorkingDatabase. Obviously, when AutoCAD is receiving request from Http and working in the request, the AutoCAD session should not be operated by a user, as least when the hosted Http server is working on a request.

There is a WCF equivalent to my post given in AU2010:

http://autodesk.mediasite.com/Mediasite/Play/fe2ab6d991b44eeca8a339da8c33a146

I wasn't in AU2010 but came across that courseware. Now that I worked with ASP.NET Web API, I thought it would be interesting to do a similar exploration to see the difference between using WCF and Web API. To me, it seems, hosting Web API is a bit easier/simpler, as it should.

Again, with Autodesk's products gradually moving to service-based products, I do not see much practical value to automate desktop application AutoCAD.

driblog said...

can you please make this project available as a download? thanks!

Norman Yuan said...

OK, I have made the source code downloadable. Click the link I added at the end of the article to download.

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.