У меня есть приложение Blazor Server с основным компонентом макета, который содержит несколько дочерних компонентов. Каждый компонент регистрируется для события, определенного в статическом классе. Когда пользователь нажимает кнопку в Child1 (например), он изменяет некоторые данные в классе обслуживания, а затем вызывает статический класс, чтобы вызвать событие, которое затем вызывает повторную визуализацию Child2 (используя InvokeAsync(StateHasChanged) в своем обработчике событий). ), заставляя Child2 отражать новые данные. Другой подход заключается в том, чтобы каждый компонент регистрировался для события, которое определяется и инициируется службой данных, как описано в этом примере.
Есть ли здесь какая-либо причина отдавать предпочтение событию службы данных, а не статическому классу? Любой из них, кажется, работает, хотя, возможно, основанный на сервисе вариант немного более элегантный/жесткий. С другой стороны, версия статического класса может использоваться для внесения изменений в несколько служб данных.
Рассмотрите область действия вашего объекта отслеживания состояния и вашего компонента/страницы. В идеале они должны совпадать.
Ваш статический класс этого не делает. Он доступен всем пользователям и всем экземплярам страницы.
Мой первоначальный ответ, который вы здесь процитировали, тоже этого не делает. Это то, над чем я работаю какое-то время. DotNetCore не предоставляет нам DI-контейнер, который мы можем применить к компоненту/странице. OwningComponentBase
признает проблему и пытается ее решить, но не подходит для этой цели.
Рассмотрим следующую реализацию страницы счетчика.
Объект состояния счетчика:
public class CounterState : IDisposable
{
// This is here simply to demonstrate Dependancy Injection
private NavigationManager _navigationManager;
public int Counter { get; private set; }
public event EventHandler<CounterChangedEventArgs>? CounterChanged;
public CounterState(NavigationManager navigationManager)
=> _navigationManager = navigationManager;
public void IncrementCounter(object? sender)
{
this.Counter++;
this.CounterChanged?.Invoke(sender, CounterChangedEventArgs.Create(this.Counter));
}
// implementated to demonstrate dealing with an IDisposable State object
public void Dispose() { }
}
И обычай EventArgs
public class CounterChangedEventArgs : EventArgs
{
public int CounterValue { get; set; }
public static CounterChangedEventArgs Create(int value)
=> new CounterChangedEventArgs { CounterValue = value };
}
Простой компонент отображения счетчика:
@implements IDisposable
<div class = "bg-dark text-white m-2 p-2">
<pre>Value : @_counterState?.Counter </pre>
</div>
@code {
[CascadingParameter] private CounterState _counterState { get; set; } = default!;
protected override void OnInitialized()
{
ArgumentNullException.ThrowIfNull(_counterState);
_counterState.CounterChanged += this.OnCounterChanged;
}
private void OnCounterChanged(object? sender, CounterChangedEventArgs e)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> _counterState.CounterChanged -= this.OnCounterChanged;
}
И компонент Incrementer:
<div class = "m-2 p-2">
<button class = "btn btn-primary" @onclick=this.OnIncrementCounter>Increment</button>
</div>
@code {
[CascadingParameter] private CounterState? _counterState { get; set; }
protected override void OnInitialized()
=> ArgumentNullException.ThrowIfNull(_counterState);
private Task OnIncrementCounter()
{
_counterState?.IncrementCounter(this);
return Task.CompletedTask;
}
}
И, наконец, обновленная страница Counter
:
@page "/counter"
@implements IDisposable
@inject IServiceProvider serviceProvider
<PageTitle>Counter</PageTitle>
<h1>Counter Page</h1>
<CascadingValue Value = "_counterState">
<CounterViewer />
<CounterViewer />
<CounterViewer />
<CounterViewer />
<CounterViewer />
<div class = "row">
<div class = "col-2">
<CounterButton />
</div>
<div class = "col-2">
<CounterButton />
</div>
<div class = "col-2">
<CounterButton />
</div>
<div class = "col-2">
<CounterButton />
</div>
</div>
</CascadingValue>
@code {
private CounterState? _counterState;
protected override void OnInitialized()
{
// demonstrates creating an object instance in the context of thw Service Container
// this will inject any defined dependencies (in our case the Scoped NavigationManager)
_counterState = ActivatorUtilities.CreateInstance<CounterState>(serviceProvider);
ArgumentNullException.ThrowIfNull(_counterState);
}
// This will get called by the Renderer when the page/component goes out of scope
public void Dispose()
=> _counterState?.Dispose();
}
Эта реализация:
Важной особенностью этого шаблона является сопоставление области действия объекта состояния с его компонентом-владельцем. Использование DI: Scoped
слишком широкое и Transient
слишком узкое [каждый компонент получает новый экземпляр]. Если вы каскадируете полученную службу Transient, которая реализует IDisposable
, вы создаете утечку памяти. Создание экземпляра с помощью ActivatorUtilities
решает проблемы с внедрением зависимостей. Однако вы несете ответственность за реализацию утилизации.
Здесь есть статья, в которой эта тема рассматривается более подробно — https://www.codeproject.com/Articles/5352916/Matching-Services-with-the-Blazor-Component-Scope.
Большое спасибо за ваше невероятно подробное объяснение. Я новичок в Blazor Server (и в веб-программировании в целом), и у меня крутая кривая обучения, связанная с управлением состоянием. Лампочка загорается благодаря вашему ответу. Я думаю, что теперь я «понял», но мне нужно больше опыта, чтобы быть уверенным. В любом случае, вы, безусловно, ответили на мой вопрос и указали, что то, что я делаю, нежизнеспособно.