Many applications, both Windows and Web, need to connect to the Internet to send or receive data. ASNA Visual RPG Classic apps do not intrinsically have the ability to make HTTP requests. This can be a crippling drawback when you have a legacy enterprise app for which you now have the requirement to send and/or receive data from the Internet. There are still a few third-party COM controls around that can enable AVR to make HTTP requests, but these controls are often troublesome and customers report that some no longer work.
This article shows how to extend AVR Classic with AVR for .NET to enable the AVR Classic app to fetch the Json data with an HTTP request. We'll first a look at the AVR for .NET class library project responsible for making the HTTP request to fetch the Json then we'll take a look at the AVR Classic app that consumes that .NET class library. This article provides a simple example of fetching Json. Things like user authentication and product-worthy error handling are omitted--but both can be added.
This article is the third in a series about integrating AVR for .NET with AVR Classic. The other two are:
- How to make .NET's command line available inside Visual Studio
- How to extend AVR Classic with AVR for .NET
Other articles that may be helpful are:
Let's start with a preview of the results. The image below shows an AVR Classic app with a simple subfile. This subfile has been populated with Json data read from the Internet--with a little help from AVR for .NET. You may not need to populate a subfile with Json data, but the subfile is a good way to show results.
The Json data
First, we need some Json test data. The JSONPlaceholder site provides several different Json test documents. This article uses its
users Json document. This URL provides fictitious Json data for 10 users and a fragment of it is shown below in Figure 1.
Figure 1. Sample Json data from https://jsonplaceholder.typicode.com/users
The red box in Figure 1 above outlines a single user element in the Json document. Each user in this Json document has nested data in its
company values. We'll need a set of AVR for .NET classes to model this data so we can deserialize the Json data into a data format easily accessible by the AVR Classic app.
The AVR for .NET class library
To integrate AVR for .NET with AVR Classic, we'll start by building an AVR for .NET class library. This library will offer AVR Classic what it sees as a custom COM component with the ability to make an HTTP request.
The AVR for .NET classes that model this data are shown below in Figure 2a. It's generally a best practice to put each class in its own source file, but to minimize the chunks presented here I'm cheating and putting these four classes in a single source member.
Using System.Runtime.InteropServices DclNameSpace AVRClassicHelper.Http BegClass User Access(*Public) Attributes(ComVisible(*True), + ClassInterface(ClassInterfaceType.AutoDual)) DclProp id Type(*String) Access(*Public) DclProp name Type(*String) Access(*Public) DclProp username Type(*String) Access(*Public) DclProp email Type(*String) Access(*Public) DclProp address Type(AddressInfo) Access(*Public) DclProp phone Type(*String) Access(*Public) DclProp website Type(*String) Access(*Public) DclProp company Type(CompanyInfo) Access(*Public) EndClass BegClass AddressInfo Access(*Public) Attributes(ComVisible(*True), + ClassInterface(ClassInterfaceType.AutoDual)) DclProp street Type(*String) Access(*Public) DclProp suite Type(*String) Access(*Public) DclProp city Type(*String) Access(*Public) DclProp zipcode Type(*String) Access(*Public) DclProp geo Type(GeoInfo) Access(*Public) EndClass BegClass GeoInfo Access(*Public) Attributes(ComVisible(*True), + ClassInterface(ClassInterfaceType.AutoDual)) DclProp lat Type(*String) Access(*Public) DclProp lng Type(*String) Access(*Public) EndClass BegClass CompanyInfo Access(*Public) Attributes(ComVisible(*True), + ClassInterface(ClassInterfaceType.AutoDual)) DclProp name Type(*String) Access(*Public) DclProp catchphrase Type(*String) Access(*Public) DclProp bs Type(*String) Access(*Public) EndClass
Figure 2a. Sample Json data from https://jsonplaceholder.typicode.com/users.
Figure 2a is a single source member that provides four classes:
Notice how the structure of these four classes echoes exactly the nested structure presented by the Json test data. Ultimately, we'll be able to fetch a user property with a nested object syntax like this:
DclFld City Type(*String) City = User.Address.City
It's very important that the structures created to represent the Json data do so accurately. Take your time and declare your data description classes very carefully. The ability to deserialize the incoming Json into a .NET object depends on their schema correctly echoing the Json schema.
You'll notice that the .NET attributes
ClassInterface have been applied to all four of the data description classes in Figure 2a. These attributes are necessary to surface .NET classes (and their properties and members) to COM. This asna.com article goes into more detail about these attributes. These attributes are applied to all four of the classes in Figure 2a.
ClassInterfaceattributes used to expose .NET classes to COM don't affect the ability of those classes to also be consumed by .NET. These classes can still be used in .NET-only projects. There may be some performance penalty imposed so watch for that--but that doesn't appear to a significant issue.
The second AVR for .NET class needed is one to make the HTTP request to fetch the Json. That
Request class is shown below in Figure 2b.
Using System Using System.IO Using System.Text Using System.Web Using System.Net Using NewtonSoft.Json DclNameSpace AVRClassicHelper.Http BegClass Request Access(*Public) DclProp HttpStatus Type(*Integer4) Access(*Public) DclProp ErrorMessage Type(*String) Access(*Public) BegFunc GetRequest Type(*String) Access(*Public) DclSrParm Url Type(*String) DclFld encoding Type(ASCIIEncoding) New() DclFld req Type(HttpWebRequest) DclFld res Type(HttpWebResponse) DclFld responseStream Type(Stream) DclFld responseString Type(*String) DclFld sr Type(StreamReader) req = WebRequest.Create(Url) *As HttpWebRequest req.Method = "GET" Try res = req.GetResponse() *As HttpWebResponse *This.HttpStatus = res.StatusCode *This.ErrorMessage = String.Empty Catch ex1 Type(WebException) If ex1.Status <> WebExceptionStatus.Success res = ex1.Response *As HttpWebResponse *This.HttpStatus = res.StatusCode *This.ErrorMessage = ex1.Message LeaveSr *Nothing EndIf Catch ex2 Type(Exception) *This.HttpStatus = 0 *This.ErrorMessage = ex2.Message LeaveSr *Nothing EndTry If (res.StatusCode = HttpStatusCode.OK) responseStream = res.GetResponseStream() sr = *New StreamReader(responseStream) responseString = sr.ReadToEnd() sr.Close() Else *This.HttpStatus = 0 *This.ErrorMessage = res.StatusDescription LeaveSr *Nothing EndIf LeaveSr responseString EndFunc BegFunc GetJson Access(*Public) Type(User) Rank(1) DclSrParm Url Type(String) DclFld JsonString Type(*String) DclArray UserList Type(User) Rank(1) JsonString = GetRequest(Url) If *This.HttpStatus = 200 UserList = JsonConvert.DeserializeObject(JsonString, + *TypeOf(User)) *As User LeaveSr UserList Else LeaveSr *Nothing EndIf EndFunc EndClass
Figure 2b. The AVR for .NET Request class.
Request class has two properties:
- HttpStatus - This property reports the HTTP status of the most recent request. If the request succeeded this value will be 200, otherwise an error occurred.
- ErrorMessage - This field reports the error message when an error occurs.
Request class has two methods:
GetRequest- This method uses the URL passed to it to make an HTTP Get request. If the request succeeds this method returns the string value of the response. When used to fetch Json, this will be a string value of the Json. A short sidebar at the end of this article goes into a little more detail on the
GetJson- This method is a wrapper around the more general-purpose
GetRequestmethod to make a Json request. It returns a .NET object that is deserialized from the Json string returned. In this example, an array of the User class (from Figure 2a) is returned. This article explains the Json deserialization process in detail.
Note that nothing in the
Request class is surfaced directly to COM.
The third and final AVR for .NET class required for this example is a class to surface Figure 2b's GetJson method and some other necessary properties. My contention is to call this class
ComBridge and it is shown in Figure 2c below.
Debugging between AVR Classic and AVR for .NET is challenging. You can't interactively debug from one environment to the other. The ComBridge class makes it easy to package and test exactly what the COM app needs--and this minimizes the .NET debugging required. As you build .NET components for AVR Classic consumption code defensively and test your .NET components carefully before attempting to consume them with COM. A .NET test harness to test your .NET work first is a good way to avoid debugging pain later.
Using System Using System.Text Using System.Runtime.InteropServices DclNameSpace AVRClassicHelper.Http BegClass ComBridge Access(*Public) + Attributes(ComVisible(*True), + ClassInterface(ClassInterfaceType.AutoDual)) DclProp HTTPStatus Type(*Integer4) Access(*Public) Attributes(ComVisible(*True)) DclProp ErrorMessage Type(*String) Access(*Public) Attributes(ComVisible(*True)) DclArray Users Type(User) Rank(1) Access(*Public) BegFunc CallGet Access(*Public) Attributes(ComVisible(*True)) Type(*Integer4) DclSrParm Url Type(*String) DclFld Req Type(AVRClassicHelper.Http.Request) New() Users = Req.GetJson(Url) *This.HTTPStatus = Req.HTTPStatus *This.ErrorMessage = Req.ErrorMessage If Req.HTTPStatus = 200 LeaveSr Users.Length EndIf Users = *Nothing LeaveSr -1 EndFunc BegFunc GetUser Access(*Public) Type(User) Attributes(ComVisible(*True)) DclSrParm Index Type(*Integer4) If Users = *Nothing LeaveSr *Nothing EndIf If Index > Users.Length - 1 LeaveSr *Nothing Else LeaveSr Users[Index] EndIf EndFunc EndClass
Figure 2c. The ComBridge class which surfaces .NET functionality to AVR Classic.
Like the data classes in Figure 2a, the
ComBridge class is also decorated with the
ComBridge has three properties:
- HttpStatus - This property exposes the Request's class's HttpStatus property.
- ErrorMessage - This property exposes the Request's class's HttpError message property.
Users - This property is an array of users, which in this example is populated with Json. When you fetch a Json array, you rarely know how many array elements will be returned (the test data we're using is hardcoded to 10 elements, but that hardcoding rarely happens in the real world). The Users array is a Ranked array; that's an array type that AVR Classic doesn't support; AVR can't use this object directly. To avoid it being surfaced to COM (where it would cause a runtime error), it is marked
Limit the data types you surface to COM from .NET to scalar types (strings and numbers, essentially) and data classes (like the four we're using from Figure 2a). Even simple .NET objects like date data types can cause issues with COM. Use getter functions (like this example does) and keep the interface between the two environments simple.
ComBridge surfaces two methods to AVR Classic:
CallGet - This method uses the Request's class's GetJson method to deserialize the Json to populate the
Usersranked array. Because AVR Classic can't directly access the
Usersarray, this method returns to AVR Classic the number of elements read.
GetUser - This method surfaces a given element of the
Userarray. You'll see in a moment that AVR Classic uses this method in a loop to fetch each user.
After compiling the .NET project, .NET's
RegAsmutility must be used to create COM-based type library needed for AVR Classic.
RegAsm and how to use it is explained in this article. After running
RegAsm you'll see that library in the same folder as the .NET DLL--the only difference is the COM library has a
RegAsm created the library and registered with COM on your system.
The AVR classic app to consume the .NET class library
Consuming the AVR for .NET project's DLL with AVR Classic is pretty simple. With a new project started, we first need to set a reference to the COM class library that the AVR for.NET project created. In this example, that DLL is named AVRClassicHelper_Http. The .NET project name was AVR ClassicHelper.HTTP and when
RegAsm compiles the COM type library, it swaps out the period for an underscore. Figure 3a below shows AVR Classic's References window with this reference set.
Figure 3a. AVR Classic's References window
Having set that reference, we can use AVR Classic's Object Browser to see what that reference makes available to AVR Classic, as shown below in Figure 3b.
Figure 3b. AVR Classic's Object Browser window
AVR Classic's Object Browser shows what .NET components the reference made available: the four data classes (
GeoInfo) and the
ComBridge class. The Object Browser view in Figure 3b shows methods and properties that the
ComBridge makes available. You'll use AVR Classic's Object Browser frequently to ensure the members and properties you think should be there are actually there and to see how to declare the classes (in the bottom of the Object Browser window).
You also notice that the
ComBridgeclass surfaces members (the properties
GetType, and the method
ToString) that weren't explicitly defined in the
ComBridgeclass in the .NET project. These members are aritifacts of .NET object inheritance and you can generally ignore them.
The AVR Classic code to use the
ComBridge class is shown below in Figure 4a:
DCLFLD httpGetJson TYPE(AVRClassicHelper_Http.ComBridge) DCLFLD User TYPE(AVRClassicHelper_Http.User) labelResult.Caption = '' BEGSR CommandButton1 Click DclFld UserCount TYPE(*Integer) Len(4) DclFld Url Type(*String) DclFld i Type(*Integer) Len(4) SetMousePtr *HourGlass Url = 'https://jsonplaceholder.typicode.com/users' UserCount = httpGetJson.CallGet(Url) If httpGetJson.HttpStatus = 200 SetMousePtr *Dft If UserCount < 0 MsgBox 'Error reading Json data' LeaveSr EndIf labelResult.Caption = 'Json rows read: ' + %TRIM(%CHAR(%EDITC(UserCount, 'J'))) subfileUsers_RRN = 0 subfileUsers.ClearObj() Do FromVal(0) ToVal(UserCount-1) Index(i) User = httpGetJson.GetUser(i) WriteSubfileRow() EndDo Else MsgBox Msg(httpGetJson.ErrorMessage) EndIf ENDSR BegSr WriteSubfileRow subfileUsers_RRN = subfileUsers_RRN + 1 Id = User.Id Name = User.Name Email = User.Email City = User.Address.City ZipCode = User.Address.ZipCode Company = User.Company.Name Write subfileUsers EndSr
Figure 4a. The AVR Classic code to consume the .NET components.
This line from the code above calls the .NET
CallGet() method, passing it a URL:
UserCount = httpGetJson.CallGet(Url)
If the call is successful,
UserCount indicates the number of Json elements available and
httpGetJson.HttpStatus will be 200. If the call isn't successful
UserCount is -1 and the
httpGetJson.HttpStatus code is the HTTP status code received from the HTTP request. If an error occurred, the error message is in the
Because AVR Classic can't access the .NET array of users directly, it uses a loop and the .NET
httpGetJson.GetUser function to fetch each user. When a user is fetched
WriteSubFile is called to add that user to the subfile.
The results of the code in Figure 4a are shown below in Figure 4b:
Figure 4b. The AVR Classic app showing Json data in a subfile.
Integrating AVR Classic with AVR for .NET is an intermediate/advanced topic to be sure. However, once you master the basics, it's actually pretty easy to do, and the power and possibilities that .NET can provide to COM are nearly endless.
- The clients need the same version of AVR for .NET's runtime installed.
- The clients also need the .NET Framework installed but for any Windows 7/8/10 box it will already be there. However, it might be an old version so you might need to update the version of the .NET Framework installed. For best results, make your client PC's .NET Framework version match your development version.
- Copy the AVR for .NET DLL the project produces to each client PC.
Register the DLL with the
RegAsmutility (which the .NET Framework provides) as explained here.
- Test your .NET code before trying to integrate with AVR Classic. Debugging between the two environments is frustrating.
- Limit the data you pass from .NET to AVR Classic to core scalar values and simple data classes.
- Don't start with your biggest, most important project! Start simple and build from there.
Sidebar: A quick note on fetching the Json response
The AVR for .NET
GetRequest method in Figure 2b above has about 45 lines of code, however much of that code is error handling. The core facility to issue an HTTP request and convert its response to a string is shown below in Figure A.
DclFld req Type(HttpWebRequest) DclFld res Type(HttpWebResponse) DclFld responseStream Type(Stream) DclFld responseString Type(*String) DclFld sr Type(StreamReader) // Make an HTTP GET request return its response object to the res variable.. req = WebRequest.Create(Url) *As HttpWebRequest req.Method = "GET" res = req.GetResponse() *As HttpWebResponse // Convert the response into a string. responseStream = res.GetResponseStream() sr = *New StreamReader(responseStream) responseString = sr.ReadToEnd() sr.Close()
Figure A. Traditional AVR for .NET code to work with HTTP.
We've been doing HTTP work with AVR for .NET for a long time and have always used the
HttpWebRequest/HttpWebResponse APIs. The grungy part of using
HttpWebRequest/HttpWebResponse isn't issuing the HTTP request, it's the four mysterious lines of code required to convert the response into a string. It occurred to me during this .NET->COM project that maybe there are better ways to work with HTTP in .NET now.
This article lead me to the RestSharp open source project. With nearly 17m downloads on nuget.org RestSharp must have something going for it. I gave it a quick spin with AVR for .NET and was impressed with its concise API and direct way of doing things. With RestSharp the code in Figure A above is reduced to the code in Figure B below:
DlFld Client Type(RestSharp.RestClient) DclFld Response Type(RestSharp.IRestResponse) DclFld ResponseString Type(*String) Client = *New RestSharp.RestClient(url) Response = Client.Execute(*New RestSharp.RestRequest()) ResponseString = Response.Content
Figure B. The RestSharp equivalent of Figure A.
The RestSharp API is comprehensive and has features for authentication and serialization baked in. It looks like a promising API for .NET HTTP work.