From ebf1d60bde98921e6e041f17a3d57accb0e0880d Mon Sep 17 00:00:00 2001 From: "Frank A. Krueger" Date: Thu, 9 Nov 2017 21:00:15 -0800 Subject: [PATCH] Working with ASP.NET MVC --- Ooui.AspNetCore/ElementResult.cs | 13 +-- Ooui.AspNetCore/Ooui.AspNetCore.csproj | 1 + Ooui.AspNetCore/OouiMiddlewareExtensions.cs | 55 +++++++++++++ Ooui.AspNetCore/WebSocketHandler.cs | 82 +++++++++++++++++++ Ooui/UI.cs | 12 +-- .../Controllers/HomeController.cs | 4 +- PlatformSamples/AspNetCoreMvc/Startup.cs | 27 +++--- 7 files changed, 167 insertions(+), 27 deletions(-) create mode 100644 Ooui.AspNetCore/OouiMiddlewareExtensions.cs create mode 100644 Ooui.AspNetCore/WebSocketHandler.cs diff --git a/Ooui.AspNetCore/ElementResult.cs b/Ooui.AspNetCore/ElementResult.cs index 19faf23..f43d342 100644 --- a/Ooui.AspNetCore/ElementResult.cs +++ b/Ooui.AspNetCore/ElementResult.cs @@ -6,20 +6,21 @@ using Microsoft.AspNetCore.Mvc; namespace Ooui.AspNetCore { public class ElementResult : ActionResult - { + { + readonly Element element; + public ElementResult (Element element) { - + this.element = element; } public override async Task ExecuteResultAsync (ActionContext context) { - var path = context.HttpContext.Request.Path; var response = context.HttpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/html"; - var html = Encoding.UTF8.GetBytes (UI.RenderTemplate (path)); + response.ContentType = "text/html; charset=utf-8"; + var sessionId = WebSocketHandler.BeginSession (context.HttpContext, element); + var html = Encoding.UTF8.GetBytes (UI.RenderTemplate (WebSocketHandler.WebSocketPath + "?id=" + sessionId)); response.ContentLength = html.Length; using (var s = response.Body) { await s.WriteAsync (html, 0, html.Length).ConfigureAwait (false); diff --git a/Ooui.AspNetCore/Ooui.AspNetCore.csproj b/Ooui.AspNetCore/Ooui.AspNetCore.csproj index 372fb3c..bb62fe3 100644 --- a/Ooui.AspNetCore/Ooui.AspNetCore.csproj +++ b/Ooui.AspNetCore/Ooui.AspNetCore.csproj @@ -6,6 +6,7 @@ + diff --git a/Ooui.AspNetCore/OouiMiddlewareExtensions.cs b/Ooui.AspNetCore/OouiMiddlewareExtensions.cs new file mode 100644 index 0000000..deaa51e --- /dev/null +++ b/Ooui.AspNetCore/OouiMiddlewareExtensions.cs @@ -0,0 +1,55 @@ +using System; +using Ooui.AspNetCore; + +namespace Microsoft.AspNetCore.Builder +{ + public static class OouiMiddlewareExtensions + { + public static void UseOoui (this IApplicationBuilder app, string jsPath = "/ooui.js", string webSocketPath = "/ooui.ws", TimeSpan? sessionTimeout = null) + { + if (string.IsNullOrWhiteSpace (webSocketPath)) + throw new ArgumentException ("A path to be used for Ooui web sockets must be specified", nameof (webSocketPath)); + + if (string.IsNullOrWhiteSpace (jsPath)) + throw new ArgumentException ("A path to be used for Ooui JavaScript must be specified", nameof (jsPath)); + + WebSocketHandler.WebSocketPath = webSocketPath; + + if (sessionTimeout.HasValue) { + WebSocketHandler.SessionTimeout = sessionTimeout.Value; + } + + var webSocketOptions = new WebSocketOptions () { + KeepAliveInterval = WebSocketHandler.SessionTimeout, + ReceiveBufferSize = 4 * 1024 + }; + app.UseWebSockets (webSocketOptions); + + app.Use (async (context, next) => + { + if (context.Request.Path == jsPath) { + var response = context.Response; + var clientJsBytes = Ooui.UI.ClientJsBytes; + response.StatusCode = 200; + response.ContentLength = clientJsBytes.Length; + response.ContentType = "application/javascript; charset=utf-8"; + response.Headers.Add ("Cache-Control", "public, max-age=3600"); + using (var s = response.Body) { + await s.WriteAsync (clientJsBytes, 0, clientJsBytes.Length).ConfigureAwait (false); + } + } + else if (context.Request.Path == WebSocketHandler.WebSocketPath) { + if (context.WebSockets.IsWebSocketRequest) { + await WebSocketHandler.HandleWebSocketRequestAsync (context).ConfigureAwait (false); + } + else { + context.Response.StatusCode = 400; + } + } + else { + await next ().ConfigureAwait (false); + } + }); + } + } +} diff --git a/Ooui.AspNetCore/WebSocketHandler.cs b/Ooui.AspNetCore/WebSocketHandler.cs new file mode 100644 index 0000000..0c22f74 --- /dev/null +++ b/Ooui.AspNetCore/WebSocketHandler.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Ooui.AspNetCore +{ + public static class WebSocketHandler + { + public static string WebSocketPath { get; set; } = "/ooui.ws"; + + public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (5); + + static readonly ConcurrentDictionary pendingSessions = + new ConcurrentDictionary (); + + public static string BeginSession (HttpContext context, Element element) + { + var id = Guid.NewGuid ().ToString ("N"); + + var s = new PendingSession { + Element = element, + RequestTimeUtc = DateTime.UtcNow, + }; + + if (!pendingSessions.TryAdd (id, s)) { + throw new Exception ("Failed to schedule pending session"); + } + + return id; + } + + public static async Task HandleWebSocketRequestAsync (HttpContext context) + { + // + // Make sure we get a good ID + // + if (!context.Request.Query.TryGetValue ("id", out var idValues)) { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + var id = idValues.FirstOrDefault (); + if (id == null || id.Length != 32) { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + // + // Find the pending session + // + if (!pendingSessions.TryRemove (id, out var pendingSession)) { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + // + // Reject the session if it's old + // + if ((DateTime.UtcNow - pendingSession.RequestTimeUtc) > SessionTimeout) { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + // + // OK, Run + // + var token = CancellationToken.None; + var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui"); + var session = new Ooui.UI.Session (webSocket, pendingSession.Element, token); + await session.RunAsync ().ConfigureAwait (false); + } + + class PendingSession + { + public Element Element; + public DateTime RequestTimeUtc; + } + } +} diff --git a/Ooui/UI.cs b/Ooui/UI.cs index 753639a..1e4bd24 100644 --- a/Ooui/UI.cs +++ b/Ooui/UI.cs @@ -27,17 +27,19 @@ namespace Ooui static readonly byte[] clientJsBytes; + public static byte[] ClientJsBytes => clientJsBytes; + public static string Template { get; set; } = $@" - @ElementPath + @Title
- + "; @@ -297,9 +299,9 @@ namespace Ooui } } - public static string RenderTemplate (string elementPath) + public static string RenderTemplate (string webSocketPath) { - return Template.Replace ("@ElementPath", elementPath).Replace ("@Styles", rules.ToString ()); + return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()); } class DataHandler : RequestHandler @@ -454,7 +456,7 @@ namespace Ooui Console.ResetColor (); } - class Session + public class Session { readonly WebSocket webSocket; readonly Element element; diff --git a/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs b/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs index 10f2378..745e96f 100644 --- a/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs +++ b/PlatformSamples/AspNetCoreMvc/Controllers/HomeController.cs @@ -15,8 +15,8 @@ namespace AspNetCoreMvc.Controllers { public IActionResult Index() { - var button = new Button (); - return new ElementResult (button); + var element = new Label { Text = "Hello Oooooui from Controller" }; + return new ElementResult (element); } public IActionResult About() diff --git a/PlatformSamples/AspNetCoreMvc/Startup.cs b/PlatformSamples/AspNetCoreMvc/Startup.cs index e4ec3f9..cc5ff26 100644 --- a/PlatformSamples/AspNetCoreMvc/Startup.cs +++ b/PlatformSamples/AspNetCoreMvc/Startup.cs @@ -11,7 +11,7 @@ namespace AspNetCoreMvc { public class Startup { - public Startup(IConfiguration configuration) + public Startup (IConfiguration configuration) { Configuration = configuration; } @@ -19,29 +19,28 @@ namespace AspNetCoreMvc public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + public void ConfigureServices (IServiceCollection services) { - services.AddMvc(); + services.AddMvc (); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure (IApplicationBuilder app, IHostingEnvironment env) { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); + if (env.IsDevelopment ()) { + app.UseDeveloperExceptionPage (); } - else - { - app.UseExceptionHandler("/Home/Error"); + else { + app.UseExceptionHandler ("/Home/Error"); } - app.UseStaticFiles(); + app.UseStaticFiles (); - app.UseMvc(routes => - { - routes.MapRoute( + app.UseOoui (); + + app.UseMvc (routes => { + routes.MapRoute ( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });