Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,23 +1,84 @@
namespace Bit.BlazorUI;

/// <summary>
/// A wrapper serive around the <see cref="BitModalService"/> to enhance showing message boxes.
/// A wrapper service around the <see cref="BitProModalService"/> and <see cref="BitModalService"/> to enhance showing message boxes.
/// It works with either of these services when available and prefers the <see cref="BitProModalService"/> when both are available.
/// </summary>
public class BitMessageBoxService(BitModalService modalService)
/// <remarks>
/// A message box only appears if the modal container of the chosen service is mounted in the layout:
/// <see cref="BitProModalContainer"/> for the <see cref="BitProModalService"/> and <see cref="BitModalContainer"/>
/// for the <see cref="BitModalService"/>. Because the two services are usually both registered but a layout may
/// mount only one container, the selection (see <see cref="Show"/>) is driven by which container is actually
/// mounted rather than by which service is registered, so the message box is not silently swallowed.
/// </remarks>
public class BitMessageBoxService(BitProModalService? proModalService = null, BitModalService? modalService = null)
{
/// <summary>
/// Shows a <see cref="BitMessageBox"/> inside a <see cref="BitModal"/> using the <see cref="BitModalService"/>.
/// Shows a <see cref="BitMessageBox"/> inside a <see cref="BitProModal"/> using the <see cref="BitProModalService"/>,
/// otherwise inside a <see cref="BitModal"/> using the <see cref="BitModalService"/>.
/// </summary>
/// <remarks>
/// Both services are commonly registered together (registering the Extras services adds both), yet whether a
/// message box actually renders depends on which modal container is mounted, not on which service is registered:
/// a modal shown through a service whose container is absent is silently not rendered. To avoid that, the service
/// to use is chosen from container availability (<see cref="BitModalServiceBase{TReference, TParameters}.IsContainerAvailable"/>),
/// in this order:
/// <list type="number">
/// <item>the <see cref="BitProModalService"/> when its <see cref="BitProModalContainer"/> is mounted (preferred when both are);</item>
/// <item>otherwise the <see cref="BitModalService"/> when its <see cref="BitModalContainer"/> is mounted;</item>
/// <item>otherwise the only registered service — or, when both are registered but neither container is mounted, the
/// preferred <see cref="BitProModalService"/> (a misconfiguration that renders nothing regardless: mount a container).</item>
/// </list>
/// Container availability reflects live state and is reliable at the moment of a user-triggered show, since the
/// layout (and thus the container) has already rendered by then.
/// </remarks>
/// <exception cref="InvalidOperationException">Neither <see cref="BitProModalService"/> nor <see cref="BitModalService"/> is registered.</exception>
public async Task Show(string title, string body)
{
BitModalReference modalRef = default!;
Dictionary<string, object> parameters = new()
if (proModalService is null && modalService is null)
{
{ nameof(BitMessageBox.Title), title },
{ nameof(BitMessageBox.Body), body },
{ nameof(BitMessageBox.OnClose), EventCallback.Factory.Create(this, () => modalRef.Close()) }
};
throw new InvalidOperationException("Neither BitProModalService nor BitModalService is available. Register at least one of them to use BitMessageBoxService.");
}

modalRef = await modalService.Show<BitMessageBox>(parameters);
// Choose the service whose container is actually mounted (preferring the pro one), because a modal shown
// through a service without a mounted container is silently not rendered. Only when neither container is
// mounted do we fall back to the preferred registered service.
bool useProModal;
if (proModalService is null)
{
useProModal = false;
}
else if (modalService is null)
{
useProModal = true;
}
else if (modalService.IsContainerAvailable && !proModalService.IsContainerAvailable)
{
// Only the classic container is mounted: route to it so the message box actually renders.
useProModal = false;
}
else
{
// The pro container is mounted, or neither is (nothing renders either way): keep the preferred pro service.
useProModal = true;
}

// The parameters are built from the modal reference the service hands back, so the OnClose callback
// closes this very modal without a window where the reference isn't assigned yet.
if (useProModal)
{
await proModalService!.Show<BitMessageBox>(modalRef => BuildParameters(title, body, modalRef.Close));
}
else
{
await modalService!.Show<BitMessageBox>(modalRef => BuildParameters(title, body, modalRef.Close));
}
}

private Dictionary<string, object> BuildParameters(string title, string body, Func<Task> onClose) => new()
{
{ nameof(BitMessageBox.Title), title },
{ nameof(BitMessageBox.Body), body },
{ nameof(BitMessageBox.OnClose), EventCallback.Factory.Create(this, onClose) }
};
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
namespace Bit.BlazorUI;
namespace Bit.BlazorUI;

