Having had my 'head down' in a rather pressing commercial engagement, I've had little time to experiment with some of the .NET and UI framework ecosystem changes that have been occurring.
So I decided to combine a whole truck load of them into one effort, creating an ASP.NET Core webapp esoteric language testbed (based on my esoteric interpreters GitHub project).
There are some screen shots at the end of this post showing an example in action. It's definitely a WIP, not quite ready to put on GitHub.
Implemented:
- Communicate with a REST API to determine what languages are available for use (supported languages are determined using a simple plugin system written with the version of MEF 2)
- Accept a language source program
- Use web sockets to request that the web app start remote interpretation, and allow client side 'interrupt' when the remotely executing program requires some user input
- Have the execution page be fully contextual in terms of what is, and is not, permitted at any point
ASP.NET Core webapp
This was reasonably straightforward to put together and get to work. Things that did bite:
- To work with AngularJS 2 RC2, the version of npm had to be a version different to the one shipped with VS 2015, meaning I had to fiddle with the external web tools to set the path
- Initial restore of bower and npm packages took a long time, and there was little in the way of progress indication sometimes
- Adding references to PCL's or .net standard assemblies often blew up the project.json file, resulting in duplicate Microsoft.NetCore.Platforms and Microsoft.NetCore.Targets dependencies that defeated package resolution. Editing project.json by hand cured this, but was not a pleasant experience
- Running under IIS; what with creating an app pool running no managed code (IIS reverse proxying out to a Kestrel instance running in a different process) and then having to use the publish feature of VS to get it to work - I spent most of my time working with IIS express instead
- Using a PCL as a reference in the web app causes all sorts of conniptions; VS 2015 still refuses to recognise the interfaces defined in a PCL of my own creation, and sometimes the build would fail. However, building using the .NET core command line tool (dotnet.exe) would cure this. Frustrating.
I never used Angular prior to v2. Never really had the opportunity, always seemed to be working in shops that used KnockoutJS (which is still a tidy library it must be said) or Ext JS (with its attendant steep learning curve).
Using it for this exercise was a pleasure. Sure, lots of set up issues, churn in the space, changes to routing discovered half way through, using Typescript (I know that is not mandatory!) - but all in all, positive.
There are a fair few components in the solution right now, but the key one is TestBedComponent, which in turn has two child components, LanguageComponent and ExecutionComponent - the first allowing the selection of a language to use for interpretation, with the languages being derived from calling an injected service, the second being responsible for the 'real' work:
- Allowing the entry of an esoteric program
- Using an injected service to request remote execution
- Responding to interrupts from the remote execution, meaning user input is required - showing a text box and button to allow the entry of user input that is then sent via the service to the remote server
The TestBedComponent has this form:
import { Component, EventEmitter, Input, Output, ViewChild } from "@angular/core";
import { LanguageComponent } from './language.component';
import { ExecutionComponent } from './execution.component';
@Component({
selector: "testbed",
template: `
<languageSelector (onLanguageChange)="languageChanged($event)"></languageSelector>
<execution></execution>
`,
directives: [LanguageComponent, ExecutionComponent]
})
export class TestBedComponent {
@ViewChild(ExecutionComponent)
private _executionComponent: ExecutionComponent;
currentLanguage: string;
languageChanged(arg) {
console.log('(Parent) --> Language changed to ' + arg);
console.log(this._executionComponent);
this._executionComponent.changeLanguage(arg);
}
}
I'm just embedding the two main components in the template, and using a reference to the execution component to communicate a change in the selected language, which is handled by the LanguageComponent, which exposes an event emitter which the test bed component listens to.
There are other ways of doing this, such as using a shared service, but I wanted to experiment with as many different parts of Angular 2 as possible, rather than be a purist :-)
The language component uses an iterated Bootstrap row; it's (too) simple at the moment, but uses an ngFor to present a list of languages discovered after consulting a service, template excerpt as below:
<div class="row">
<div class="col-xs-3" *ngFor="let language of languages">
<button class="btn btn-primary" (click)="languageChanged($event)">
{{language.Name}}
</button>
</div>
</div>
The execution component is a little more interesting, having a more involved template, as below:
<form (ngSubmit)="run()" #executionForm="ngForm">
<div class="row">
<div class="col-xs-12">
<h4>Source code</h4>
</div>
<div class="col-xs-12">
<textarea cols="80" rows="10" [(ngModel)]="sourceCode" style="min-width: 100%;"
name="sourceCode" required [disabled]="running"></textarea>
</div>
</div>
<p></p>
<div class="row">
<div class="col-xs-6">
<button type="submit" class="btn btn-primary" [disabled]="!executionForm.form.valid || running">
Run
</button>
<button type="button" class="btn btn-danger" (click)="cancel()" [disabled]="!running">
Cancel
</button>
</div>
</div>
<div class="row" *ngIf="inputRequired">
<div class="col-xs-12">
<h4>Input</h4>
</div>
<div class="col-xs-12">
<input [(ngModel)]="programInput" name="programInput" required/>
<button type="button" class="btn btn-primary" (click)="send()"
[disabled]="!executionForm.form.valid">
Send
</button>
</div>
</div>
</form>
<div class="row">
<div class="col-xs-12">
<h4>Output</h4>
</div>
<div class="col-xs-12">
<textarea cols="80" rows="10" [value]="programOutput" disabled style="min-width: 100%;"></textarea>
</div>
</div>
As you can see, it uses a form, and a range of one and two way bindings, and a few ngIf's to control visibility depending on context.
The actual implementation of this component is also quite simple:
1: export class ExecutionComponent {
2: language: string;
3: sourceCode: string;
4: programOutput = '';
5: programInput = '';
6: running = false;
7: inputRequired = false;
8: constructor(private _esolangService: EsolangService) {
9: console.log('built EC');
10: }
11: changeLanguage(lang) {
12: this.language = lang;
13: console.log(this.sourceCode);
14: }
15: run() {
16: console.log('Run! --> ' + this.sourceCode);
17: this.running = true;
18: this.programOutput = '';
19: this._esolangService.execute(
20: this.sourceCode,
21: {
22: next: m => this.programOutput += m,
23: complete: () => this.cancel()
24: },
25: () => this.inputRequired = true
26: );
27: }
28: send() {
29: console.log('Sending ' + this.programInput);
30: this._esolangService.send(this.programInput);
31: this.inputRequired = false;
32: }
33: cancel() {
34: this.running = this.inputRequired = false;
35: this._esolangService.close();
36: }
37: }
Lines of interest:
- 8 - EsoLangService is injected as a private member
- 11 - target language is changed
- 19-26 - the eso lang service is asked to execute the supplied source code. A NextObserver<any> is supplied as argument 2, and is hooked up internally within the service to a web sockets RxJs Subject (using https://github.com/afrad/angular2-websocket as a base). The third argument is a lambda that is called when the service receives a web socket message that indicates that user input is required. On receipt of this, inputRequired changes, which in turn affects this part of the template, displaying the user input text box and Send button:
<div class="row" *ngIf="inputRequired">
Screen shots
Just a few, with WARP as the target, executing a program that derives prime numbers from some user entered upper bound.
Initial page
Source entered
Execution started, but input required interrupt received
Execution complete
No comments:
Post a Comment