Sdílet prostřednictvím


kontext synchronizace ASP.NET Core Blazor

Poznámka:

Toto není nejnovější verze tohoto článku. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Upozorňující

Tato verze ASP.NET Core se už nepodporuje. Další informace najdete v tématu .NET a .NET Core Zásady podpory. Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Důležité

Tyto informace se týkají předběžného vydání produktu, který může být podstatně změněn před komerčním vydáním. Microsoft neposkytuje žádné záruky, výslovné ani předpokládané, týkající se zde uváděných informací.

Aktuální verzi najdete ve verzi .NET 8 tohoto článku.

Blazor používá kontext synchronizace (SynchronizationContext) k vynucování jednoho logického vlákna provádění. Metody životního cyklu komponent a zpětná volání událostí aktivovaná architekturou Blazor se provádějí v kontextu synchronizace.

BlazorKontext synchronizace na straně serveru se pokouší emulovat jednovláknové prostředí tak, aby úzce odpovídal modelu WebAssembly v prohlížeči, což je jedno vlákno. Tato emulace je vymezena pouze na jednotlivé okruhy, což znamená, že dva různé okruhy mohou běžet paralelně. V každém časovém okamžiku v okruhu se práce provádí na přesně jednom vlákně, což dává dojem z jednoho logického vlákna. V rámci stejného okruhu se souběžně nespouštějí žádné dvě operace.

Nepoužívejte volání blokující vlákno

Obecně platí, že by se v komponentách neměly volat následující metody. Následující metody blokují vlákno provádění, a tím brání aplikaci pokračovat v práci, dokud se nedokončí základní úlohy (Task):

Poznámka:

V příkladech v dokumentaci k architektuře Blazor, ve kterých se používají metody blokující vlákno uvedené v této části, se tyto metody používají pouze pro účely ukázky, a nikoli jako doporučené pokyny pro kódování. Například v několika ukázkách kódu komponent se k simulaci dlouho běžících procesů volá metoda Thread.Sleep.

Externí volání metod komponent za účelem aktualizace stavu

Pokud je potřeba komponentu aktualizovat na základě externí události, jako je událost časovače nebo jiné oznámení, použijte metodu InvokeAsync, která odešle provádění kódu do kontextu synchronizace architektury Blazor. Podívejte se například na následující oznamovací službu, která může o aktualizovaném stavu informovat jakoukoli naslouchající komponentu. Metodu Update je možné zavolat odkudkoli z aplikace.

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;
}

Registrace služeb:

  • Pro vývoj na straně klienta zaregistrujte služby jako singletony v souboru na straně Program klienta:

    builder.Services.AddSingleton<NotifierService>();
    builder.Services.AddSingleton<TimerService>();
    
  • Pro vývoj na straně serveru zaregistrujte služby jako vymezené v souboru serveru Program :

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

K aktualizaci komponenty použijte NotifierService.

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;
    }
}

V předchozím příkladu:

  • Časovač je inicializován mimo Blazorkontext synchronizace s _ = Task.Run(Timer.Start).
  • NotifierService vyvolá metodu OnNotify komponenty. Metoda InvokeAsync slouží k přepnutí na správný kontext a zařazení vykreslení do fronty. Další informace najdete v tématu Vykreslování komponent ASP.NET Core Razor.
  • Komponenta implementuje rozhraní IDisposable. V metodě Dispose, kterou zavolá architektura při odstranění komponenty, se odhlásí odběr delegáta OnNotify. Další informace najdete v tématu Životní cyklus komponent ASP.NET Core Razor.
  • NotifierService zavolá metodu OnNotify komponenty mimo kontext synchronizace Blazor. Metoda InvokeAsync slouží k přepnutí na správný kontext a zařazení vykreslení do fronty. Další informace najdete v tématu Vykreslování komponent ASP.NET Core Razor.
  • Komponenta implementuje rozhraní IDisposable. V metodě Dispose, kterou zavolá architektura při odstranění komponenty, se odhlásí odběr delegáta OnNotify. Další informace najdete v tématu Životní cyklus komponent ASP.NET Core Razor.

Důležité