/// <summary>
/// A service to show any content inside a centralized <see cref="BitProModal"/> using <see cref="BitProModalContainer"/>.
/// </summary>
/// <remarks>
/// A <see cref="BitProModalContainer"/> must be mounted in the layout for shown modals to render: a non-persistent modal
/// shown while no container is mounted is silently not rendered (see the base type remarks). Use
/// <see cref="BitModalServiceBase{TReference, TParameters}.IsContainerAvailable"/> to check whether a container is
/// currently mounted before showing a modal.
/// </remarks>
public class BitProModalService : BitModalServiceBase<BitProModalReference, BitProModalParameters>
{
protected override BitProModalReference CreateReference(bool persistent)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
namespace Bit.BlazorUI;
namespace Bit.BlazorUI;

/// <summary>
/// A core service to show any content inside a centralized <see cref="BitModal"/> using <see cref="BitModalContainer"/>.
/// </summary>
/// <remarks>
/// A <see cref="BitModalContainer"/> must be mounted in the layout for shown modals to render: a non-persistent modal
/// shown while no container is mounted is silently not rendered (see the base type remarks). Use
/// <see cref="BitModalServiceBase{TReference, TParameters}.IsContainerAvailable"/> to check whether a container is
/// currently mounted before showing a modal.
/// </remarks>
public class BitModalService : BitModalServiceBase<BitModalReference, BitModalParameters>
{
protected override BitModalReference CreateReference(bool persistent)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;

namespace Bit.BlazorUI;

Expand Down Expand Up @@ -79,6 +79,24 @@ public void RemoveContainer(BitModalContainerBase<TReference, TParameters> conta
}
}

/// <summary>
/// Whether a modal container is currently mounted for this service, i.e. whether a <c>Show</c> call right now
/// would actually render its modal.
/// </summary>
/// <remarks>
/// This reflects LIVE state, not registration: the service being registered in DI does not imply a container
/// exists. A container attaches during its own initialization (after its first render) via <see cref="InitContainer"/>
/// and detaches on dispose via <see cref="RemoveContainer"/>, so this can be <c>false</c> very early in a render
/// cycle before any container has initialized, and it can flip back to <c>false</c> across a container remount.
/// It is therefore reliable at the moment of a user-triggered <c>Show</c> (the layout has long since rendered),
/// but should not be treated as a permanent guarantee.
/// <br/>
/// The main use is a caller that can show through more than one modal service (for example a wrapper that prefers
/// one service but can fall back to another) and wants to pick a service whose container is actually mounted,
/// because a non-persistent modal shown while this is <c>false</c> is silently not rendered (see the type remarks).
/// </remarks>
public bool IsContainerAvailable => _container is not null;

/// <summary>
/// Closes an already opened modal using its reference.
/// </summary>
Expand Down Expand Up @@ -147,7 +165,7 @@ public Task Refresh(TReference modalRef)
public Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
bool persistent = false) where T : IComponent
{
return Show<T>(null, null, persistent);
return Show<T>((Dictionary<string, object>?)null, null, persistent);
}

/// <summary>
Expand All @@ -174,7 +192,7 @@ public Task Refresh(TReference modalRef)
public Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
TParameters modalParameters) where T : IComponent
{
return Show<T>(null, modalParameters, false);
return Show<T>((Dictionary<string, object>?)null, modalParameters, false);
}

/// <summary>
Expand All @@ -183,22 +201,43 @@ public Task Refresh(TReference modalRef)
public Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
TParameters? modalParameters, bool persistent = false) where T : IComponent
{
return Show<T>(null, modalParameters, persistent);
return Show<T>((Dictionary<string, object>?)null, modalParameters, persistent);
}

/// <summary>
/// Shows a new modal with a custom component as its content with custom parameters for the custom component and the modal.
/// </summary>
public async Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
public Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
Dictionary<string, object>? parameters,
TParameters? modalParameters,
bool persistent = false) where T : IComponent
{
return Show<T>(_ => parameters, modalParameters, persistent);
}

