Udostępnij za pośrednictwem


kontekst synchronizacji ASP.NET Core Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Platforma Blazor używa kontekstu synchronizacji (SynchronizationContext), aby wymuszać pojedynczy wątek logiczny wykonywania. Metody cyklu życia składnika i wywołania zwrotne zdarzeń zgłaszane przez platformę Blazor są wykonywane w kontekście synchronizacji.

BlazorKontekst synchronizacji po stronie serwera próbuje emulować jednowątkowe środowisko, tak aby ściśle pasował do modelu zestawu WebAssembly w przeglądarce, który jest wątek. Ta emulacja jest ograniczona tylko do pojedynczego obwodu, co oznacza, że dwa różne obwody mogą działać równolegle. W dowolnym momencie w obwodzie praca jest wykonywana na dokładnie jednym wątku, co daje wrażenie pojedynczego wątku logicznego. Żadne dwie operacje nie są wykonywane współbieżnie w tym samym obwodzie.

Unikanie wywołań blokujących wątki

Ogólnie rzecz biorąc, nie należy wywoływać następujących metod w składnikach. Poniższe metody blokują wątek wykonywania, a tym samym blokują wznowienie działania aplikacji, dopóki nie zostanie wykonany bazowy element Task:

Uwaga

Przykłady w dokumentacji platformy Blazor, które używają metod blokujących wątki wymienionych w tej sekcji, robią to jedynie w celach demonstracyjnych, co nie stanowi zaleceń dotyczących programowania. Na przykład kilka przykładów kodu składników symuluje długo działający proces przez wywołanie metody Thread.Sleep.

Wywoływanie metod składników zewnętrznie w celu zaktualizowania stanu

Jeśli składnik musi być aktualizowany na podstawie zdarzenia zewnętrznego, takiego jak czasomierz lub inne powiadomienie, użyj metody InvokeAsync, która kieruje wykonywanie kodu do kontekstu synchronizacji platformy Blazor. Na przykład rozważmy następującą usługę powiadamiania, która może powiadamiać dowolny nasłuchujący składnik o zaktualizowanym stanie. Metodę Update można wywoływać z dowolnego miejsca w aplikacji.

TimerService.cs:

namespace BlazorSample;

public class TimerService(NotifierService notifier, 
    ILogger<TimerService> logger) : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger = logger;
    private readonly NotifierService notifier = notifier;
    private PeriodicTimer? timer;

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation("ElapsedCount {Count}", elapsedCount);
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation($"elapsedCount: {elapsedCount}");
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly static TimeSpan heartbeatTickRate = TimeSpan.FromSeconds(5);
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private PeriodicTimer? timer;

    public TimerService(NotifierService notifier,
        ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public async Task Start()
    {
        if (timer is null)
        {
            timer = new(heartbeatTickRate);
            logger.LogInformation("Started");

            using (timer)
            {
                while (await timer.WaitForNextTickAsync())
                {
                    elapsedCount += 1;
                    await notifier.Update("elapsedCount", elapsedCount);
                    logger.LogInformation($"elapsedCount: {elapsedCount}");
                }
            }
        }
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation($"elapsedCount: {elapsedCount}");
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}
using System;
using System.Timers;
using Microsoft.Extensions.Logging;

public class TimerService : IDisposable
{
    private int elapsedCount;
    private readonly ILogger<TimerService> logger;
    private readonly NotifierService notifier;
    private Timer timer;

    public TimerService(NotifierService notifier, ILogger<TimerService> logger)
    {
        this.notifier = notifier;
        this.logger = logger;
    }

    public void Start()
    {
        if (timer is null)
        {
            timer = new Timer();
            timer.AutoReset = true;
            timer.Interval = 10000;
            timer.Elapsed += HandleTimer;
            timer.Enabled = true;
            logger.LogInformation("Started");
        }
    }

    private async void HandleTimer(object source, ElapsedEventArgs e)
    {
        elapsedCount += 1;
        await notifier.Update("elapsedCount", elapsedCount);
        logger.LogInformation($"elapsedCount: {elapsedCount}");
    }

    public void Dispose()
    {
        timer?.Dispose();
    }
}

NotifierService.cs:

namespace BlazorSample;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task>? Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;

public class NotifierService
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}

Rejestrowanie usług:

  • W przypadku programowania po stronie klienta zarejestruj usługi jako singletony w pliku po stronie Program klienta:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • W przypadku programowania po stronie serwera zarejestruj usługi w zakresie w pliku serwera Program :

    builder.Services.AddScoped<NotifierService>();
    builder.Services.AddScoped<TimerService>();
    

Użyj obiektu NotifierService, aby zaktualizować składnik.

Notifications.razor:

