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:
> Due to the way the Http self-host server runs, the MdiActiveDocuement is not available.
ReplyDeleteIt 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.
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.
ReplyDeleteThere 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.
can you please make this project available as a download? thanks!
ReplyDeleteOK, I have made the source code downloadable. Click the link I added at the end of the article to download.
ReplyDelete