//
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
//
namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.Reflection;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using MUnique.OpenMU.DataModel;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Services;
///
/// Abstract common base class for an edit page.
///
public abstract class EditBase : ComponentBase, IAsyncDisposable
{
private object? _model;
private Type? _type;
private bool _isOwningContext;
private IContext? _persistenceContext;
private CancellationTokenSource? _disposeCts;
private DataLoadingState _loadingState;
private Task? _loadTask;
private IDisposable? _modalDisposable;
private IDisposable? _navigationLockDisposable;
private enum DataLoadingState
{
NotLoadedYet,
LoadingStarted,
Loading,
Loaded,
NotFound,
Error,
Cancelled,
}
///
/// Gets or sets the identifier of the object which should be edited.
///
[Parameter]
public Guid Id { get; set; }
///
/// Gets or sets the of the object which should be edited.
///
[Parameter]
public string TypeString { get; set; } = string.Empty;
///
/// Gets or sets the persistence context provider which loads and saves the object.
///
[Inject]
public IPersistenceContextProvider PersistenceContextProvider { get; set; } = null!;
///
/// Gets or sets the modal service.
///
[Inject]
public IModalService ModalService { get; set; } = null!;
///
/// Gets or sets the toast service.
///
[Inject]
public IToastService ToastService { get; set; } = null!;
///
/// Gets or sets the loading overlay service.
///
[Inject]
public LoadingOverlayService LoadingOverlay { get; set; } = null!;
///
/// Gets or sets the configuration data source.
///
[Inject]
public IDataSource ConfigDataSource { get; set; } = null!;
///
/// Gets or sets the navigation manager.
///
[Inject]
public NavigationManager NavigationManager { get; set; } = null!;
///
/// Gets or sets the navigation history.
///
[Inject]
public NavigationHistory NavigationHistory { get; set; } = null!;
///
/// Gets or sets the java script runtime.
///
[Inject]
public IJSRuntime JavaScript { get; set; } = null!;
///
/// Gets or sets the logger.
///
[Inject]
public ILogger? Logger { get; set; }
///
/// Gets the data source of the type which is edited.
///
protected virtual IDataSource EditDataSource => this.ConfigDataSource;
///
/// Gets the model which should be edited.
///
protected object? Model => this._model;
///
/// Gets the type.
///
protected virtual Type? Type => this._type ??= this.DetermineTypeByTypeString();
///
public async ValueTask DisposeAsync()
{
this._navigationLockDisposable?.Dispose();
this._navigationLockDisposable = null;
await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._disposeCts?.Dispose();
this._disposeCts = null;
await (this._loadTask ?? Task.CompletedTask).ConfigureAwait(false);
await this.EditDataSource.DiscardChangesAsync().ConfigureAwait(true);
if (this._isOwningContext)
{
this._persistenceContext?.Dispose();
}
this._persistenceContext = null;
}
///
public override async Task SetParametersAsync(ParameterView parameters)
{
this._model = null;
await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false);
this._disposeCts?.Dispose();
await (this._loadTask ?? Task.CompletedTask).ConfigureAwait(true);
await base.SetParametersAsync(parameters).ConfigureAwait(true);
}
///
protected override async Task OnParametersSetAsync()
{
this._loadingState = DataLoadingState.LoadingStarted;
var cts = new CancellationTokenSource();
this._disposeCts = cts;
this._type = null;
this._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token), cts.Token);
await base.OnParametersSetAsync().ConfigureAwait(true);
}
///
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (this.Model is null)
{
return;
}
builder.OpenComponent(0);
builder.AddAttribute(1, nameof(Breadcrumb.Caption), this.Model.GetName());
builder.CloseComponent();
var downloadMarkup = this.GetDownloadMarkup();
var editorsMarkup = this.GetEditorsMarkup();
builder.AddMarkupContent(10, $"
{Resources.Edit} {this.Type!.GetTypeCaption()}
{downloadMarkup}{editorsMarkup}\r\n");
builder.OpenComponent>(11);
builder.AddAttribute(12, nameof(CascadingValue.Value), this._persistenceContext);
builder.AddAttribute(13, nameof(CascadingValue.IsFixed), this._isOwningContext);
RenderFragment childContent = builder2 =>
{
var sequence = 14;
this.AddFormToRenderTree(builder2, ref sequence);
};
builder.AddAttribute(14, nameof(CascadingValue.ChildContent), childContent);
builder.CloseComponent();
}
///
protected override Task OnInitializedAsync()
{
this._navigationLockDisposable = this.NavigationManager.RegisterLocationChangingHandler(this.OnBeforeInternalNavigationAsync);
return base.OnInitializedAsync();
}
///
/// Adds the form to the render tree.
///
/// The builder.
/// The current sequence.
protected abstract void AddFormToRenderTree(RenderTreeBuilder builder, ref int currentSequence);
///
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (this._loadingState is not DataLoadingState.Loading && this._modalDisposable is { } modal)
{
modal.Dispose();
this._modalDisposable = null;
}
if (this._loadingState == DataLoadingState.LoadingStarted)
{
this._loadingState = DataLoadingState.Loading;
await this.InvokeAsync(() =>
{
if (this._loadingState != DataLoadingState.Loaded)
{
this._modalDisposable = this.LoadingOverlay.ShowLoadingIndicator();
this.StateHasChanged();
}
}).ConfigureAwait(false);
}
await base.OnAfterRenderAsync(firstRender).ConfigureAwait(true);
}
///
/// Saves the changes.
///
protected async Task SaveChangesAsync()
{
try
{
if (this._persistenceContext is { } context)
{
var success = await context.SaveChangesAsync().ConfigureAwait(true);
var text = success ? Resources.SavedChanges : Resources.NoChangesToSave;
this.ToastService.ShowSuccess(text);
}
else
{
this.ToastService.ShowError(Resources.FailedByUninitializedContext);
}
}
catch (Exception ex)
{
this.Logger?.LogError(ex, $"Error during saving {this.Id}");
var text = string.Format(Resources.UnexpectedErrorOccurred, ex.Message);
this.ToastService.ShowError(text);
}
}
///
/// Refreshes the data by discarding changes and reloading it from the database.
///
protected async Task RefreshAsync()
{
var isConfirmed = await this.JavaScript.InvokeAsync("window.confirm", Resources.UnsavedChangesQuestion).ConfigureAwait(true);
if (!isConfirmed)
{
return;
}
await this.EditDataSource.ForceDiscardChangesAsync().ConfigureAwait(true);
this._loadingState = DataLoadingState.LoadingStarted;
var cts = new CancellationTokenSource();
this._disposeCts = cts;
this._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token), cts.Token);
this.StateHasChanged();
}
///
/// Gets the optional editors markup for the current type.
///
/// The optional editors markup for the current type.
protected virtual string? GetEditorsMarkup()
{
return null;
}
///
/// It loads the owner of the .
///
/// The cancellation token.
protected virtual async ValueTask LoadOwnerAsync(CancellationToken cancellationToken)
{
await this.EditDataSource.GetOwnerAsync(Guid.Empty, cancellationToken).ConfigureAwait(true);
}
private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext context)
{
if (this._persistenceContext?.HasChanges is true)
{
var isConfirmed = await this.JavaScript.InvokeAsync(
"window.confirm",
Resources.UnsavedChangesQuestion)
.ConfigureAwait(true);
if (!isConfirmed)
{
context.PreventNavigation();
}
else if (this._isOwningContext)
{
this._persistenceContext.Dispose();
this._persistenceContext = null;
}
else
{
await this.EditDataSource.DiscardChangesAsync().ConfigureAwait(true);
}
}
}
private string? GetDownloadMarkup()
{
if (this.Type is not null && GenericControllerFeatureProvider.SupportedTypes.Any(t => t.Item1 == this.Type))
{
var uri = $"/download/{this.Type.Name}/{this.Type.Name}_{this.Id}.json";
return $"
{Resources.DownloadAsJson}:
";
}
return null;
}
private Type? DetermineTypeByTypeString()
{
return AppDomain.CurrentDomain.GetAssemblies().Where(assembly => assembly.FullName?.StartsWith(nameof(MUnique)) ?? false)
.Select(assembly => assembly.GetType(this.TypeString)).FirstOrDefault(t => t != null);
}
private async Task LoadDataAsync(CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
if (this.Type is null)
{
throw new InvalidOperationException($"Only types of namespace {nameof(MUnique)} can be edited on this page.");
}
await this.LoadOwnerAsync(cancellationToken).ConfigureAwait(true);
cancellationToken.ThrowIfCancellationRequested();
if (this.EditDataSource.IsSupporting(this.Type))
{
this._isOwningContext = false;
this._persistenceContext = await this.EditDataSource.GetContextAsync(cancellationToken).ConfigureAwait(true);
}
else
{
this._isOwningContext = true;
var gameConfiguration = await this.ConfigDataSource.GetOwnerAsync(Guid.Empty, cancellationToken).ConfigureAwait(true);
this._persistenceContext = this.PersistenceContextProvider.CreateNewTypedContext(this.Type, true, gameConfiguration);
}
cancellationToken.ThrowIfCancellationRequested();
try
{
if (this.EditDataSource.IsSupporting(this.Type))
{
this._model = this.Id == default
? this.EditDataSource.GetAll(this.Type).OfType