From 96f4dd90b36256ae2a5d56d24c3f0eab30a66a4a Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 25 Jul 2019 07:26:51 -0700 Subject: [PATCH 1/4] Render blazor on correct synchronization context --- src/HotUI.Blazor/Components/HotUIComponentBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotUI.Blazor/Components/HotUIComponentBase.cs b/src/HotUI.Blazor/Components/HotUIComponentBase.cs index b72b802..55bc9ec 100644 --- a/src/HotUI.Blazor/Components/HotUIComponentBase.cs +++ b/src/HotUI.Blazor/Components/HotUIComponentBase.cs @@ -8,6 +8,6 @@ namespace HotUI.Blazor.Components { } - internal void NotifyUpdate() => base.StateHasChanged(); + internal void NotifyUpdate() => Invoke(StateHasChanged); } } From 520d0c284793d11dcf70915bedfa80b811a8590e Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 25 Jul 2019 07:27:09 -0700 Subject: [PATCH 2/4] Add progress bar --- .../Components/BProgressBar.razor | 22 ++++++++++++ src/HotUI.Blazor/Components/BView.cs | 14 ++++++-- .../Handlers/ProgressBarHandler.cs | 34 +++++++++++++++++++ src/HotUI.Blazor/UI.cs | 1 + 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/HotUI.Blazor/Components/BProgressBar.razor create mode 100644 src/HotUI.Blazor/Handlers/ProgressBarHandler.cs diff --git a/src/HotUI.Blazor/Components/BProgressBar.razor b/src/HotUI.Blazor/Components/BProgressBar.razor new file mode 100644 index 0000000..c0eb948 --- /dev/null +++ b/src/HotUI.Blazor/Components/BProgressBar.razor @@ -0,0 +1,22 @@ +@inherits HotUIComponentBase + +@if (IsIndeterminate) +{ +
+
+
+} +else +{ +
+
+
+} + +@code { + [Parameter] + public double Value { get; set; } + + [Parameter] + public bool IsIndeterminate { get; set; } +} diff --git a/src/HotUI.Blazor/Components/BView.cs b/src/HotUI.Blazor/Components/BView.cs index 9d53333..3961193 100644 --- a/src/HotUI.Blazor/Components/BView.cs +++ b/src/HotUI.Blazor/Components/BView.cs @@ -16,18 +16,26 @@ namespace HotUI.Blazor.Components // Check if unsupported as this can cause infinite recursion if not checked if (View.IsIUnsupportednternalView()) { - builder.AddContent(2, $"Unsupported view: {View.GetType()}"); + builder.OpenElement(2, "div"); + builder.AddAttribute(3, "class", "alert alert-warning"); + builder.AddAttribute(4, "role", "alert"); + builder.AddMarkupContent(5, $"Unsupported view: {View.GetType()}"); + builder.CloseElement(); } else if (View?.GetOrCreateViewHandler() is IBlazorViewHandler handler) { - builder.OpenComponent(3, handler.Component); + builder.OpenComponent(6, handler.Component); builder.SetKey(handler); builder.AddComponentReferenceCapture(4, handler.OnComponentLoad); builder.CloseComponent(); } else { - builder.AddContent(5, "Error: No view"); + builder.OpenElement(7, "div"); + builder.AddAttribute(8, "class", "alert alert-danger"); + builder.AddAttribute(9, "role", "alert"); + builder.AddMarkupContent(10, $"Invalid view handler: {View.GetType()}"); + builder.CloseElement(); } builder.CloseElement(); diff --git a/src/HotUI.Blazor/Handlers/ProgressBarHandler.cs b/src/HotUI.Blazor/Handlers/ProgressBarHandler.cs new file mode 100644 index 0000000..3b381ee --- /dev/null +++ b/src/HotUI.Blazor/Handlers/ProgressBarHandler.cs @@ -0,0 +1,34 @@ +using System; +using HotUI.Blazor.Components; + +namespace HotUI.Blazor.Handlers +{ + internal class ProgressBarHandler : BlazorHandler + { + public static readonly PropertyMapper Mapper = new PropertyMapper + { + { nameof(ProgressBar.Value), MapValueProperty }, + { nameof(ProgressBar.IsIndeterminate), MapIsIndeterminateProperty } + }; + + public ProgressBarHandler() + : base(Mapper) + { + } + + public static void MapValueProperty(IViewHandler viewHandler, ProgressBar virtualView) + { + var nativeView = (BProgressBar)viewHandler.NativeView; + + nativeView.Value = virtualView.Value; + } + + + private static void MapIsIndeterminateProperty(IViewHandler viewHandler, ProgressBar virtualView) + { + var nativeView = (BProgressBar)viewHandler.NativeView; + + nativeView.IsIndeterminate = virtualView.IsIndeterminate; + } + } +} diff --git a/src/HotUI.Blazor/UI.cs b/src/HotUI.Blazor/UI.cs index 9acacf5..ff6f91c 100644 --- a/src/HotUI.Blazor/UI.cs +++ b/src/HotUI.Blazor/UI.cs @@ -24,6 +24,7 @@ namespace HotUI.Blazor Registrar.Handlers.Register(); Registrar.Handlers.Register(); Registrar.Handlers.Register(); + Registrar.Handlers.Register(); Device.PerformInvokeOnMainThread = a => a(); Device.OnStateChanged = view => () => From 3a80c7f10e4d6c4667eea3412763f5cfac5ec832 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 25 Jul 2019 08:49:11 -0700 Subject: [PATCH 3/4] Add image handler and middleware --- sample/HotUI.Blazor.Sample/Startup.cs | 9 +-- sample/HotUI.Samples/ListViewDetails.cs | 10 ++-- src/HotUI.Blazor/BlazorExtensions.cs | 6 -- src/HotUI.Blazor/Components/BImage.razor | 10 ++++ src/HotUI.Blazor/Components/BLabel.razor | 4 +- src/HotUI.Blazor/Handlers/ImageHandler.cs | 24 ++++++++ src/HotUI.Blazor/HotUIExtensions.cs | 20 +++++++ .../Middleware/Images/BitmapExtensions.cs | 27 +++++++++ .../Middleware/Images/BitmapMiddleware.cs | 57 +++++++++++++++++++ .../Middleware/Images/BitmapRepository.cs | 31 ++++++++++ .../Middleware/Images/BlazorBitmap.cs | 46 +++++++++++++++ .../Middleware/Images/BlazorBitmapService.cs | 29 ++++++++++ src/HotUI.Blazor/UI.cs | 4 +- 13 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 src/HotUI.Blazor/Components/BImage.razor create mode 100644 src/HotUI.Blazor/Handlers/ImageHandler.cs create mode 100644 src/HotUI.Blazor/HotUIExtensions.cs create mode 100644 src/HotUI.Blazor/Middleware/Images/BitmapExtensions.cs create mode 100644 src/HotUI.Blazor/Middleware/Images/BitmapMiddleware.cs create mode 100644 src/HotUI.Blazor/Middleware/Images/BitmapRepository.cs create mode 100644 src/HotUI.Blazor/Middleware/Images/BlazorBitmap.cs create mode 100644 src/HotUI.Blazor/Middleware/Images/BlazorBitmapService.cs diff --git a/sample/HotUI.Blazor.Sample/Startup.cs b/sample/HotUI.Blazor.Sample/Startup.cs index 5053200..594237b 100644 --- a/sample/HotUI.Blazor.Sample/Startup.cs +++ b/sample/HotUI.Blazor.Sample/Startup.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using HotUI.Blazor.Sample.Data; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using HotUI.Blazor.Sample.Data; namespace HotUI.Blazor.Sample { @@ -25,6 +20,7 @@ namespace HotUI.Blazor.Sample // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddHotUI(); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton(); @@ -48,6 +44,7 @@ namespace HotUI.Blazor.Sample app.UseStaticFiles(); app.UseRouting(); + app.UseHotUI(); app.UseEndpoints(endpoints => { diff --git a/sample/HotUI.Samples/ListViewDetails.cs b/sample/HotUI.Samples/ListViewDetails.cs index d4c476e..b4bc7e4 100644 --- a/sample/HotUI.Samples/ListViewDetails.cs +++ b/sample/HotUI.Samples/ListViewDetails.cs @@ -4,15 +4,15 @@ using HotUI.Samples.Models; namespace HotUI.Samples { public class ListViewDetails : View { [State] - Song song; + readonly Song song; public ListViewDetails (Song song) { this.song = song; Body = () => new VStack { - new Image(song.ArtworkUrl), - new Text(song.Title), - new Text(song.Artist), - new Text(song.Album), + new Image(() => song.ArtworkUrl), + new Text(() => song.Title), + new Text(() => song.Artist), + new Text(() => song.Album), }; } } diff --git a/src/HotUI.Blazor/BlazorExtensions.cs b/src/HotUI.Blazor/BlazorExtensions.cs index 74f75bf..a846e70 100644 --- a/src/HotUI.Blazor/BlazorExtensions.cs +++ b/src/HotUI.Blazor/BlazorExtensions.cs @@ -1,15 +1,9 @@ using HotUI.Blazor.Handlers; -using Microsoft.AspNetCore.Components.RenderTree; namespace HotUI.Blazor { internal static class BlazorExtensions { - static BlazorExtensions() - { - UI.Init(); - } - public static IBlazorViewHandler GetOrCreateViewHandler(this View view) { if (view == null) diff --git a/src/HotUI.Blazor/Components/BImage.razor b/src/HotUI.Blazor/Components/BImage.razor new file mode 100644 index 0000000..1f48534 --- /dev/null +++ b/src/HotUI.Blazor/Components/BImage.razor @@ -0,0 +1,10 @@ +@inherits HotUIComponentBase + + + + + +@code { + [Parameter] + public string Url { get; set; } +} diff --git a/src/HotUI.Blazor/Components/BLabel.razor b/src/HotUI.Blazor/Components/BLabel.razor index 8769889..ea65b20 100644 --- a/src/HotUI.Blazor/Components/BLabel.razor +++ b/src/HotUI.Blazor/Components/BLabel.razor @@ -1,8 +1,8 @@ @inherits HotUIComponentBase - +

