Merge pull request #97 from twsouthwick/blazor

Enable progress bar, images, and breadcrumb navigation in Blazor
This commit is contained in:
jonlipsky 2019-07-25 10:31:13 -07:00 коммит произвёл GitHub
Родитель 240b91c3ef 13f7b5ab37
Коммит 5462494722
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 359 добавлений и 42 удалений

Просмотреть файл

@ -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<WeatherForecastService>();
@ -48,6 +44,7 @@ namespace HotUI.Blazor.Sample
app.UseStaticFiles();
app.UseRouting();
app.UseHotUI();
app.UseEndpoints(endpoints =>
{

Просмотреть файл

@ -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),
};
}
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -0,0 +1,10 @@
@inherits HotUIComponentBase
<span class="hotui-img">
<img src="@Url" />
</span>
@code {
[Parameter]
public string Url { get; set; }
}

Просмотреть файл

@ -1,8 +1,8 @@
@inherits HotUIComponentBase
<span class="hotui-label">
<div class="hotui-label">
<p>@Value</p>
</span>
</div>
@code {
[Parameter]

Просмотреть файл

@ -0,0 +1,22 @@
@inherits HotUIComponentBase
@if (IsIndeterminate)
{
<div class="hotui-progress progress">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" />
</div>
}
else
{
<div class="hotui-progress progress">
<div class="progress-bar" role="progressbar" style="width: @Value%" aria-valuenow="@Value" aria-valuemin="0" aria-valuemax="100" />
</div>
}
@code {
[Parameter]
public double Value { get; set; }
[Parameter]
public bool IsIndeterminate { get; set; }
}

Просмотреть файл

@ -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: <b>{View.GetType()}</b>");
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: <b>{View.GetType()}</b>");
builder.CloseElement();
}
builder.CloseElement();

Просмотреть файл

@ -8,6 +8,6 @@ namespace HotUI.Blazor.Components
{
}
internal void NotifyUpdate() => base.StateHasChanged();
internal void NotifyUpdate() => Invoke(StateHasChanged);
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using HotUI.Blazor.Components;
namespace HotUI.Blazor.Handlers
{
internal class ImageHandler : BlazorHandler<Image, BImage>
{
public static readonly PropertyMapper<Image> Mapper = new PropertyMapper<Image>
{
{ 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;
}
}
}

Просмотреть файл

@ -0,0 +1,34 @@
using System;
using HotUI.Blazor.Components;
namespace HotUI.Blazor.Handlers
{
internal class ProgressBarHandler : BlazorHandler<ProgressBar, BProgressBar>
{
public static readonly PropertyMapper<ProgressBar> Mapper = new PropertyMapper<ProgressBar>
{
{ 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;
}
}
}

Просмотреть файл

@ -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();
}
}
}

Просмотреть файл

@ -1,20 +1,24 @@
@using HotUI.Blazor.Components
<div class="hotui-page">
@if (_views.Any())
{
<button class="btn btn-link" @onclick="@Back">Back</button>
}
else
{
<button class="btn btn-link" @onclick="@Back" disabled>Back</button>
}
<br />
<BView @key="@View" View="@View" />
</div>
<div class="hotui-page">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@foreach (var view in _navigationStack.Reverse())
{
<li class="breadcrumb-item" aria-current="page">
<a href="#" @onclick="@(()=>Back(view))">@GetTitle(view)</a>
</li>
}
<li class="breadcrumb-item active" aria-current="page">@GetTitle(View)</li>
</ol>
</nav>
<br />
<BView @key="@View" View="@View" />
</div>
@code {
private Stack<View> _views = new Stack<View>();
private readonly Stack<View> _navigationStack = new Stack<View>();
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<string>(EnvironmentKeys.View.Title) ?? "Unnamed page";
}
private void Back(View view)
{
while (_navigationStack.Any())
{
View = _navigationStack.Pop();
if (View == view)
{
StateHasChanged();
return;
}
}
}
}

Просмотреть файл

@ -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<BitmapRepository>();
services.AddSingleton<IBitmapService, BlazorBitmapService>();
services.AddHttpClient<BitmapMiddleware>();
}
public static void UseImages(this IApplicationBuilder app)
{
Device.BitmapService = app.ApplicationServices.GetRequiredService<IBitmapService>();
app.Map(BlazorBitmap.Prefix, app2 =>
{
app2.UseMiddleware<BitmapMiddleware>();
});
}
}
}

Просмотреть файл

@ -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<BitmapMiddleware> _logger;
public BitmapMiddleware(HttpClient client, BitmapRepository repository, ILogger<BitmapMiddleware> 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);
}
}
}
}
}
}

Просмотреть файл

@ -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<string, string> _repository;
public BitmapRepository()
{
_repository = new ConcurrentDictionary<string, string>(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);
}
}
}

Просмотреть файл

@ -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<BlazorBitmap> _remove;
public BlazorBitmap(string url, Action<BlazorBitmap> 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<byte> bytes = stackalloc byte[16];
Debug.Assert(guid.TryWriteBytes(bytes));
// Remove any padding done with '='
return Convert.ToBase64String(bytes).TrimEnd('=');
}
}
}

Просмотреть файл

@ -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<Bitmap> LoadBitmapAsync(string source)
{
var bitmap = new BlazorBitmap(source, _repo.Remove);
_repo.Add(bitmap);
return Task.FromResult<Bitmap>(bitmap);
}
public Task<Bitmap> LoadBitmapFromFileAsync(string file) => LoadBitmapAsync(file);
public Task<Bitmap> LoadBitmapFromUrlAsync(string source) => LoadBitmapAsync(source);
}
}

Просмотреть файл

@ -1,5 +1,4 @@
using HotUI.Blazor.Components;
using HotUI.Blazor.Handlers;
using HotUI.Blazor.Handlers;
namespace HotUI.Blazor
{
@ -24,6 +23,8 @@ namespace HotUI.Blazor
Registrar.Handlers.Register<ListView, ListViewHandler>();
Registrar.Handlers.Register<Spacer, SpacerHandler>();
Registrar.Handlers.Register<TextField, TextFieldHandler>();
Registrar.Handlers.Register<ProgressBar, ProgressBarHandler>();
Registrar.Handlers.Register<Image, ImageHandler>();
Device.PerformInvokeOnMainThread = a => a();
Device.OnStateChanged = view => () =>