Создание объекта (ViewModel) один раз для каждой схемы Blazor

В настоящее время я пишу небольшой веб-интерфейс Blazor. Я пытаюсь внедрить / реализовать шаблон MVVM. Для этого я создал два базовых класса компонентов. Один, который просто обрабатывает методы жизненного цикла Blazor (добавляет некоторую обработку исключений), а другой обрабатывает инициализацию ViewModel на основе выполнения этих методов жизненного цикла. Компонент также реализует интерфейс IDisposable, который автоматически вызывается Blazor, когда компонент становится невидимым.

Вот фрагмент кода из моего класса WebComponentBase и класса ViewModelAwareComponent, чтобы примерно дать представление о шаблоне:


    public abstract class WebFpComponentBase : ComponentBase, IDisposable, IHtmlStyles
    {

         private const string DEFAULT_DIM_VALUE = "auto";

         [Inject]
         protected IExceptionUiHandler<ErrorRedirectViewModel> ExceptionUiHandler { get; set; }

         //fields and parameters omitted for brevity

         #region Blazor Component LifeCycle
         /// <summary>
         /// OnInitialized is called after OnInitialized, when the component has received all initial parameters. Place any asynchronous operations here,
         /// which require the component to re-render.
         /// </summary>
         /// <returns></returns>
         protected override async Task OnInitializedAsync()
         {
              try
              {
                  await base.OnInitializedAsync();
                  _logger.Info($"{nameof(OnInitializedAsync)} - method invoked in component of type ${this.GetType().FullName}");
                  await OnInitializedInternalAsync();
                  _logger.Info($"{nameof(OnInitializedAsync)} - method finished in component of type ${this.GetType().FullName}");
              } catch(Exception ex)
              {
                 //Exception, if any happend, is forwared using the IExceptionUiHandler in the AfterRenderAsync() method
                  _forwardException = ex;
                  _logger.Error($"{nameof(OnInitializedAsync)}: Catching and forwarding exception of type {_forwardException.GetType().FullName}");
              }
         }

         protected abstract Task OnInitializedInternalAsync();
         //other methods of the class omitted for brevity
    }

Затем мой ViewModelAwareComponent, у которого есть свойство, содержащее ViewModel, и который автоматически запускает инициализацию и деинициализацию ViewModel (закрытие любых соединений службы, сброс любых значений и т. Д.) Путем реализации внутренних абстрактных методов [BlazorLifecycleMethod].

public abstract class ViewModelAwareComponent<TViewModel> : WebFpComponentBase where TViewModel : BaseViewModel
{
    private Logger _logger = LogManager.GetCurrentClassLogger();

    [Parameter]
    public virtual TViewModel ViewModel { get; set; }

    protected override async Task OnInitializedInternalAsync()
    {
        await ViewModel.InitializeAsync();
        ViewModel.PropertyChanged += this.FireComponentStateHasChanged;
    }

    public override async void Dispose()
    {
        base.Dispose();
        await ViewModel.DeactivateAsync();
    }

    protected virtual async void FireComponentStateHasChanged(object sender, PropertyChangedEventArgs e)
    {
        _logger.Trace($"FireComponentStateHasChanged: property {e.PropertyName} has changed!");
        await InvokeAsync(this.StateHasChanged);
    }

    protected override async Task OnParametersSetAsyncInternal()
    {
        if (ViewModel == null)
        {
            throw new ArgumentNullException($"{nameof(ViewModel)}", "Parameter must be supplied!");
        }
    }
}

BaseViewModel-Type реализует INotifyPropertyChanged только обычным образом. У меня есть «MainViewModel», который должен быть создан только один раз для всего соединения (схема Blazor). Поэтому я добавил его через services.AddScoped() в Startup.cs. Поскольку он не привязан к какому-либо конкретному компоненту, я вставляю его в свой MainLayout.razor, который является макетом для каждого компонента Razor, который я написал.

Макет реализует protected override async Task OnInitializedAsync(), как указано ниже:

protected override async Task OnInitializedAsync()
{
    MainViewModel.PropertyChanged += (obj, args) => InvokeAsync(StateHasChanged);
    await MainViewModel.InitializeAsync();
    //some other stuff happening afterwards
}

Моя проблема сейчас в том, что инициализация и выполняется дважды при каждом запуске приложения, а не только один раз за соединение. То же самое верно и для деинициализации, потому что Dispose () компонента вызывается также дважды.

При отладке я заметил, что OnInitializedAsync не вызывается повторно при переключении между двумя моими существующими страницами (маршрутизируемыми компонентами). Он вызывается только дважды при запуске.

Может быть, у вас есть предложения по этому поводу? Есть ли лучший способ добиться желаемого поведения для MainViewModel?

С уважением,

наклон


person tilt32    schedule 26.09.2019    source источник
comment
Почему вы называете await MainViewModel.InitializeAsync(); в макете OnInitializedAsync?   -  person agua from mars    schedule 26.09.2019
comment
Может дело в пре-рендере? См. Мой ответ здесь: stackoverflow.com/a/57696602/842935   -  person dani herrera    schedule 26.09.2019
comment
@aguafrommars MainViewModel действительно содержит соединение службы WCF, которое необходимо создать до запуска приложения. Поскольку ViewModel не принадлежит какому-либо конкретному представлению (это больше похоже на оболочку, которая включает другие ViewModel, используемые в других компонентах), я подумал, что лучше всего инициализировать ее там (создать соединение с сервисом). Есть ли способ лучше решить такую ​​тему по поводу сервиса?   -  person tilt32    schedule 27.09.2019
comment
Хорошо, но MainViewModel - это ComponentBase, верно? Итак, при рендеринге вызывается метод InitializeAsync. ИМХО, не надо это называть.   -  person agua from mars    schedule 27.09.2019
comment
@aguafrommars MainViewModel не является производным от ComponentBase. Он не представляет собой каких-либо UI-элементов. Он действительно содержит данные для пользовательского интерфейса, выполняет некоторую логику для преобразования данных перед представлением (при необходимости), команды, которые должны выполняться после определенных событий пользовательского интерфейса. Метод InitializeAsync - это виртуальный метод, определенный в базовом классе (BaseViewModel), дочерние классы которого могут переопределить, если необходимо выполнить какие-либо действия по инициализации, прежде чем ViewModel можно будет использовать внутри компонента. Компонент может внедрить ViewModel через DI ядра asp.net по умолчанию.   -  person tilt32    schedule 27.09.2019
comment
Извините, я думал, что это ViewModel вашей главной страницы.   -  person agua from mars    schedule 27.09.2019
comment
Нет проблем, думаю, в моем посте это четко не описано;)   -  person tilt32    schedule 27.09.2019


Ответы (2)


Я вижу, что на это есть ответ, но мы также достигли этого, написав небольшой обработчик схемы Blazor и затем сопоставив возвращенный идентификатор схемы с услугой сеанса с заданной областью действия. Обработчик цепи предоставляет события при создании (или отключении / повторном подключении) новой цепи.

https://docs.microsoft.com/en-us/aspnet/core/blazor/advanced-scenarios?view=aspnetcore-5.0.

person Craig Johnson    schedule 01.02.2021

Ответил комментарий Дани Эррера:

Может дело в пре-рендере? См. Мой ответ здесь: https://stackoverflow.com/a/57696602/842935

Отключите предварительную визуализацию, изменив @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered)) на @(await Html.RenderComponentAsync(RenderMode.Server)). Теперь OnInitializedAsync () MainLayout.razor вызывается только один раз.

person tilt32    schedule 27.09.2019