@Value

- +
@code { [Parameter] diff --git a/src/HotUI.Blazor/Handlers/ImageHandler.cs b/src/HotUI.Blazor/Handlers/ImageHandler.cs new file mode 100644 index 0000000..1236bca --- /dev/null +++ b/src/HotUI.Blazor/Handlers/ImageHandler.cs @@ -0,0 +1,24 @@ +using HotUI.Blazor.Components; + +namespace HotUI.Blazor.Handlers +{ + internal class ImageHandler : BlazorHandler + { + public static readonly PropertyMapper Mapper = new PropertyMapper + { + { nameof(Image.Bitmap), MapBitmapProperty }, + }; + + public ImageHandler() + : base(Mapper) + { + } + + public static void MapBitmapProperty(IViewHandler viewHandler, Image virtualView) + { + var nativeView = (BImage)viewHandler.NativeView; + + nativeView.Url = (string)virtualView.Bitmap.NativeBitmap; + } + } +} diff --git a/src/HotUI.Blazor/HotUIExtensions.cs b/src/HotUI.Blazor/HotUIExtensions.cs new file mode 100644 index 0000000..09f5742 --- /dev/null +++ b/src/HotUI.Blazor/HotUIExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace HotUI.Blazor +{ + public static class HotUIExtensions + { + public static void AddHotUI(this IServiceCollection services) + { + UI.Init(); + + services.AddImages(); + } + + public static void UseHotUI(this IApplicationBuilder app) + { + app.UseImages(); + } + } +} diff --git a/src/HotUI.Blazor/Middleware/Images/BitmapExtensions.cs b/src/HotUI.Blazor/Middleware/Images/BitmapExtensions.cs new file mode 100644 index 0000000..9638fa3 --- /dev/null +++ b/src/HotUI.Blazor/Middleware/Images/BitmapExtensions.cs @@ -0,0 +1,27 @@ +using HotUI.Blazor.Middleware.Images; +using HotUI.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace HotUI.Blazor +{ + internal static class BitmapExtensions + { + public static void AddImages(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient(); + } + + public static void UseImages(this IApplicationBuilder app) + { + Device.BitmapService = app.ApplicationServices.GetRequiredService(); + + app.Map(BlazorBitmap.Prefix, app2 => + { + app2.UseMiddleware(); + }); + } + } +} diff --git a/src/HotUI.Blazor/Middleware/Images/BitmapMiddleware.cs b/src/HotUI.Blazor/Middleware/Images/BitmapMiddleware.cs new file mode 100644 index 0000000..4a229a5 --- /dev/null +++ b/src/HotUI.Blazor/Middleware/Images/BitmapMiddleware.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.Net.Http; +using System.Threading.Tasks; + +namespace HotUI.Blazor.Middleware.Images +{ + internal class BitmapMiddleware : IMiddleware + { + private readonly HttpClient _client; + private readonly BitmapRepository _repository; + private readonly ILogger _logger; + + public BitmapMiddleware(HttpClient client, BitmapRepository repository, ILogger logger) + { + _client = client; + _repository = repository; + _logger = logger; + } + + public Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (_repository.TryMatch(context.Request.Path, out var url)) + { + return ProxyImageAsync(context, url); + } + else + { + context.Response.StatusCode = 404; + return Task.CompletedTask; + } + } + + private async Task ProxyImageAsync(HttpContext context, string url) + { + using (var result = await _client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false)) + { + if (!result.IsSuccessStatusCode) + { + _logger.LogInformation("Could not find image at {Url}", url); + context.Response.StatusCode = 404; + return; + } + else + { + context.Response.StatusCode = 200; + context.Response.ContentType = result.Content.Headers.ContentType.MediaType; + + using (var stream = await result.Content.ReadAsStreamAsync()) + { + await stream.CopyToAsync(context.Response.Body, context.RequestAborted).ConfigureAwait(false); + } + } + } + } + } +} diff --git a/src/HotUI.Blazor/Middleware/Images/BitmapRepository.cs b/src/HotUI.Blazor/Middleware/Images/BitmapRepository.cs new file mode 100644 index 0000000..cf8999d --- /dev/null +++ b/src/HotUI.Blazor/Middleware/Images/BitmapRepository.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Concurrent; + +namespace HotUI.Blazor.Middleware.Images +{ + internal class BitmapRepository + { + private readonly ConcurrentDictionary _repository; + + public BitmapRepository() + { + _repository = new ConcurrentDictionary(StringComparer.Ordinal); + } + + public void Add(BlazorBitmap bitmap) + { + _repository.TryAdd(bitmap.Id, bitmap.Url); + } + + public void Remove(BlazorBitmap bitmap) + { + _repository.TryRemove(bitmap.Id, out _); + } + + public bool TryMatch(PathString path, out string url) + { + return _repository.TryGetValue(path.Value.Substring(1), out url); + } + } +} diff --git a/src/HotUI.Blazor/Middleware/Images/BlazorBitmap.cs b/src/HotUI.Blazor/Middleware/Images/BlazorBitmap.cs new file mode 100644 index 0000000..984c3fd --- /dev/null +++ b/src/HotUI.Blazor/Middleware/Images/BlazorBitmap.cs @@ -0,0 +1,46 @@ +using HotUI.Graphics; +using Microsoft.AspNetCore.Http; +using System; +using System.Diagnostics; + +namespace HotUI.Blazor +{ + internal class BlazorBitmap : Bitmap + { + public static readonly PathString Prefix = "/hotui/blazor/image"; + + private readonly Action _remove; + + public BlazorBitmap(string url, Action remove) + { + Url = url; + Id = GenerateId(); + _remove = remove; + } + + public string LocalUrl => $"{Prefix}/{Id}"; + + public string Id { get; } + + public string Url { get; } + + public override SizeF Size => default; + + public override object NativeBitmap => LocalUrl; + + protected override void DisposeNative() + { + _remove(this); + } + + private static string GenerateId() + { + var guid = Guid.NewGuid(); + Span bytes = stackalloc byte[16]; + Debug.Assert(guid.TryWriteBytes(bytes)); + + // Remove any padding done with '=' + return Convert.ToBase64String(bytes).TrimEnd('='); + } + } +} diff --git a/src/HotUI.Blazor/Middleware/Images/BlazorBitmapService.cs b/src/HotUI.Blazor/Middleware/Images/BlazorBitmapService.cs new file mode 100644 index 0000000..e889df5 --- /dev/null +++ b/src/HotUI.Blazor/Middleware/Images/BlazorBitmapService.cs @@ -0,0 +1,29 @@ +using HotUI.Graphics; +using HotUI.Services; +using System.Threading.Tasks; + +namespace HotUI.Blazor.Middleware.Images +{ + internal class BlazorBitmapService : IBitmapService + { + private readonly BitmapRepository _repo; + + public BlazorBitmapService(BitmapRepository repo) + { + _repo = repo; + } + + public Task LoadBitmapAsync(string source) + { + var bitmap = new BlazorBitmap(source, _repo.Remove); + + _repo.Add(bitmap); + + return Task.FromResult(bitmap); + } + + public Task LoadBitmapFromFileAsync(string file) => LoadBitmapAsync(file); + + public Task LoadBitmapFromUrlAsync(string source) => LoadBitmapAsync(source); + } +} diff --git a/src/HotUI.Blazor/UI.cs b/src/HotUI.Blazor/UI.cs index ff6f91c..c7a16f6 100644 --- a/src/HotUI.Blazor/UI.cs +++ b/src/HotUI.Blazor/UI.cs @@ -1,5 +1,4 @@ -using HotUI.Blazor.Components; -using HotUI.Blazor.Handlers; +using HotUI.Blazor.Handlers; namespace HotUI.Blazor { @@ -25,6 +24,7 @@ namespace HotUI.Blazor Registrar.Handlers.Register(); Registrar.Handlers.Register(); Registrar.Handlers.Register(); + Registrar.Handlers.Register(); Device.PerformInvokeOnMainThread = a => a(); Device.OnStateChanged = view => () => From 13f7b5ab37eb51fa4445221000aafe5ca0e5cbb8 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Thu, 25 Jul 2019 09:33:16 -0700 Subject: [PATCH 4/4] Add breadcrumb navigation --- src/HotUI.Blazor/HotUIPage.razor | 51 +++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/HotUI.Blazor/HotUIPage.razor b/src/HotUI.Blazor/HotUIPage.razor index 132a07c..2c4eb4c 100644 --- a/src/HotUI.Blazor/HotUIPage.razor +++ b/src/HotUI.Blazor/HotUIPage.razor @@ -1,20 +1,24 @@ @using HotUI.Blazor.Components -
- @if (_views.Any()) - { - - } - else - { - - } -
- -
+
+ +
+ +
@code { - private Stack _views = new Stack(); + private readonly Stack _navigationStack = new Stack(); private View _view; [Parameter] @@ -31,7 +35,7 @@ { nav.PerformNavigate = toView => { - _views.Push(View); + _navigationStack.Push(View); View = toView; StateHasChanged(); }; @@ -39,9 +43,22 @@ } } - private void Back() + private string GetTitle(View view) { - View = _views.Pop(); - StateHasChanged(); + return view.GetEnvironment(EnvironmentKeys.View.Title) ?? "Unnamed page"; + } + + private void Back(View view) + { + while (_navigationStack.Any()) + { + View = _navigationStack.Pop(); + + if (View == view) + { + StateHasChanged(); + return; + } + } } }