26ff8d9b4f
Set up repository with legacy .NET Framework 4.8 source (OLD/), new .NET 10 Blazor solution (NEW/), OpenSpec specifications, documentation, and project configuration.
694 lines
22 KiB
Markdown
694 lines
22 KiB
Markdown
# Blazor UI Design
|
|
|
|
## Overview
|
|
|
|
This document describes the architecture and implementation approach for the Blazor WebAssembly user interface, including the component structure, state management, SignalR integration, and service layer design.
|
|
|
|
## Architecture
|
|
|
|
### High-Level Component Diagram
|
|
|
|
```
|
|
+------------------------------------------------------------------+
|
|
| App.razor |
|
|
| - Router |
|
|
| - CascadingAuthenticationState |
|
|
+------------------------------------------------------------------+
|
|
|
|
|
v
|
|
+------------------------------------------------------------------+
|
|
| MainLayout.razor |
|
|
| - RadzenLayout (Header, Body, Footer) |
|
|
| - AuthorizeView for user display |
|
|
| - Navigation links |
|
|
+------------------------------------------------------------------+
|
|
|
|
|
+----------------------+----------------------+
|
|
| | |
|
|
v v v
|
|
+----------------+ +------------------+ +------------------+
|
|
| Login.razor | | Searches.razor | | RefreshStatus |
|
|
| (anonymous) | | SearchEdit.razor | | .razor |
|
|
| | | SearchQueue.razor| | |
|
|
+----------------+ +------------------+ +------------------+
|
|
|
|
|
+----------+-----------+
|
|
| |
|
|
v v
|
|
+------------------+ +------------------+
|
|
| Filter Panels | | Services |
|
|
| (components) | | (DI injected) |
|
|
+------------------+ +------------------+
|
|
```
|
|
|
|
### Project Structure
|
|
|
|
```
|
|
NEW/src/JdeScoping.Client/
|
|
├── wwwroot/
|
|
│ ├── index.html
|
|
│ ├── css/
|
|
│ │ └── app.css
|
|
│ └── js/
|
|
│ └── interop.js # File download helper
|
|
├── Layout/
|
|
│ └── MainLayout.razor
|
|
├── Pages/
|
|
│ ├── Login.razor
|
|
│ ├── NotAuthorized.razor
|
|
│ ├── Searches.razor # Home/Search list
|
|
│ ├── SearchEdit.razor # Create/Edit search
|
|
│ ├── SearchQueue.razor
|
|
│ └── RefreshStatus.razor
|
|
├── Components/
|
|
│ ├── FilterPanels/
|
|
│ │ ├── TimeSpanFilterPanel.razor
|
|
│ │ ├── WorkOrderFilterPanel.razor
|
|
│ │ ├── ItemNumberFilterPanel.razor
|
|
│ │ ├── ProfitCenterFilterPanel.razor
|
|
│ │ ├── WorkCenterFilterPanel.razor
|
|
│ │ ├── ComponentLotFilterPanel.razor
|
|
│ │ ├── OperatorFilterPanel.razor
|
|
│ │ └── PartOperationFilterPanel.razor
|
|
│ └── Shared/
|
|
│ └── LoadingIndicator.razor
|
|
├── Services/
|
|
│ ├── IAuthService.cs
|
|
│ ├── AuthService.cs
|
|
│ ├── ISearchService.cs
|
|
│ ├── SearchService.cs
|
|
│ ├── ILookupService.cs
|
|
│ ├── LookupService.cs
|
|
│ ├── IFileService.cs
|
|
│ ├── FileService.cs
|
|
│ ├── IRefreshStatusService.cs
|
|
│ ├── RefreshStatusService.cs
|
|
│ ├── IHubConnectionService.cs
|
|
│ └── HubConnectionService.cs
|
|
├── Auth/
|
|
│ ├── AuthStateProvider.cs
|
|
│ └── TokenStorageService.cs
|
|
├── Models/
|
|
│ ├── ValidCombination.cs
|
|
│ ├── LoginModel.cs
|
|
│ ├── SearchViewModel.cs
|
|
│ ├── SearchCriteriaViewModel.cs
|
|
│ ├── ItemViewModel.cs
|
|
│ ├── ProfitCenterViewModel.cs
|
|
│ ├── WorkCenterViewModel.cs
|
|
│ ├── OperatorViewModel.cs
|
|
│ ├── WorkOrderViewModel.cs
|
|
│ ├── ComponentLotViewModel.cs
|
|
│ ├── PartOperationViewModel.cs
|
|
│ ├── DataUpdateViewModel.cs
|
|
│ ├── SearchUpdate.cs
|
|
│ └── StatusUpdate.cs
|
|
├── _Imports.razor
|
|
├── App.razor
|
|
└── Program.cs
|
|
```
|
|
|
|
## Blazor WebAssembly Configuration
|
|
|
|
### Program.cs Service Registration
|
|
|
|
```csharp
|
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
|
builder.RootComponents.Add<App>("#app");
|
|
builder.RootComponents.Add<HeadOutlet>("head::after");
|
|
|
|
// Configure HttpClient for API calls
|
|
builder.Services.AddScoped(sp => new HttpClient
|
|
{
|
|
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
|
|
});
|
|
|
|
// Radzen services
|
|
builder.Services.AddRadzenComponents();
|
|
builder.Services.AddScoped<DialogService>();
|
|
builder.Services.AddScoped<NotificationService>();
|
|
|
|
// Authentication
|
|
builder.Services.AddAuthorizationCore();
|
|
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
|
builder.Services.AddScoped<AuthStateProvider>();
|
|
builder.Services.AddScoped<ITokenStorageService, TokenStorageService>();
|
|
|
|
// Application services
|
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
|
builder.Services.AddScoped<ISearchService, SearchService>();
|
|
builder.Services.AddScoped<ILookupService, LookupService>();
|
|
builder.Services.AddScoped<IFileService, FileService>();
|
|
builder.Services.AddScoped<IRefreshStatusService, RefreshStatusService>();
|
|
builder.Services.AddScoped<IHubConnectionService, HubConnectionService>();
|
|
|
|
// Logging
|
|
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
|
|
|
await builder.Build().RunAsync();
|
|
```
|
|
|
|
### App.razor Router Configuration
|
|
|
|
```razor
|
|
<CascadingAuthenticationState>
|
|
<Router AppAssembly="@typeof(App).Assembly">
|
|
<Found Context="routeData">
|
|
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
|
<NotAuthorized>
|
|
<RedirectToLogin />
|
|
</NotAuthorized>
|
|
</AuthorizeRouteView>
|
|
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
|
</Found>
|
|
<NotFound>
|
|
<PageTitle>Not found</PageTitle>
|
|
<LayoutView Layout="@typeof(MainLayout)">
|
|
<p role="alert">Sorry, there's nothing at this address.</p>
|
|
</LayoutView>
|
|
</NotFound>
|
|
</Router>
|
|
</CascadingAuthenticationState>
|
|
```
|
|
|
|
## State Management
|
|
|
|
### AuthStateProvider
|
|
|
|
Custom authentication state provider for JWT token-based auth:
|
|
|
|
```csharp
|
|
public class AuthStateProvider : AuthenticationStateProvider
|
|
{
|
|
private readonly ITokenStorageService _tokenStorage;
|
|
private readonly HttpClient _httpClient;
|
|
|
|
public AuthStateProvider(ITokenStorageService tokenStorage, HttpClient httpClient)
|
|
{
|
|
_tokenStorage = tokenStorage;
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
{
|
|
var token = await _tokenStorage.GetTokenAsync();
|
|
if (string.IsNullOrEmpty(token))
|
|
{
|
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
|
}
|
|
|
|
_httpClient.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", token);
|
|
|
|
var claims = ParseClaimsFromJwt(token);
|
|
var identity = new ClaimsIdentity(claims, "jwt");
|
|
return new AuthenticationState(new ClaimsPrincipal(identity));
|
|
}
|
|
|
|
public async Task NotifyAuthenticationStateChangedAsync()
|
|
{
|
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
}
|
|
|
|
public async Task LogoutAsync()
|
|
{
|
|
await _tokenStorage.RemoveTokenAsync();
|
|
_httpClient.DefaultRequestHeaders.Authorization = null;
|
|
NotifyAuthenticationStateChanged(Task.FromResult(
|
|
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))));
|
|
}
|
|
|
|
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
|
|
{
|
|
var payload = jwt.Split('.')[1];
|
|
var jsonBytes = ParseBase64WithoutPadding(payload);
|
|
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
|
|
|
return keyValuePairs!.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()!));
|
|
}
|
|
|
|
private byte[] ParseBase64WithoutPadding(string base64)
|
|
{
|
|
switch (base64.Length % 4)
|
|
{
|
|
case 2: base64 += "=="; break;
|
|
case 3: base64 += "="; break;
|
|
}
|
|
return Convert.FromBase64String(base64);
|
|
}
|
|
}
|
|
```
|
|
|
|
### Token Storage Service
|
|
|
|
Uses browser localStorage via JS interop:
|
|
|
|
```csharp
|
|
public interface ITokenStorageService
|
|
{
|
|
Task<string?> GetTokenAsync();
|
|
Task SetTokenAsync(string token);
|
|
Task RemoveTokenAsync();
|
|
}
|
|
|
|
public class TokenStorageService : ITokenStorageService
|
|
{
|
|
private readonly IJSRuntime _jsRuntime;
|
|
private const string TokenKey = "authToken";
|
|
|
|
public TokenStorageService(IJSRuntime jsRuntime)
|
|
{
|
|
_jsRuntime = jsRuntime;
|
|
}
|
|
|
|
public async Task<string?> GetTokenAsync()
|
|
{
|
|
return await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", TokenKey);
|
|
}
|
|
|
|
public async Task SetTokenAsync(string token)
|
|
{
|
|
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", TokenKey, token);
|
|
}
|
|
|
|
public async Task RemoveTokenAsync()
|
|
{
|
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", TokenKey);
|
|
}
|
|
}
|
|
```
|
|
|
|
## SignalR Client Integration
|
|
|
|
### IHubConnectionService
|
|
|
|
```csharp
|
|
public interface IHubConnectionService
|
|
{
|
|
HubConnectionState State { get; }
|
|
event Action<SearchUpdate>? OnSearchUpdate;
|
|
event Action<StatusUpdate>? OnStatusUpdate;
|
|
|
|
Task StartAsync(CancellationToken ct = default);
|
|
Task StopAsync(CancellationToken ct = default);
|
|
Task<StatusUpdate?> GetCachedStatusAsync(CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
### HubConnectionService Implementation
|
|
|
|
```csharp
|
|
public class HubConnectionService : IHubConnectionService, IAsyncDisposable
|
|
{
|
|
private readonly HubConnection _hubConnection;
|
|
private readonly ILogger<HubConnectionService> _logger;
|
|
|
|
public HubConnectionState State => _hubConnection.State;
|
|
public event Action<SearchUpdate>? OnSearchUpdate;
|
|
public event Action<StatusUpdate>? OnStatusUpdate;
|
|
|
|
public HubConnectionService(
|
|
NavigationManager navigation,
|
|
ILogger<HubConnectionService> logger)
|
|
{
|
|
_logger = logger;
|
|
_hubConnection = new HubConnectionBuilder()
|
|
.WithUrl(navigation.ToAbsoluteUri("/hubs/status"))
|
|
.WithAutomaticReconnect(new[]
|
|
{
|
|
TimeSpan.Zero,
|
|
TimeSpan.FromSeconds(2),
|
|
TimeSpan.FromSeconds(5),
|
|
TimeSpan.FromSeconds(10),
|
|
TimeSpan.FromSeconds(30)
|
|
})
|
|
.ConfigureLogging(logging =>
|
|
{
|
|
logging.SetMinimumLevel(LogLevel.Information);
|
|
})
|
|
.Build();
|
|
|
|
// Register message handlers
|
|
_hubConnection.On<SearchUpdate>("searchUpdate", update =>
|
|
{
|
|
_logger.LogDebug("Received searchUpdate: {SearchId} - {Status}",
|
|
update.ID, update.Status);
|
|
OnSearchUpdate?.Invoke(update);
|
|
});
|
|
|
|
_hubConnection.On<StatusUpdate>("statusUpdate", update =>
|
|
{
|
|
_logger.LogDebug("Received statusUpdate: {Message}", update.Message);
|
|
OnStatusUpdate?.Invoke(update);
|
|
});
|
|
|
|
// Handle reconnection events
|
|
_hubConnection.Reconnected += connectionId =>
|
|
{
|
|
_logger.LogInformation("SignalR reconnected: {ConnectionId}", connectionId);
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
_hubConnection.Reconnecting += error =>
|
|
{
|
|
_logger.LogWarning(error, "SignalR reconnecting...");
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
_hubConnection.Closed += error =>
|
|
{
|
|
_logger.LogWarning(error, "SignalR connection closed");
|
|
return Task.CompletedTask;
|
|
};
|
|
}
|
|
|
|
public async Task StartAsync(CancellationToken ct = default)
|
|
{
|
|
if (_hubConnection.State == HubConnectionState.Disconnected)
|
|
{
|
|
_logger.LogInformation("Starting SignalR connection...");
|
|
await _hubConnection.StartAsync(ct);
|
|
_logger.LogInformation("SignalR connected");
|
|
}
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken ct = default)
|
|
{
|
|
if (_hubConnection.State == HubConnectionState.Connected)
|
|
{
|
|
await _hubConnection.StopAsync(ct);
|
|
_logger.LogInformation("SignalR disconnected");
|
|
}
|
|
}
|
|
|
|
public async Task<StatusUpdate?> GetCachedStatusAsync(CancellationToken ct = default)
|
|
{
|
|
if (_hubConnection.State == HubConnectionState.Connected)
|
|
{
|
|
return await _hubConnection.InvokeAsync<StatusUpdate?>("GetCachedStatus", ct);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _hubConnection.DisposeAsync();
|
|
}
|
|
}
|
|
```
|
|
|
|
### SignalR Message Types
|
|
|
|
```csharp
|
|
public record SearchUpdate(
|
|
int ID,
|
|
string Name,
|
|
string UserName,
|
|
SearchStatus Status,
|
|
DateTime? SubmitDT,
|
|
DateTime? StartDT,
|
|
DateTime? EndDT);
|
|
|
|
public record StatusUpdate(
|
|
string Message,
|
|
DateTime Timestamp);
|
|
```
|
|
|
|
## API Client Services
|
|
|
|
### ISearchService
|
|
|
|
```csharp
|
|
public interface ISearchService
|
|
{
|
|
Task<List<SearchViewModel>> GetUserSearchesAsync(CancellationToken ct = default);
|
|
Task<SearchViewModel> GetSearchAsync(int id, CancellationToken ct = default);
|
|
Task<SearchViewModel> CopySearchAsync(int id, CancellationToken ct = default);
|
|
Task<int> SaveSearchAsync(SearchViewModel search, CancellationToken ct = default);
|
|
Task<List<SearchViewModel>> GetQueueAsync(CancellationToken ct = default);
|
|
Task DownloadResultsAsync(int searchId, CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
### ILookupService
|
|
|
|
```csharp
|
|
public interface ILookupService
|
|
{
|
|
Task<IEnumerable<ItemViewModel>> FindItemsAsync(string query, CancellationToken ct = default);
|
|
Task<IEnumerable<ProfitCenterViewModel>> FindProfitCentersAsync(string query, CancellationToken ct = default);
|
|
Task<IEnumerable<WorkCenterViewModel>> FindWorkCentersAsync(string query, CancellationToken ct = default);
|
|
Task<IEnumerable<OperatorViewModel>> FindOperatorsAsync(string query, CancellationToken ct = default);
|
|
}
|
|
```
|
|
|
|
### IFileService
|
|
|
|
```csharp
|
|
public interface IFileService
|
|
{
|
|
Task DownloadTemplateAsync(string templateType, CancellationToken ct = default);
|
|
Task<FileUploadResult<T>> UploadAsync<T>(IBrowserFile file, string endpoint, CancellationToken ct = default);
|
|
}
|
|
|
|
public record FileUploadResult<T>(
|
|
bool WasSuccessful,
|
|
string? ErrorMessage,
|
|
List<T>? Data);
|
|
```
|
|
|
|
## File Download via JS Interop
|
|
|
|
### wwwroot/js/interop.js
|
|
|
|
```javascript
|
|
window.downloadFileFromStream = async (fileName, streamRef) => {
|
|
const arrayBuffer = await streamRef.arrayBuffer();
|
|
const blob = new Blob([arrayBuffer]);
|
|
const url = URL.createObjectURL(blob);
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = fileName ?? '';
|
|
anchor.click();
|
|
anchor.remove();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
window.downloadFileFromUrl = (url, fileName) => {
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = fileName ?? '';
|
|
anchor.click();
|
|
anchor.remove();
|
|
};
|
|
```
|
|
|
|
### FileService Download Implementation
|
|
|
|
```csharp
|
|
public async Task DownloadResultsAsync(int searchId, CancellationToken ct)
|
|
{
|
|
var response = await _httpClient.GetAsync($"/api/search/{searchId}/results", ct);
|
|
response.EnsureSuccessStatusCode();
|
|
|
|
var fileName = $"Search_{searchId}_Results.xlsx";
|
|
var content = await response.Content.ReadAsStreamAsync(ct);
|
|
|
|
using var streamRef = new DotNetStreamReference(content);
|
|
await _jsRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, streamRef);
|
|
}
|
|
```
|
|
|
|
## Filter Panel Component Pattern
|
|
|
|
### Base Structure
|
|
|
|
All filter panels follow a consistent pattern with:
|
|
- Card container with title
|
|
- Optional toolbar (Download Template, Upload Data, Clear Data)
|
|
- Input controls (autocomplete, date picker, etc.)
|
|
- Data grid of selected items
|
|
- Count display
|
|
|
|
### Two-Way Binding Pattern
|
|
|
|
```razor
|
|
@* ItemNumberFilterPanel.razor - simplified *@
|
|
<RadzenCard>
|
|
<RadzenStack Gap="1rem">
|
|
<RadzenText TextStyle="TextStyle.H6" Text="Filter by item number" />
|
|
|
|
@if (!IsReadOnly)
|
|
{
|
|
<RadzenRow Gap="1rem" AlignItems="AlignItems.End">
|
|
<RadzenColumn Size="8">
|
|
<RadzenAutoComplete @bind-Value="@selectedItemText"
|
|
Data="@itemSuggestions"
|
|
LoadData="@LoadItemSuggestions"
|
|
MinLength="3" />
|
|
</RadzenColumn>
|
|
<RadzenColumn Size="4">
|
|
<RadzenButton Text="Add to filter" Click="@HandleAddItem" />
|
|
</RadzenColumn>
|
|
</RadzenRow>
|
|
}
|
|
|
|
<RadzenDataGrid Data="@Items" TItem="ItemViewModel">
|
|
<Columns>
|
|
<RadzenDataGridColumn Property="ItemNumber" Title="Item Number" />
|
|
<RadzenDataGridColumn Property="Description" Title="Description" />
|
|
@if (!IsReadOnly)
|
|
{
|
|
<RadzenDataGridColumn Title="Actions" Width="100px">
|
|
<Template Context="item">
|
|
<RadzenButton Icon="delete" Click="@(() => HandleDeleteItem(item))" />
|
|
</Template>
|
|
</RadzenDataGridColumn>
|
|
}
|
|
</Columns>
|
|
</RadzenDataGrid>
|
|
|
|
<RadzenText Text="@($"# of item numbers: {Items.Count}")" />
|
|
</RadzenStack>
|
|
</RadzenCard>
|
|
|
|
@code {
|
|
[Parameter] public List<ItemViewModel> Items { get; set; } = new();
|
|
[Parameter] public EventCallback<List<ItemViewModel>> ItemsChanged { get; set; }
|
|
[Parameter] public bool IsReadOnly { get; set; }
|
|
|
|
private async Task HandleAddItem()
|
|
{
|
|
// Add item and notify parent
|
|
Items.Add(new ItemViewModel { ... });
|
|
await ItemsChanged.InvokeAsync(Items);
|
|
}
|
|
|
|
private async Task HandleDeleteItem(ItemViewModel item)
|
|
{
|
|
Items.Remove(item);
|
|
await ItemsChanged.InvokeAsync(Items);
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Global Error Boundary
|
|
|
|
```razor
|
|
@* In MainLayout.razor *@
|
|
<ErrorBoundary @ref="errorBoundary">
|
|
<ChildContent>
|
|
@Body
|
|
</ChildContent>
|
|
<ErrorContent Context="exception">
|
|
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true">
|
|
An error occurred. Please try again or contact support.
|
|
@if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
|
|
{
|
|
<pre>@exception.Message</pre>
|
|
}
|
|
</RadzenAlert>
|
|
</ErrorContent>
|
|
</ErrorBoundary>
|
|
|
|
@code {
|
|
private ErrorBoundary? errorBoundary;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
errorBoundary?.Recover();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Service-Level Error Handling
|
|
|
|
```csharp
|
|
public class SearchService : ISearchService
|
|
{
|
|
private readonly HttpClient _httpClient;
|
|
private readonly NotificationService _notificationService;
|
|
private readonly ILogger<SearchService> _logger;
|
|
|
|
public async Task<List<SearchViewModel>> GetUserSearchesAsync(CancellationToken ct)
|
|
{
|
|
try
|
|
{
|
|
return await _httpClient.GetFromJsonAsync<List<SearchViewModel>>(
|
|
"/api/search", ct) ?? new();
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to fetch user searches");
|
|
_notificationService.Notify(NotificationSeverity.Error,
|
|
"Error", "Failed to load searches. Please try again.");
|
|
return new();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Loading States
|
|
|
|
### Component-Level Loading
|
|
|
|
```razor
|
|
@if (isLoading)
|
|
{
|
|
<RadzenStack AlignItems="AlignItems.Center" JustifyContent="JustifyContent.Center"
|
|
Style="min-height: 200px;">
|
|
<RadzenProgressBarCircular Mode="ProgressBarMode.Indeterminate" Size="60" />
|
|
<RadzenText Text="Loading..." />
|
|
</RadzenStack>
|
|
}
|
|
else
|
|
{
|
|
@* Content *@
|
|
}
|
|
```
|
|
|
|
### Grid-Level Loading
|
|
|
|
```razor
|
|
<RadzenDataGrid TItem="SearchViewModel" Data="@searches"
|
|
IsLoading="@isLoading" Style="min-height: 450px;">
|
|
```
|
|
|
|
## Radzen Component Reference
|
|
|
|
| UI Element | Component | Key Properties |
|
|
|------------|-----------|----------------|
|
|
| Layout container | RadzenLayout | RadzenHeader, RadzenBody, RadzenFooter |
|
|
| Navigation | RadzenLink, NavigationManager | Path, NavigateTo |
|
|
| Data grid | RadzenDataGrid<T> | AllowPaging, AllowSorting, IsLoading |
|
|
| Dropdown | RadzenDropDown<T> | Data, TextProperty, ValueProperty |
|
|
| Autocomplete | RadzenAutoComplete | LoadData, MinLength |
|
|
| Date picker | RadzenDatePicker | DateFormat, Min, Max |
|
|
| Text input | RadzenTextBox | Placeholder, Disabled |
|
|
| Password | RadzenPassword | Placeholder |
|
|
| Button | RadzenButton | Text, Icon, ButtonStyle, IsBusy |
|
|
| Card | RadzenCard | - |
|
|
| Alert | RadzenAlert | AlertStyle, ShowIcon |
|
|
| Badge | RadzenBadge | BadgeStyle, Text |
|
|
| Progress | RadzenProgressBarCircular | Mode="Indeterminate" |
|
|
| Dialog | DialogService | Confirm, Alert |
|
|
| Notification | NotificationService | Notify |
|
|
| File upload | RadzenUpload | Url, Accept, Complete |
|
|
| Checkbox | RadzenCheckBox | @bind-Value |
|
|
| Validation | EditForm + DataAnnotationsValidator | OnValidSubmit |
|
|
|
|
## NuGet Dependencies
|
|
|
|
The JdeScoping.Client project already includes required packages:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.*" />
|
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.*" />
|
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.*" />
|
|
<PackageReference Include="Radzen.Blazor" Version="8.*" />
|
|
</ItemGroup>
|
|
```
|