@page "/notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<PageTitle>Notifications</PageTitle>

<h1>Notifications Example</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose() => Notifier.Notify -= OnNotify;
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        _ = Task.Run(Timer.Start);
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key is not null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

ReceiveNotifications.razor:

@page "/receive-notifications"
@implements IDisposable
@inject NotifierService Notifier
@inject TimerService Timer

<h1>Receive Notifications</h1>

<h2>Timer Service</h2>

<button @onclick="StartTimer">Start Timer</button>

<h2>Notifications</h2>

<p>
    Status:
    @if (lastNotification.key != null)
    {
        <span>@lastNotification.key = @lastNotification.value</span>
    }
    else
    {
        <span>Awaiting notification</span>
    }
</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    private void StartTimer()
    {
        Timer.Start();
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

W powyższym przykładzie:

  • Czasomierz jest inicjowany poza Blazorkontekstem synchronizacji za pomocą polecenia _ = Task.Run(Timer.Start).
  • NotifierService wywołuje metodę OnNotify składnika. Metoda InvokeAsync jest używana w celu przełączenia na właściwy kontekst i dodania operacji renderowania do kolejki. Aby uzyskać więcej informacji, zobacz Renderowanie składników platformy ASP.NET Core Razor.
  • Składnik implementuje interfejs IDisposable. Subskrypcja delegata OnNotify jest anulowana w metodzie Dispose, która jest wywoływana przez platformę podczas usuwania składnika. Aby uzyskać więcej informacji, zobacz Cykl życia składników platformy ASP.NET Core Razor.
  • Obiekt NotifierService wywołuje metodę OnNotify składnika poza kontekstem synchronizacji platformy Blazor. Metoda InvokeAsync jest używana w celu przełączenia na właściwy kontekst i dodania operacji renderowania do kolejki. Aby uzyskać więcej informacji, zobacz Renderowanie składników platformy ASP.NET Core Razor.
  • Składnik implementuje interfejs IDisposable. Subskrypcja delegata OnNotify jest anulowana w metodzie Dispose, która jest wywoływana przez platformę podczas usuwania składnika. Aby uzyskać więcej informacji, zobacz Cykl życia składników platformy ASP.NET Core Razor.

Ważne

Razor Jeśli składnik definiuje zdarzenie wyzwalane z wątku w tle, składnik może być wymagany do przechwycenia i przywrócenia kontekstu wykonywania (ExecutionContext) w momencie zarejestrowania programu obsługi. Aby uzyskać więcej informacji, zobacz Wywoływanie InvokeAsync(StateHasChanged) powoduje powrót strony do domyślnej kultury (dotnet/aspnetcore #28521).

Aby wysłać przechwycone wyjątki z tła TimerService do składnika w celu traktowania wyjątków, takich jak wyjątki zdarzeń normalnego cyklu życia, zobacz sekcję Obsługa przechwyconych wyjątków poza cyklem Razor życia składnika.

Obsługa przechwyconych wyjątków poza cyklem Razor życia składnika

Użyj ComponentBase.DispatchExceptionAsync w składniku Razor , aby przetworzyć wyjątki zgłoszone poza stosem wywołań cyklu życia składnika. Pozwala to kodowi składnika traktować wyjątki tak, jakby były wyjątkami metody cyklu życia. Następnie mechanizmy obsługi błędów, Blazortakie jak granice błędów, mogą przetwarzać wyjątki.

Uwaga

ComponentBase.DispatchExceptionAsync jest używany w Razor plikach składników (.razor), które dziedziczą z ComponentBase. Podczas tworzenia składników, które implement IComponent directlyużywają polecenia RenderHandle.DispatchExceptionAsync.

Aby obsłużyć wyjątki przechwycone poza cyklem Razor życia składnika, przekaż wyjątek i DispatchExceptionAsync poczekaj na wynik:

try
{
    ...
}
catch (Exception ex)
{
    await DispatchExceptionAsync(ex);
}

Typowy scenariusz dla powyższego podejścia polega na tym, że składnik uruchamia operację asynchroniczną, ale nie oczekuje elementu , często nazywanego wzorcem Taskpożaru i zapominania, ponieważ metoda jest uruchamiana (uruchomiona), a wynik metody jest zapomniany (wyrzucany). Jeśli operacja nie powiedzie się, możesz chcieć, aby składnik potraktował błąd jako wyjątek cyklu życia składnika dla dowolnego z następujących celów:

  • Umieść składnik w stanie błędu, na przykład, aby wyzwolić granicę błędu.
  • Zakończ obwód, jeśli nie ma granicy błędu.
  • Wyzwól to samo rejestrowanie, które występuje w przypadku wyjątków cyklu życia.

W poniższym przykładzie użytkownik wybiera przycisk Wyślij raport , aby wyzwolić metodę w tle, ReportSender.SendAsyncktóra wysyła raport. W większości przypadków składnik oczekuje na Task wywołanie asynchroniczne i aktualizuje interfejs użytkownika, aby wskazać ukończoną operację. W poniższym przykładzie SendReport metoda nie oczekuje na element Task i nie zgłasza wyniku użytkownikowi. Ponieważ składnik celowo odrzuca Task element w SendReportelemencie , wszystkie błędy asynchroniczne występują poza normalnym stosem wywołań cyklu życia, dlatego nie są widoczne przez Blazorprogram :

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = ReportSender.SendAsync();
    }
}

