Blog
All Blog Posts | Next Post | Previous Post#WEBWONDERS : Making web async easy
Monday, April 5, 2021
Even these Delphi developers who have only set the very first steps into web client development will realize that quite a few important functionalities in web clients are executed only asynchronously by the browser.
Why asynchronous?
First of all, why did the creators of browser implement certain functionality only asynchronously? The answer to this question is quite simple. For the developers behind the browser, the user experience comes first and a UI that freezes is a big no-go. At all times, the user interface should remain response. The user should never get the impression that the browser or the machine hangs while navigating on the internet. Now, several operations happening in the browser client application can by nature have a fairly unpredictable time to execute. So, no such function should stop the execution of user interface handling code in the browser.
Traditionally, the browser developers solved this "issue" by JavaScript event handlers that are called asynchronously when the result of a certain operation is ready and meanwhile, any other code can continue to execute. This is no different in TMS WEB Core web client applications written in Object Pascal (but of course compiled to JavaScript by the pas2js compiler). Here, we typically solved this with the Object Pascal event paradigm. When a class instance method is invoked that can only have its result asynchronously, this class instance triggers an event handler when the underlying function completed. When the pas2js compiler also introduced support for anonymous methods, we offered for various such asynchronous operations, anonymous result handlers. To focus on this, let's take the example of a TWebHttpRequest class. This class permits to invoke HTTP(s) requests from the web client application. Clearly, this is an operation with an unpredictable duration, hence, the result of the request should be asynchronously handled.
Classic approaches
Example 1: handling via an event:
// method starting the HTTP request procedure TForm1.WebButton1Click(Sender: TObject); begin WebHttpRequest1.URL := 'https://www.tmssoftware.com/sample.json'; WebHttpRequest1.Execute(); end; // TWebHTTPRequest event handler for OnResponse procedure TForm1.WebHttpRequest1Response(Sender: TObject; AResponse: string); var jo: TJSONObject; jv: TJSONValue; begin jo := TJSONObject.Create; try jv := jo.ParseJSONValue(AResponse) // do any further processing here on the parsed JSON finally end; end;
Example 2: handling via an anonymous method:
procedure TForm1.WebButton1Click(Sender: TObject); begin WebHttpRequest1.URL := 'https://www.tmssoftware.com/sample.json';
WebHttpRequest1.Execute( procedure(AResponse: string; ARequest: TJSXMLHttpRequest) var jo: TJSOBject; jv: TJSONObject; begin jv := TJSONObject.Create; try jv.ParseJSONValue(AResponse); // do any further processing here on the parsed JSON finally end; end); end;
Introducing promises
Now, while the implementation using anonymous methods probably leads to fairly readable code, imagine needing to handle with multiple HTTP requests that depend on each other, which is in web development a fairly common use-case. While you can in theory invoke another http request from the anonymous method handler that is in turn also handled by a new anonymous method handler, you can see that it quickly becomes clumsy.
In the JavaScript world, a more elegant solution was introduced in 2015 with the ES6 standard and by now, any modern browser adopted this and supports it. The good news is that the pas2js v2.0 compiler also embraced and adopted JavaScript promises and offers a Pascal styled equivalent. If you want to read all about JavaScript promises, there are many good resources, but this is a well-written and concise one. In a nutshell, with pas2js v2.0, we can decorate a method with the async keyword or attribute and when having done so, it allows to use the await() function to (asynchronously) wait for a result of a function returning a promise. When the function is successful, it returns the promises, when not, it triggers an exception we can handle. By using this await(), we can further write our code as if the execution was sequential (i.e. synchronous) and our brain seems to be programmed to understand this kind of logic much better.
Now, in TMS WEB Core v1.7, we have already introduced in many components functions returning promises where you can use at application level, await() constructs to handle the code 'sequentially'. An example of this is the TWebHttpRequest.Perform: TJSPromise function that now allows to perform HTTP requests and deal with these with an await. The equivalent code for the first two examples becomes:
procedure TForm1.WebButton1Click(Sender: TObject); var req: TJSXMLHttpRequest; jo, jv: TJSONOBject; begin WebHttpRequest1.URL := 'https://www.tmssoftware.com/sample.json'; try req := await(TJSXMLHttpRequest, WebHttpRequest1.Perform()); jo := TJSONObject.Create; jv := jo.ParseJSONValue(string(req.response));
except // handle failure to execute request here end; end;
type TForm1 = class(TWebForm) [async] procedure WebButton1Click(Sender: TObject); end;
TMS WEB Core v1.7 includes many new TJSPromise functions
var newform: TForm2; begin newform := TForm2.CreateNew(procedure(AForm: TObject) begin // anonymous method needed as the form is asynchronously loaded and initialized (AForm as TForm2).frm2Edit.Text := WebEdit1.Text; end ); newform.Caption := 'Child form'; // asynchronously handling closing the form via an anonymous method newform.ShowModal(procedure(AValue: TModalResult) begin ShowMessage('Form 2 closed with new value:"'+newform.frm2Edit.Text+'"'); WebEdit1.Text := newform.frm2Edit.Text; newform.Free; end ); end;
procedure TForm1.WebButton1Click(Sender: TObject); var newform: TForm2; mr: TModalResult; begin newform := TForm2.Create(Self); newform.Caption := 'Child form'; // load file HTML template + controls await(TForm2, newform.Load()); // init control after loading newform.frm2Edit.Text := WebEdit1.Text; try // excute form and wait for close mr := await(TModalResult, newform.Execute); ShowMessage('Form 2 closed with new value:"'+newform.frm2Edit.Text+'"'); WebEdit1.Text := newform.frm2Edit.Text; finally newform.Free; end; end;
Oh, and of course, the same applies to the upcoming TMS WEB Core for Visual Studio Code.
We look forward to your feedback & comments! Happy coding meanwhile.
Learn more about TMS WEB Core 1.7 Ancona?
- Read this blog introducing all new features
- Participate in the free webinar on Apr 8, 2021
Bruno Fierens
This blog post has not received any comments yet.
All Blog Posts | Next Post | Previous Post