You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Components/Components/Popover/PopoverService.cs

196 lines
5.8 KiB

// Copyright (c) MudBlazor 2021
// MudBlazor licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Connected.Components;
public interface IPopoverService
{
PopoverHandler Register(RenderFragment fragment);
Task<bool> Unregister(PopoverHandler hanlder);
ValueTask<int> CountProviders();
bool ThrowOnDuplicateProvider { get; }
IEnumerable<PopoverHandler> Handlers { get; }
Task InitializeIfNeeded();
event EventHandler FragmentsChanged;
}
public class PopoverHandler
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
private readonly IJSRuntime _runtime;
private readonly Action _updater;
private bool _detached;
public Guid Id { get; }
public RenderFragment Fragment { get; private set; }
public bool IsConnected { get; private set; }
public string Class { get; private set; }
public string Style { get; private set; }
public object Tag { get; private set; }
public bool ShowContent { get; private set; }
public DateTime? ActivationDate { get; private set; }
public Dictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
public Render ElementReference { get; set; }
public PopoverHandler(RenderFragment fragment, IJSRuntime jsInterop, Action updater)
{
Fragment = fragment ?? throw new ArgumentNullException(nameof(fragment));
_runtime = jsInterop ?? throw new ArgumentNullException(nameof(jsInterop));
_updater = updater ?? throw new ArgumentNullException(nameof(updater));
Id = Guid.NewGuid();
}
public void SetComponentBaseParameters(UIComponent componentBase, string @class, string style, bool showContent)
{
Class = @class;
Style = style;
Tag = componentBase.Tag;
UserAttributes = componentBase.UserAttributes;
ShowContent = showContent;
if (showContent == true)
{
ActivationDate = DateTime.Now;
}
else
{
ActivationDate = null;
}
}
public void UpdateFragment(RenderFragment fragment,
UIComponent componentBase, string @class, string style, bool showContent)
{
Fragment = fragment;
SetComponentBaseParameters(componentBase, @class, @style, showContent);
// this basically calls StateHasChanged on the Popover
ElementReference?.ForceRender();
_updater?.Invoke(); // <-- this doesn't do anything anymore except making unit tests happy
}
public async Task Initialize()
{
await _semaphore.WaitAsync();
try
{
if (_detached)
{
// If _detached is True, it means Detach() was invoked before Initialize() has had
// a chance to run. In this case, we just want to return and not do anything else
// otherwise we will end up with a memory leak.
return;
}
IsConnected = await _runtime.InvokeVoidAsyncWithErrorHandling("mudPopover.connect", Id); ;
}
finally
{
_semaphore.Release();
}
}
public async Task Detach()
{
await _semaphore.WaitAsync();
try
{
_detached = true;
if (IsConnected)
{
await _runtime.InvokeVoidAsyncWithErrorHandling("mudPopover.disconnect", Id);
}
}
finally
{
IsConnected = false;
_semaphore.Release();
}
}
}
public class PopoverService : IPopoverService, IAsyncDisposable
{
private Dictionary<Guid, PopoverHandler> _handlers = new();
private bool _isInitialized = false;
private readonly IJSRuntime _jsRuntime;
private readonly PopoverOptions _options;
private SemaphoreSlim _semaphoreSlim = new(1, 1);
public event EventHandler FragmentsChanged;
public bool ThrowOnDuplicateProvider => _options.ThrowOnDuplicateProvider;
public IEnumerable<PopoverHandler> Handlers => _handlers.Values.AsEnumerable();
public PopoverService(IJSRuntime jsInterop, IOptions<PopoverOptions> options = null)
{
this._options = options?.Value ?? new PopoverOptions();
_jsRuntime = jsInterop ?? throw new ArgumentNullException(nameof(jsInterop));
}
public async Task InitializeIfNeeded()
{
if (_isInitialized == true) { return; }
try
{
await _semaphoreSlim.WaitAsync();
if (_isInitialized == true) { return; }
await _jsRuntime.InvokeVoidAsyncWithErrorHandling("mudPopover.initialize", _options.ContainerClass, _options.FlipMargin);
_isInitialized = true;
}
finally
{
_semaphoreSlim.Release();
}
}
public PopoverHandler Register(RenderFragment fragment)
{
var handler = new PopoverHandler(fragment, _jsRuntime, () => { /*not doing anything on purpose for now*/ });
_handlers.Add(handler.Id, handler);
FragmentsChanged?.Invoke(this, EventArgs.Empty);
return handler;
}
public async Task<bool> Unregister(PopoverHandler handler)
{
if (handler == null) { return false; }
if (_handlers.Remove(handler.Id) == false) { return false; }
await handler.Detach();
FragmentsChanged?.Invoke(this, EventArgs.Empty);
return true;
}
public async ValueTask<int> CountProviders()
{
if (!_isInitialized) { return -1; }
var (success, value) = await _jsRuntime.InvokeAsyncWithErrorHandling<int>("mudpopoverHelper.countProviders");
if (success)
return value;
return 0;
}
//TO DO add js test
[ExcludeFromCodeCoverage]
public async ValueTask DisposeAsync()
{
if (_isInitialized == false) { return; }
await _jsRuntime.InvokeVoidAsyncWithErrorHandling("mudPopover.dispose");
}
}