Aby traktować błędy, takie jak wyjątki metody cyklu życia, jawnie wysyłaj wyjątki z powrotem do składnika za pomocą DispatchExceptionAsyncpolecenia , jak pokazano w poniższym przykładzie:

<button @onclick="SendReport">Send report</button>

@code {
    private void SendReport()
    {
        _ = SendReportAsync();
    }

    private async Task SendReportAsync()
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    }
}

Alternatywne podejście wykorzystuje metodę Task.Run:

private void SendReport()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await ReportSender.SendAsync();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Aby przeprowadzić demonstrację pracy, zaimplementuj przykład powiadomienia czasomierza w metodach składników Wywołaj zewnętrznie, aby zaktualizować stan. Blazor W aplikacji dodaj następujące pliki z przykładu powiadomienia czasomierza i zarejestruj usługi w pliku, jak wyjaśniono w Program sekcji:

  • TimerService.cs
  • NotifierService.cs
  • Notifications.razor

W przykładzie użyto czasomierza poza cyklem Razor życia składnika, w którym nieobsługiwany wyjątek zwykle nie jest przetwarzany przez Blazormechanizmy obsługi błędów, takie jak granica błędu.

Najpierw zmień kod, TimerService.cs aby utworzyć sztuczny wyjątek poza cyklem życia składnika. while W pętli TimerService.cs, wyrzuć wyjątek, gdy elapsedCount osiągnie wartość dwóch:

if (elapsedCount == 2)
{
    throw new Exception("I threw an exception! Somebody help me!");
}

Umieść granicę błędu w głównym układzie aplikacji. Zastąp <article>...</article> znacznik następującym znacznikiem.

W pliku MainLayout.razor:

<article class="content px-4">
    <ErrorBoundary>
        <ChildContent>
            @Body
        </ChildContent>
        <ErrorContent>
            <p class="alert alert-danger" role="alert">
                Oh, dear! Oh, my! - George Takei
            </p>
        </ErrorContent>
    </ErrorBoundary>
</article>

W Blazor usłudze Web Apps z granicą błędu zastosowaną tylko do składnika statycznego MainLayout granica jest aktywna tylko podczas fazy renderowania statycznego po stronie serwera (statycznego SSR). Granica nie aktywuje się tylko dlatego, że składnik dalej w hierarchii składników jest interaktywny. Aby ogólnie włączyć interakcyjność dla MainLayout składnika i reszty składników w dalszej części hierarchii składników, włącz interaktywne renderowanie HeadOutletRoutes wystąpień składników w składniku App (Components/App.razor). W poniższym przykładzie jest wdrażany tryb renderowania Interactive Server (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Jeśli uruchomisz aplikację w tym momencie, wyjątek zostanie zgłoszony, gdy liczba upłynięcia osiągnie wartość dwóch. Jednak interfejs użytkownika nie zmienia się. Granica błędu nie pokazuje zawartości błędu.

Aby wysłać wyjątki z usługi czasomierza z powrotem do Notifications składnika, do składnika są wprowadzane następujące zmiany:

  • Uruchom czasomierz w instrukcji try-catch. catch W klauzuli try-catch bloku wyjątki są wysyłane z powrotem do składnika, przekazując Exception element do DispatchExceptionAsync i oczekując na wynik.
  • W metodzie StartTimer uruchom asynchroniczną usługę czasomierza w Action delegacie Task.Run i celowo odrzucić zwrócony Taskelement .

StartTimer Metoda Notifications składnika (Notifications.razor):

private void StartTimer()
{
    _ = Task.Run(async () =>
    {
        try
        {
            await Timer.Start();
        }
        catch (Exception ex)
        {
            await DispatchExceptionAsync(ex);
        }
    });
}

Gdy usługa czasomierza jest wykonywana i osiąga liczbę dwóch, wyjątek jest wysyłany do Razor składnika, co z kolei wyzwala granicę błędu, aby wyświetlić zawartość <ErrorBoundary> błędu w składniku MainLayout :

Och, drogi! Och, ja! - George Takei