BreakpointService
Descriere Generală
BreakpointService
este serviciul responsabil pentru detectarea și monitorizarea dimensiunii ferestrei browserului și a breakpoint-urilor responsive în aplicații Blazor. Permite componentelor să reacționeze la schimbări de dimensiune și să adapteze interfața în funcție de breakpoint-ul curent (Xs, Sm, Md, Lg, Xl).
Configurare
Înregistrare în Dependency Injection
// În Program.cs sau Startup.cs
builder.Services.AddScoped<IBreakpointService, BreakpointService>();
builder.Services.AddScoped<IBrowserWindowSizeProvider, BrowserWindowSizeProvider>();
// Cu opțiuni personalizate
builder.Services.Configure<ResizeOptions>(options =>
{
options.ReportRate = 100; // ms
options.EnableLogging = false;
options.SuppressInitEvent = true;
options.NotifyOnBreakpointOnly = true;
});
JavaScript necesar
// Inclus automat în FodComponents.js
window.fodResizeListenerFactory = {
listeners: {},
listenForResize: function(dotnetRef, options, id) {
if (!this.listeners[id]) {
this.listeners[id] = new FodResizeListener(id);
}
this.listeners[id].listenForResize(dotnetRef, options);
},
cancelListener: function(id) {
if (this.listeners[id]) {
this.listeners[id].cancelListener();
delete this.listeners[id];
}
}
};
Interfața IBreakpointService
public interface IBreakpointService : IAsyncDisposable
{
Task<bool> IsMediaSize(Breakpoint breakpoint);
bool IsMediaSize(Breakpoint breakpoint, Breakpoint reference);
Task<Breakpoint> GetBreakpoint();
Task<BreakpointServiceSubscribeResult> Subscribe(Action<Breakpoint> callback);
Task<BreakpointServiceSubscribeResult> Subscribe(Action<Breakpoint> callback, ResizeOptions options);
Task<bool> Unsubscribe(Guid subscriptionId);
}
Enum-uri și clase asociate
Breakpoint Enum
public enum Breakpoint
{
Xs, // Extra small
Sm, // Small
Md, // Medium
Lg, // Large
Xl, // Extra large
Xxl, // Extra extra large
SmAndDown, // Small și mai mic
MdAndDown, // Medium și mai mic
LgAndDown, // Large și mai mic
XlAndDown, // Extra large și mai mic
SmAndUp, // Small și mai mare
MdAndUp, // Medium și mai mare
LgAndUp, // Large și mai mare
XlAndUp, // Extra large și mai mare
None, // Niciunul
Always // Întotdeauna
}
ResizeOptions
public class ResizeOptions
{
public int ReportRate { get; set; } = 100; // Rate în ms
public bool EnableLogging { get; set; } = false;
public bool SuppressInitEvent { get; set; } = true;
public bool NotifyOnBreakpointOnly { get; set; } = true;
public Dictionary<string, int> BreakpointDefinitions { get; set; }
}
BrowserWindowSize
public class BrowserWindowSize
{
public int Height { get; set; }
public int Width { get; set; }
}
Metode disponibile
GetBreakpoint
Obține breakpoint-ul curent al ferestrei.
Parametri: Niciun parametru
Returnează: Task<Breakpoint>
- Breakpoint-ul curent
Breakpoint-uri default: - Xl: ≥ 1920px - Lg: ≥ 1280px - Md: ≥ 960px - Sm: ≥ 600px - Xs: < 600px
IsMediaSize
Verifică dacă breakpoint-ul curent se potrivește cu cel specificat.
Parametri:
- breakpoint: Breakpoint
- Breakpoint-ul de verificat
Returnează: Task<bool>
- True dacă se potrivește
IsMediaSize (overload)
Verifică dacă un breakpoint se potrivește cu o referință.
Parametri:
- breakpoint: Breakpoint
- Breakpoint-ul de verificat
- reference: Breakpoint
- Breakpoint-ul de referință
Returnează: bool
- True dacă se potrivește
Subscribe
Abonează la schimbări de breakpoint.
Parametri:
- callback: Action<Breakpoint>
- Funcția apelată la schimbare
- options: ResizeOptions
- Opțiuni (opțional)
Returnează: Task<BreakpointServiceSubscribeResult>
- ID subscripție și breakpoint curent
Unsubscribe
Dezabonează de la schimbări.
Parametri:
- subscriptionId: Guid
- ID-ul subscripției
Returnează: Task<bool>
- True dacă s-a dezabonat cu succes
Exemple de utilizare
Layout responsive simplu
@inject IBreakpointService BreakpointService
@implements IAsyncDisposable
<div class="@GetLayoutClass()">
@if (isMobile)
{
<MobileNavigation />
}
else
{
<DesktopNavigation />
}
<main class="content">
@Body
</main>
</div>
@code {
private Breakpoint currentBreakpoint = Breakpoint.Md;
private bool isMobile;
private Guid subscriptionId;
protected override async Task OnInitializedAsync()
{
var result = await BreakpointService.Subscribe(OnBreakpointChanged);
subscriptionId = result.SubscriptionId;
currentBreakpoint = result.Breakpoint;
UpdateLayoutState();
}
private void OnBreakpointChanged(Breakpoint breakpoint)
{
currentBreakpoint = breakpoint;
UpdateLayoutState();
InvokeAsync(StateHasChanged);
}
private void UpdateLayoutState()
{
isMobile = BreakpointService.IsMediaSize(Breakpoint.SmAndDown, currentBreakpoint);
}
private string GetLayoutClass()
{
return currentBreakpoint switch
{
Breakpoint.Xs => "layout-xs",
Breakpoint.Sm => "layout-sm",
Breakpoint.Md => "layout-md",
Breakpoint.Lg => "layout-lg",
Breakpoint.Xl => "layout-xl",
_ => "layout-default"
};
}
public async ValueTask DisposeAsync()
{
await BreakpointService.Unsubscribe(subscriptionId);
}
}
Grid responsive
@inject IBreakpointService BreakpointService
<FodGrid>
<FodItem xs="12" sm="6" md="4" lg="3">
<FodCard>
<CardContent>
<h5>Card 1</h5>
<p>Breakpoint curent: @currentBreakpoint</p>
</CardContent>
</FodCard>
</FodItem>
@foreach (var item in items)
{
<FodItem xs="12" sm="6" md="4" lg="3">
<ProductCard Product="item" />
</FodItem>
}
</FodGrid>
@code {
private Breakpoint currentBreakpoint;
private List<Product> items = new();
protected override async Task OnInitializedAsync()
{
currentBreakpoint = await BreakpointService.GetBreakpoint();
var subscription = await BreakpointService.Subscribe((bp) =>
{
currentBreakpoint = bp;
StateHasChanged();
});
}
}
Drawer responsive
@inject IBreakpointService BreakpointService
<FodDrawer @bind-Open="drawerOpen"
Variant="@GetDrawerVariant()"
Anchor="@GetDrawerAnchor()">
<DrawerContent>
<NavMenu />
</DrawerContent>
</FodDrawer>
<FodMain>
<FodToolbar>
@if (showMenuButton)
{
<FodIconButton Icon="menu" @onclick="ToggleDrawer" />
}
<FodSpacer />
<AppTitle />
</FodToolbar>
@Body
</FodMain>
@code {
private bool drawerOpen = true;
private bool showMenuButton;
private Breakpoint currentBreakpoint;
protected override async Task OnInitializedAsync()
{
var result = await BreakpointService.Subscribe(OnBreakpointChanged);
OnBreakpointChanged(result.Breakpoint);
}
private void OnBreakpointChanged(Breakpoint breakpoint)
{
currentBreakpoint = breakpoint;
showMenuButton = BreakpointService.IsMediaSize(Breakpoint.MdAndDown, breakpoint);
drawerOpen = !showMenuButton;
InvokeAsync(StateHasChanged);
}
private FodDrawerVariant GetDrawerVariant()
{
return showMenuButton ? FodDrawerVariant.Temporary : FodDrawerVariant.Persistent;
}
private FodDrawerAnchor GetDrawerAnchor()
{
return currentBreakpoint == Breakpoint.Xs ? FodDrawerAnchor.Bottom : FodDrawerAnchor.Left;
}
private void ToggleDrawer() => drawerOpen = !drawerOpen;
}
Componente adaptive
@inject IBreakpointService BreakpointService
@if (isDesktop)
{
<FodDataTable Items="@items" Dense="false">
<FodColumn Title="Nume" Field="@nameof(Item.Name)" />
<FodColumn Title="Descriere" Field="@nameof(Item.Description)" />
<FodColumn Title="Preț" Field="@nameof(Item.Price)" />
<FodColumn Title="Stoc" Field="@nameof(Item.Stock)" />
</FodDataTable>
}
else
{
<div class="mobile-list">
@foreach (var item in items)
{
<FodCard Class="mb-2">
<CardContent>
<h6>@item.Name</h6>
<p class="text-muted">@item.Description</p>
<div class="d-flex justify-content-between">
<span>Preț: @item.Price.ToString("C")</span>
<span>Stoc: @item.Stock</span>
</div>
</CardContent>
</FodCard>
}
</div>
}
@code {
private List<Item> items = new();
private bool isDesktop = true;
protected override async Task OnInitializedAsync()
{
var result = await BreakpointService.Subscribe(OnBreakpointChanged);
UpdateDisplay(result.Breakpoint);
}
private void OnBreakpointChanged(Breakpoint breakpoint)
{
UpdateDisplay(breakpoint);
InvokeAsync(StateHasChanged);
}
private void UpdateDisplay(Breakpoint breakpoint)
{
isDesktop = BreakpointService.IsMediaSize(Breakpoint.MdAndUp, breakpoint);
}
}
Service extins cu cache
public interface ICachedBreakpointService : IBreakpointService
{
Breakpoint? CachedBreakpoint { get; }
event EventHandler<Breakpoint> BreakpointChanged;
}
public class CachedBreakpointService : ICachedBreakpointService
{
private readonly IBreakpointService _innerService;
private Breakpoint? _cachedBreakpoint;
public Breakpoint? CachedBreakpoint => _cachedBreakpoint;
public event EventHandler<Breakpoint> BreakpointChanged;
public CachedBreakpointService(IBreakpointService innerService)
{
_innerService = innerService;
_ = InitializeAsync();
}
private async Task InitializeAsync()
{
var result = await _innerService.Subscribe(OnBreakpointChanged);
_cachedBreakpoint = result.Breakpoint;
}
private void OnBreakpointChanged(Breakpoint breakpoint)
{
_cachedBreakpoint = breakpoint;
BreakpointChanged?.Invoke(this, breakpoint);
}
public async Task<Breakpoint> GetBreakpoint()
{
if (_cachedBreakpoint.HasValue)
return _cachedBreakpoint.Value;
return await _innerService.GetBreakpoint();
}
}
Hook personalizat
public static class BreakpointHooks
{
public static async Task<BreakpointInfo> UseBreakpoint(
this ComponentBase component,
IBreakpointService breakpointService)
{
var info = new BreakpointInfo();
var result = await breakpointService.Subscribe((bp) =>
{
info.Current = bp;
info.IsMobile = breakpointService.IsMediaSize(Breakpoint.SmAndDown, bp);
info.IsTablet = breakpointService.IsMediaSize(Breakpoint.Md, bp);
info.IsDesktop = breakpointService.IsMediaSize(Breakpoint.LgAndUp, bp);
component.InvokeAsync(component.StateHasChanged);
});
info.SubscriptionId = result.SubscriptionId;
info.Current = result.Breakpoint;
return info;
}
}
public class BreakpointInfo
{
public Breakpoint Current { get; set; }
public bool IsMobile { get; set; }
public bool IsTablet { get; set; }
public bool IsDesktop { get; set; }
public Guid SubscriptionId { get; set; }
}
Tratare erori
Service cu fallback
public class SafeBreakpointService : IBreakpointService
{
private readonly IBreakpointService _innerService;
private readonly ILogger<SafeBreakpointService> _logger;
public async Task<Breakpoint> GetBreakpoint()
{
try
{
return await _innerService.GetBreakpoint();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get breakpoint");
return Breakpoint.Md; // Default fallback
}
}
public async Task<BreakpointServiceSubscribeResult> Subscribe(
Action<Breakpoint> callback)
{
try
{
return await _innerService.Subscribe(callback);
}
catch (JSException)
{
// Fallback pentru SSR sau JS disabled
return new BreakpointServiceSubscribeResult(
Guid.Empty,
Breakpoint.Md
);
}
}
}
Note tehnice
- Throttling - Evenimentele resize sunt limitate la ReportRate (default 100ms)
- Breakpoint only - Poate notifica doar la schimbarea breakpoint-ului
- Memory management - Important să dezabonați în Dispose
- SSR compatibility - Necesită verificări pentru server-side rendering
- Performance - Folosiți NotifyOnBreakpointOnly pentru performanță
Configurări personalizate
Breakpoint-uri custom
builder.Services.Configure<ResizeOptions>(options =>
{
options.BreakpointDefinitions = new Dictionary<string, int>
{
["Xs"] = 0,
["Sm"] = 576,
["Md"] = 768,
["Lg"] = 992,
["Xl"] = 1200,
["Xxl"] = 1400
};
});
Opțiuni per subscripție
@code {
private async Task SubscribeWithCustomOptions()
{
var customOptions = new ResizeOptions
{
ReportRate = 250, // Mai puțin frecvent
EnableLogging = true, // Debug în consolă
NotifyOnBreakpointOnly = false // Notifică la orice resize
};
await BreakpointService.Subscribe(OnResize, customOptions);
}
}
Bune practici
- Unsubscribe - Întotdeauna dezabonați în DisposeAsync
- Cache results - Cache-uiți breakpoint-ul pentru acces sincron
- Debounce - Folosiți debouncing pentru acțiuni costisitoare
- Mobile first - Proiectați pentru mobile și adaptați pentru desktop
- Test resize - Testați toate breakpoint-urile în development
- Performance - Evitați re-render-uri inutile
Concluzie
BreakpointService oferă o soluție robustă pentru gestionarea responsive design în aplicații Blazor. Cu suport pentru monitorizare în timp real, breakpoint-uri personalizabile și performanță optimizată, serviciul facilitează crearea de interfețe adaptive care oferă experiențe excelente pe toate dispozitivele.