Razor Pokud komponenta definuje událost aktivovanou z vlákna na pozadí, může být komponenta nutná k zachycení a obnovení kontextu spuštění (ExecutionContext) v době registrace obslužné rutiny. Další informace najdete v tématu Volání InvokeAsync(StateHasChanged) způsobí, že se stránka vrátí do výchozí jazykové verze (dotnet/aspnetcore #28521).

Pokud chcete odeslat zachycené výjimky z pozadí TimerService do komponenty, aby se s výjimkami zacházeli jako s běžnými výjimkami událostí životního cyklu, podívejte se na popisovač zachycených výjimek mimo Razor oddíl životního cyklu komponenty.

Zpracování zachycených výjimek mimo Razor životní cyklus komponenty

Používá ComponentBase.DispatchExceptionAsync se v komponentě Razor ke zpracování výjimek vyvolaných mimo zásobník volání životního cyklu komponenty. To umožňuje kódu komponenty zacházet s výjimkami, jako by se jedná o výjimky metody životního cyklu. Následně Blazormohou mechanismy zpracování chyb, jako jsou hranice chyb, zpracovávat výjimky.

Poznámka:

ComponentBase.DispatchExceptionAsync se používá v Razor souborech komponent (.razor), které dědí z ComponentBase. Při vytváření komponent, které implement IComponent directly, použít RenderHandle.DispatchExceptionAsync.

Pokud chcete zpracovat zachycené výjimky mimo Razor životní cyklu komponenty, předejte výjimku DispatchExceptionAsync a čekejte na výsledek:

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

Běžným scénářem předchozího přístupu je situace, kdy komponenta spustí asynchronní operaci, ale neočekávanou metodu Task, která se často označuje jako metoda fire and forget , protože metoda se aktivuje (spustí) a výsledek metody se zapomene (vyhodí). Pokud operace selže, můžete chtít, aby komponenta zachází se selháním jako s výjimkou životního cyklu komponent pro některý z následujících cílů:

  • Vložte komponentu do chybného stavu, například pro aktivaci hranice chyby.
  • Ukončete okruh, pokud neexistuje žádná hranice chyby.
  • Aktivujte stejné protokolování, ke kterému dochází u výjimek životního cyklu.

V následujícím příkladu uživatel vybere tlačítko Odeslat sestavu a aktivuje metodu pozadí, ReportSender.SendAsynckterá odešle sestavu. Ve většině případů komponenta čeká na Task asynchronní volání a aktualizuje uživatelské rozhraní tak, aby indikuje dokončení operace. V následujícím příkladu metoda SendReport neočeká Task a nehlásí výsledek uživateli. Vzhledem k tomu, že komponenta záměrně zahodí Task in SendReport, dojde k asynchronním selháním ze zásobníku volání normálního životního cyklu, takže je nevidí Blazor:

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

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

Pokud chcete zacházet se selháními, jako jsou výjimky metody životního cyklu, explicitně odešlete výjimky zpět do komponenty, DispatchExceptionAsyncjak ukazuje následující příklad:

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

Alternativní přístup využívá Task.Run:

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

Pro funkční ukázku implementujte příklad oznámení časovače v metodách vyvolání komponent externě za účelem aktualizace stavu. Blazor V aplikaci přidejte následující soubory z příkladu oznámení časovače a zaregistrujte služby v Program souboru, jak vysvětluje část:

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

Příklad používá časovač mimo Razor životní cyklus komponenty, kdy neošetřená výjimka obvykle není zpracována Blazormechanismy zpracování chyb, jako je například hranice chyby.

Nejprve změňte kód TimerService.cs tak, aby se vytvořila umělá výjimka mimo životní cyklus komponenty. while Ve smyčce TimerService.csvyvolá výjimku, když elapsedCount dosáhne hodnoty dvou:

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

Umístěte do hlavního rozložení aplikace hranici chyby. <article>...</article> Nahraďte kód následujícím kódem.

V 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>

Ve Blazor službě Web Apps s chybovou hranicí použitou pouze pro statickou MainLayout komponentu je hranice aktivní pouze během fáze statického vykreslování na straně serveru (statická služba SSR). Hranice se neaktivuje, protože komponenta dále v hierarchii komponent je interaktivní. Pokud chcete pro komponentu MainLayout a zbytek komponent dál v hierarchii komponent povolit interaktivitu, povolte interaktivní vykreslování instancí HeadOutletRoutes komponent v App komponentě (Components/App.razor). Následující příklad přijímá režim vykreslování Interactive Server (InteractiveServer):

<HeadOutlet @rendermode="InteractiveServer" />

...

<Routes @rendermode="InteractiveServer" />

Pokud v tomto okamžiku spustíte aplikaci, vyvolá se výjimka, když uplynulý počet dosáhne hodnoty 2. Uživatelské rozhraní se ale nezmění. Hranice chyby nezobrazuje obsah chyby.

Pokud chcete odesílat výjimky ze služby časovače zpět do Notifications komponenty, provede se v této komponentě následující změny:

StartTimer Metoda Notifications komponenty (Notifications.razor):

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

Když se služba časovače spustí a dosáhne počtu dvou, výjimka se odešle do Razor komponenty, která zase aktivuje hranici chyby, aby se zobrazil obsah <ErrorBoundary> chyby komponenty MainLayout :

Oh, drahoušku! Oh, moje! - George Takei