baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,435 @@
// <copyright file="EditBase.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
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;
/// <summary>
/// Abstract common base class for an edit page.
/// </summary>
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,
}
/// <summary>
/// Gets or sets the identifier of the object which should be edited.
/// </summary>
[Parameter]
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the <see cref="Type.FullName"/> of the object which should be edited.
/// </summary>
[Parameter]
public string TypeString { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the persistence context provider which loads and saves the object.
/// </summary>
[Inject]
public IPersistenceContextProvider PersistenceContextProvider { get; set; } = null!;
/// <summary>
/// Gets or sets the modal service.
/// </summary>
[Inject]
public IModalService ModalService { get; set; } = null!;
/// <summary>
/// Gets or sets the toast service.
/// </summary>
[Inject]
public IToastService ToastService { get; set; } = null!;
/// <summary>
/// Gets or sets the loading overlay service.
/// </summary>
[Inject]
public LoadingOverlayService LoadingOverlay { get; set; } = null!;
/// <summary>
/// Gets or sets the configuration data source.
/// </summary>
[Inject]
public IDataSource<GameConfiguration> ConfigDataSource { get; set; } = null!;
/// <summary>
/// Gets or sets the navigation manager.
/// </summary>
[Inject]
public NavigationManager NavigationManager { get; set; } = null!;
/// <summary>
/// Gets or sets the navigation history.
/// </summary>
[Inject]
public NavigationHistory NavigationHistory { get; set; } = null!;
/// <summary>
/// Gets or sets the java script runtime.
/// </summary>
[Inject]
public IJSRuntime JavaScript { get; set; } = null!;
/// <summary>
/// Gets or sets the logger.
/// </summary>
[Inject]
public ILogger<EditBase>? Logger { get; set; }
/// <summary>
/// Gets the data source of the type which is edited.
/// </summary>
protected virtual IDataSource EditDataSource => this.ConfigDataSource;
/// <summary>
/// Gets the model which should be edited.
/// </summary>
protected object? Model => this._model;
/// <summary>
/// Gets the type.
/// </summary>
protected virtual Type? Type => this._type ??= this.DetermineTypeByTypeString();
/// <inheritdoc />
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;
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (this.Model is null)
{
return;
}
builder.OpenComponent<Breadcrumb>(0);
builder.AddAttribute(1, nameof(Breadcrumb.Caption), this.Model.GetName());
builder.CloseComponent();
var downloadMarkup = this.GetDownloadMarkup();
var editorsMarkup = this.GetEditorsMarkup();
builder.AddMarkupContent(10, $"<h1>{Resources.Edit} {this.Type!.GetTypeCaption()}</h1>{downloadMarkup}{editorsMarkup}\r\n");
builder.OpenComponent<CascadingValue<IContext>>(11);
builder.AddAttribute(12, nameof(CascadingValue<IContext>.Value), this._persistenceContext);
builder.AddAttribute(13, nameof(CascadingValue<IContext>.IsFixed), this._isOwningContext);
RenderFragment childContent = builder2 =>
{
var sequence = 14;
this.AddFormToRenderTree(builder2, ref sequence);
};
builder.AddAttribute(14, nameof(CascadingValue<IContext>.ChildContent), childContent);
builder.CloseComponent();
}
/// <inheritdoc />
protected override Task OnInitializedAsync()
{
this._navigationLockDisposable = this.NavigationManager.RegisterLocationChangingHandler(this.OnBeforeInternalNavigationAsync);
return base.OnInitializedAsync();
}
/// <summary>
/// Adds the form to the render tree.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="currentSequence">The current sequence.</param>
protected abstract void AddFormToRenderTree(RenderTreeBuilder builder, ref int currentSequence);
/// <inheritdoc />
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);
}
/// <summary>
/// Saves the changes.
/// </summary>
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);
}
}
/// <summary>
/// Refreshes the data by discarding changes and reloading it from the database.
/// </summary>
protected async Task RefreshAsync()
{
var isConfirmed = await this.JavaScript.InvokeAsync<bool>("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();
}
/// <summary>
/// Gets the optional editors markup for the current type.
/// </summary>
/// <returns>The optional editors markup for the current type.</returns>
protected virtual string? GetEditorsMarkup()
{
return null;
}
/// <summary>
/// It loads the owner of the <see cref="EditDataSource" />.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
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<bool>(
"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 $"<p>{Resources.DownloadAsJson}: <a href=\"{uri}\" download><span class=\"oi oi-data-transfer-download\"></span></a></p>";
}
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<object>().FirstOrDefault()
: this.EditDataSource.Get(this.Id);
}
else
{
this._model = this.Id == default
? (await this._persistenceContext.GetAsync(this.Type, cancellationToken).ConfigureAwait(true)).OfType<object>().FirstOrDefault()
: await this._persistenceContext.GetByIdAsync(this.Id, this.Type, cancellationToken).ConfigureAwait(true);
}
this._loadingState = this.Model is not null
? DataLoadingState.Loaded
: DataLoadingState.NotFound;
}
catch (OperationCanceledException)
{
this._loadingState = DataLoadingState.Cancelled;
throw;
}
catch (Exception ex)
{
this._loadingState = DataLoadingState.Error;
this.Logger?.LogError(ex, $"Could not load {this.Type.FullName} with {this.Id}: {ex.Message}{Environment.NewLine}{ex.StackTrace}");
await this.InvokeAsync(() => this.ModalService.ShowMessageAsync(Resources.Error, Resources.LoadingErrorCheckLog)).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(true);
}
catch (OperationCanceledException)
{
// expected when the page is getting disposed.
}
catch (TargetInvocationException ex) when (ex.InnerException is ObjectDisposedException)
{
// See ObjectDisposedException.
}
catch (ObjectDisposedException)
{
// Happens when the user navigated away (shouldn't happen with the modal loading indicator, but we check it anyway).
// It would be great to have an async api with cancellation token support in the persistence layer
// For the moment, we swallow the exception
}
catch (Exception ex)
{
this.Logger?.LogError(ex, "Unexpected error when loading data.");
}
}
}