зеркало из https://github.com/jsuarezruiz/HotUI.git
Merge pull request #97 from twsouthwick/blazor
Enable progress bar, images, and breadcrumb navigation in Blazor
This commit is contained in:
Коммит
5462494722
|
@ -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())
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
@foreach (var view in _navigationStack.Reverse())
|
||||
{
|
||||
<button class="btn btn-link" @onclick="@Back">Back</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-link" @onclick="@Back" disabled>Back</button>
|
||||
<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)
|
||||
{
|
||||
return view.GetEnvironment<string>(EnvironmentKeys.View.Title) ?? "Unnamed page";
|
||||
}
|
||||
|
||||
private void Back(View view)
|
||||
{
|
||||
while (_navigationStack.Any())
|
||||
{
|
||||
View = _navigationStack.Pop();
|
||||
|
||||
if (View == view)
|
||||
{
|
||||
View = _views.Pop();
|
||||
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 => () =>
|
||||
|
|
Загрузка…
Ссылка в новой задаче