ant-design-blazor/components/affix/Affix.razor.cs
Tony Yip 4b04f09832 perf: avoid memory leak issue of event listener (#1857)
* perf: avoid memory leak #1834

Avoid memory leak by remove the exclusive parameter and logic in the code block on AddEventListener method in DomEventService class.

The following are the components affected:
components/affix/Affix.razor.cs
components/anchor/Anchor.razor.cs
components/carousel/Carousel.razor.cs
components/core/Component/Overlay/Overlay.razor.cs
components/core/Component/Overlay/OverlayTrigger.razor.cs
components/core/JsInterop/DomEventService.cs
components/descriptions/Descriptions.razor.cs
components/dropdown/DropdownButton.cs
components/grid/Row.razor.cs
components/input/Input.cs
components/input/TextArea.razor.cs
components/layout/Sider.razor.cs
components/list/ListItem.razor.cs
components/select/Select.razor.cs
components/select/internal/SelectContent.razor.cs
components/slider/Slider.razor.cs
components/table/Table.razor.cs
components/tabs/Tabs.razor.cs

* fix override AddEventListener method in AntDesign.TestKit project

* add register/remove event listerner for exclusive use in DomEventService class

* move _dotNetObjects to DomEventListerner class/service, so that users not required to maintain it in each component.

* * move share/reuse dom event listerner methods to DomEventListerner class

* remove method 'AddEventListener' that no longer exists in DomEventService class in AntDesign.TestKit project

* * change the component referring to an IDomEventListerner interface instead of a concrete class,
  so that the component can be tested via a mock TestDomEventListerner.

* introduce DisposeShared and Dispose method in DomEventListerner to ease user remove callback from DomEventListerner

* register IDomEventListerner into DI container instead of create manually

* fix FormatKey

* fix FormatKey

* fix tests

* fix test

* fix test

Co-authored-by: James Yeung <shunjiey@hotmail.com>
2021-09-09 12:56:11 +08:00

195 lines
5.8 KiB
C#

using System.Text.Json;
using System.Threading.Tasks;
using AntDesign.JsInterop;
using Microsoft.AspNetCore.Components;
namespace AntDesign
{
public partial class Affix : AntDomComponentBase
{
private const string PrefixCls = "ant-affix";
private const string RootScollSelector = "window";
private bool _affixed;
private bool _rootListened;
private bool _targetListened;
private bool Affixed
{
get => _affixed;
set
{
if (_affixed != value)
{
_affixed = value;
if (OnChange.HasDelegate)
{
OnChange.InvokeAsync(_affixed);
}
}
}
}
private ElementReference _childRef;
private string _hiddenStyle;
private string _affixStyle;
[Inject]
private IDomEventListener DomEventListener { get; set; }
#region Parameters
/// <summary>
/// Offset from the bottom of the viewport (in pixels)
/// </summary>
[Parameter]
public int OffsetBottom { get; set; }
/// <summary>
/// Offset from the top of the viewport (in pixels)
/// </summary>
[Parameter]
public int OffsetTop { get; set; }
[Parameter]
public string TargetSelector { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<bool> OnChange { get; set; }
#endregion Parameters
protected override void OnInitialized()
{
base.OnInitialized();
ClassMapper
.If(PrefixCls, () => _affixed);
}
public async override Task SetParametersAsync(ParameterView parameters)
{
await base.SetParametersAsync(parameters);
}
protected async override Task OnFirstAfterRenderAsync()
{
await base.OnFirstAfterRenderAsync();
if (ChildContent == null)
{
return;
}
var domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.GetBoundingClientRect, _childRef);
_hiddenStyle = $"width: {domRect.Width}px; height: {domRect.Height}px;";
await RenderAffixAsync();
if (!_rootListened && string.IsNullOrEmpty(TargetSelector))
{
DomEventListener.AddShared<JsonElement>(RootScollSelector, "scroll", OnWindowScroll);
DomEventListener.AddShared<JsonElement>(RootScollSelector, "resize", OnWindowResize);
_rootListened = true;
}
else if (!string.IsNullOrEmpty(TargetSelector))
{
DomEventListener.AddExclusive<JsonElement>(TargetSelector, "scroll", OnTargetScroll);
DomEventListener.AddExclusive<JsonElement>(TargetSelector, "resize", OnTargetResize);
_targetListened = true;
}
}
private async void OnWindowScroll(JsonElement obj) => await RenderAffixAsync();
private async void OnWindowResize(JsonElement obj) => await RenderAffixAsync();
private async void OnTargetScroll(JsonElement obj) => await RenderAffixAsync();
private async void OnTargetResize(JsonElement obj) => await RenderAffixAsync();
private async Task RenderAffixAsync()
{
var originalAffixStyle = _affixStyle;
DomRect domRect = null;
Window window = null;
async Task GetWindow()
{
window = await JsInvokeAsync<Window>(JSInteropConstants.GetWindow);
}
async Task GetDomReact()
{
domRect = await JsInvokeAsync<DomRect>(JSInteropConstants.GetBoundingClientRect, Ref);
}
await Task.WhenAll(new[] { GetWindow(), GetDomReact() });
if (domRect == null || window == null)
{
return;
}
_hiddenStyle = $"width: {domRect.Width}px; height: {domRect.Height}px;";
DomRect containerRect;
if (string.IsNullOrEmpty(TargetSelector))
{
containerRect = new DomRect()
{
Top = 0,
Bottom = window.innerHeight,
Height = window.innerHeight,
};
}
else
{
containerRect = await JsInvokeAsync<DomRect>(JSInteropConstants.GetBoundingClientRect, TargetSelector);
}
var topDist = containerRect.Top + OffsetTop;
var bottomDist = containerRect.Bottom - OffsetBottom;
if (OffsetBottom > 0) // only affix bottom
{
if (domRect.Bottom > bottomDist)
{
_affixStyle = _hiddenStyle + $"bottom: { window.innerHeight - bottomDist}px; position: fixed;";
Affixed = true;
}
else
{
_affixStyle = string.Empty;
Affixed = false;
}
}
else if (domRect.Top < topDist)
{
_affixStyle = _hiddenStyle + $"top: {topDist}px; position: fixed;";
Affixed = true;
}
else
{
_affixStyle = string.Empty;
Affixed = false;
}
if (originalAffixStyle != _affixStyle)
{
StateHasChanged();
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
DomEventListener.Dispose();
}
}
}