/// <summary>
/// Shows a new modal, building the content component's parameters from a factory that receives the modal reference.
/// Use this overload when a parameter needs the reference itself (e.g. an <c>OnClose</c> callback that closes this
/// very modal): the reference is handed to the factory before the content is rendered, so the callback can never
/// observe an unassigned reference the way it can when the reference is only captured after Show returns.
/// </summary>
public async Task<TReference> Show<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(
Func<TReference, Dictionary<string, object>?> parametersFactory,
TParameters? modalParameters = null,
bool persistent = false) where T : IComponent
{
ArgumentNullException.ThrowIfNull(parametersFactory);

var componentType = typeof(T);

var modalReference = CreateReference(persistent);
modalReference.SetParameters(modalParameters);

// Build the content parameters with the reference already in hand so a parameter such as an OnClose
// callback can capture modalReference.Close directly, closing the window a caller would otherwise face
// when wiring the close callback only after Show returns.
var parameters = parametersFactory(modalReference);

var content = new RenderFragment(builder =>
{
var i = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,55 @@
</NotesTemplate>
<Examples>
<DemoExample Title="Basic" RazorCode="@example1RazorCode" Id="example1">
<div>The basic usage of the BitMessageBox, rendered inline inside a BitCard.</div>
<br />
<BitCard Style="padding:0">
<BitMessageBox Title="It's a title" Body="It's a body." />
</BitCard>
</DemoExample>

<DemoExample Title="BitModal" RazorCode="@example2RazorCode" CsharpCode="@example2CsharpCode" Id="example2">
<div>Show the BitMessageBox inside a BitModal component and close it using the OnClose event.</div>
<br />
<BitButton OnClick="() => isModalOpen = true">Show</BitButton>
<BitModal @bind-IsOpen="isModalOpen">
<BitMessageBox OnClose="() => isModalOpen = false" Title="This is the Title" Body="This is the Body!" />
</BitModal>
</DemoExample>

<DemoExample Title="BitModalService" RazorCode="@example3RazorCode" CsharpCode="@example3CsharpCode" Id="example3">
<DemoExample Title="BitProModal" RazorCode="@example3RazorCode" CsharpCode="@example3CsharpCode" Id="example3">
<div>Show the BitMessageBox inside a BitProModal component and close it using the OnClose event.</div>
<br />
<BitButton OnClick="() => isProModalOpen = true">Show</BitButton>
<BitProModal @bind-IsOpen="isProModalOpen">
<BitMessageBox OnClose="() => isProModalOpen = false" Title="This is the Title" Body="This is the Body!" />
</BitProModal>
</DemoExample>

<DemoExample Title="BitModalService" RazorCode="@example4RazorCode" CsharpCode="@example4CsharpCode" Id="example4">
<div>Show the BitMessageBox as the content of a modal shown by the BitModalService.</div>
<br />
<BitButton OnClick="ShowMessageBox">Show MessageBox</BitButton>
</DemoExample>

<DemoExample Title="BitMessageBoxService" RazorCode="@example4RazorCode" CsharpCode="@example4CsharpCode" Id="example4">
<DemoExample Title="BitProModalService" RazorCode="@example5RazorCode" CsharpCode="@example5CsharpCode" Id="example5">
<div>Show the BitMessageBox as the content of a pro modal shown by the BitProModalService.</div>
<br />
<BitButton OnClick="ShowProMessageBox">Show MessageBox</BitButton>
</DemoExample>

<DemoExample Title="BitMessageBoxService" RazorCode="@example6RazorCode" CsharpCode="@example6CsharpCode" Id="example6">
<div>
Show the BitMessageBox with a simple call to the Show method of the BitMessageBoxService.
This service uses the BitProModalService or the BitModalService (whichever is available) to show the message box,
preferring the BitProModalService when both are available.
</div>
<br />
<BitButton OnClick="ShowMessageBoxService">Show MessageBox</BitButton>
</DemoExample>

<DemoExample Title="Style & Class" RazorCode="@example5RazorCode" Id="example5">
<div>Customize the appearance of BitActionButton using styles and CSS classes.</div>
<DemoExample Title="Style & Class" RazorCode="@example7RazorCode" Id="example7">
<div>Customize the appearance of BitMessageBox using styles and CSS classes.</div>
<br />
<div>
<BitCard Style="padding:0">
Expand All @@ -63,7 +90,7 @@
</div>
</DemoExample>

<DemoExample Title="RTL" RazorCode="@example6RazorCode" Id="example6">
<DemoExample Title="RTL" RazorCode="@example8RazorCode" Id="example8">
<div>Use BitMessageBox in right-to-left (RTL).</div>
<br /><br />
<div dir="rtl">
Expand All @@ -73,4 +100,4 @@
</div>
</DemoExample>
</Examples>
</DemoPage>
</DemoPage>
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,28 @@ public partial class BitMessageBoxDemo


private bool isModalOpen;
private bool isProModalOpen;

[AutoInject] private BitModalService modalService { get; set; } = default!;
private async Task ShowMessageBox()
{
BitModalReference modalRef = default!;
Dictionary<string, object> parameters = new()
await modalService.Show<BitMessageBox>(modalRef => new()
{
{ nameof(BitMessageBox.Title), "This is a title" },
{ nameof(BitMessageBox.Body), "This is a body." },
{ nameof(BitMessageBox.OnClose), EventCallback.Factory.Create(this, () => modalRef.Close()) }
};
modalRef = await modalService.Show<BitMessageBox>(parameters);
{ nameof(BitMessageBox.OnClose), EventCallback.Factory.Create(this, modalRef.Close) }
});
}

[AutoInject] private BitProModalService proModalService { get; set; } = default!;
private async Task ShowProMessageBox()
{
await proModalService.Show<BitMessageBox>(modalRef => new()
{
{ nameof(BitMessageBox.Title), "This is a title" },
{ nameof(BitMessageBox.Body), "This is a body." },
{ nameof(BitMessageBox.OnClose), EventCallback.Factory.Create(this, modalRef.Close) }
});
}

[AutoInject] private BitMessageBoxService messageBoxService { get; set; } = default!;
Expand Down
Loading
Loading